У квантификаторов есть два режима работы:
По умолчанию движок регулярного выражения пытается повторить квантификатор столько раз, сколько это возможно. Например, \d+ получит все возможные цифры. Когда цифры закончатся или он дойдёт до конца строки, движок продолжит искать совпадение для оставшегося шаблона. Если совпадения не будет, он уменьшит количество повторов (осуществит возврат) и попробует снова.
pattern =
/".+"/g, str = a "witch" and her "broom" is one
Включается с помощью знака вопроса ? после квантификатора. Движок регулярного выражения пытается найти совпадение для оставшегося шаблона перед каждым повторением квантификатора.
Как мы увидели на примере поиска строк в кавычках, ленивый режим не «панацея» от всех проблем жадного поиска. В качестве альтернативы может выступать «хорошо настроенный» жадный поиск, как в шаблоне "[^"]+".
pattern =
/".+?"/g, str = a "witch" and her "broom" is one
На первый взгляд квантификаторы – это просто, но на самом деле это не совсем так.
Нужно очень хорошо разбираться, как работает поиск, если планируем искать что-то сложнее, чем \d+.
Давайте в качестве примера рассмотрим следующую задачу:
У нас есть текст, в котором нужно заменить все кавычки "..." на «ёлочки» «...», которые используются в типографике многих стран.
Например: "Привет, мир" должно превратиться в «Привет, мир». Есть и другие кавычки, вроде „Witam, świat!” (польский язык) или 「你好,世界」 (китайский язык), но для нашей задачи давайте выберем «...».
Первое, что нам нужно – это найти строки с кавычками, а затем мы сможем их заменить.
Регулярное выражение вроде /".+"/g (кавычка, какой-то текст, другая кавычка) может выглядеть хорошим решением, но это не так!
Давайте это проверим:
let regexp = /".+"/g;
let str = 'a "witch" and her "broom" is one';
alert( str.match(regexp) ); // "witch" and her "broom"…Как мы видим, регулярное выражение работает не как задумано!
Вместо того, чтобы найти два совпадения "witch" и "broom", было найдено одно: "witch" and her "broom".
Причину можно описать, как «жадность – причина всех зол».
a "witch" and her "broom" is one
Чтобы найти совпадение, движок регулярных выражений работает по следующему алгоритму:
- Для каждой позиции в строке для поиска:
- Попробовать найти совпадение с шаблоном на этой позиции.
- Если нет совпадения, переход к следующей позиции.
Эти общие слова никак не объясняют, почему регулярное выражение работает неправильно, так что давайте разберём подробно, как работает шаблон ".+".
- Первый символ шаблона – это кавычка
"
Движок регулярного выражения пытается найти его на нулевой позиции исходной строки a "witch" and her "broom" is one, но там – a`, так что совпадения нет.
Он продолжает: двигается к следующей позиции исходной строки и пытается найти первый символ шаблона там. У него не получается, он двигается дальше и, наконец, находит кавычку на третьей позиции:
- Кавычка замечена, после чего движок пытается найти совпадение для оставшегося шаблона. Смотрит, удовлетворяет ли остаток строки шаблону
.+"
В нашем случае следующий символ шаблона: . (точка). Она обозначает «любой символ, кроме новой строки», так что следующая буква строки 'w' подходит.
- Затем точка повторяется из-за квантификатора
.+.Движок регулярного выражения добавляет к совпадению один символ за другим
…До каких пор? Точке соответствуют любые символы, так что движок остановится только тогда, когда достигнет конца строки:
- Тогда он перестанет повторять
.+и попробует найти следующий символ шаблона. Это кавычка".Но есть проблема: строка для поиска закончилась, больше нет символов!
Движок регулярного выражения понимает, что захватил слишком много .+ и начинает отступать.
Другими словами, он сокращает совпадение по квантификатору на один символ:
Теперь он предполагает, что .+ заканчивается за один символ до конца строки и пытается сопоставить остаток шаблона для этой позиции.
Если бы тут была кавычка, тогда бы поиск закончился, но последний символ – это 'e', так что он не подходит.
- …Поэтому движок уменьшает количество повторений
.+ещё на один символ:
Кавычка '"' не соответствует 'n'
- Движок продолжает возвращаться: он уменьшает количество повторений
'.', пока оставшийся шаблон (в нашем случае'"') не совпадёт:
-
Совпадение найдено
-
Так что первое совпадение:
"witch" and her "broom". Если у регулярного выражения стоит флагg, то поиск продолжится с того места, где закончился предыдущий. В оставшейся строкеis oneнет кавычек, так что совпадений больше не будет
Это, определённо, не то, что мы ожидали. Но так оно работает.
В жадном режиме (по умолчанию) квантификатор повторяется столько раз, сколько это возможно
Движок регулярного выражения пытается получить максимальное количество символов, соответствующих .+, а затем сокращает это количество символ за символом, если остаток шаблона не совпадает.
В нашей задаче мы хотим другого. И нам поможет ленивый режим квантификатора
a "witch" and her "broom" is one
«Ленивый» режим противоположен «жадному». Он означает: «повторять квантификатор наименьшее количество раз».
Мы можем включить его, вставив знак вопроса '?' после квантификатора, то есть будет *? или +? или даже ?? для '?'.
Проясним: обычно знак вопроса ? сам по себе является квантификатором (ноль или один), но, если он добавлен после другого квантификатора (или даже после самого себя), он получает другое значение – он меняет режим совпадения с жадного на ленивый.
Регулярное выражение /".+?"/g работает как задумано, оно находит "witch" и "broom":
let regexp = /".+?"/g;
let str = 'a "witch" and her "broom" is one';
alert( str.match(regexp) ); // witch, broomЧтобы лучше понять, что поменялось, давайте рассмотрим процесс поиска шаг за шагом.
- Первый шаг будет таким же: движок находит начало шаблона
'"'на 3-ей позиции:
- Следующий шаг аналогичен: он найдёт совпадение для точки '.':
- А отсюда поиск продолжится по-другому. Из-за того, что у нас включён ленивый режим для +?, движок не будет пытаться найти совпадение для точки ещё раз, оно остановится и попробует найти совпадение для оставшегося шаблона
'"'прямо сейчас:
Если бы на этом месте была кавычка, то поиск бы закончился, но там находится 'i', то есть совпадения нет.
- Тогда движок регулярного выражения увеличит количество повторений для точки и попробует ещё раз:
Опять неудача. Тогда количество повторений будет увеличено ещё и ещё…
- …До тех пор, пока совпадение для оставшегося шаблона не будет найдено:
- Следующий поиск начнётся с того места, где закончилось текущее совпадение и у нас будет ещё один результат:
В этом примере мы увидели, как ленивый режим работает для +?. Квантификаторы *? и ?? работают аналогичным образом – движок регулярного выражения увеличит количество совпадений, только если не сможет найти совпадение для оставшегося шаблона на текущей позиции.
Ленивый режим включается только для квантификаторов с
?
Остальные квантификаторы остаются жадными.
Строка = "123 456" и регулярный паттерн \d+ \d+? выдаст при поиске по строке следующий результат: 123 4. Почему?
-
Шаблон
\d+пытается найти столько цифр, сколько возможно (жадный режим), так что он находит123и останавливается, потому что следующим символом будет пробел' ' -
Дальше в шаблоне пробел и в строке тоже, так что есть совпадение.
-
Затем идёт
\d+?. Квантификатор находится в ленивом режиме, так что он находит одну цифру4и проверяет, есть ли совпадение для оставшегося шаблона с этого места.…Но в шаблоне
\d+?больше ничего нет.Ленивый режим ничего не повторяет без необходимости. Шаблон закончился, заканчивается и поиск. Мы получаем
123 4.
С регулярными выражениями часто есть несколько путей добиться одного и того же результата.
В нашем случаем мы можем найти кавычки без использования ленивого режима с помощью регулярного выражения "[^"]+":
a "witch" and her "broom" is one
Регулярное выражение "[^"]+" получит нужный результат, потому что оно ищет кавычку ", за которой следует один или несколько символов «не-кавычек» [^"], а затем – закрывающая кавычка.
Движок регулярного выражения набирает, сколько может [^"]+ пока не встречает закрывающую кавычку, на которой останавливается.
Обратите внимание, что эта логика не заменяет ленивые квантификаторы!
Просто она работает по-другому. Временами нужен один вариант, временами – другой.
Давайте посмотрим пример, в котором ленивый квантификатор не справляется, а этот вариант работает правильно.
Например, мы хотим найти ссылки вида <a href="..." class="doc">, с произвольным href.
Какое регулярное выражение нам нужно использовать?
Первой мыслью может быть: /<a href=".*" class="doc">/g.
Давайте проверим:
let str = '...<a href="link" class="doc">...';
let regexp = /<a href=".*" class="doc">/g;
// Работает!
alert( str.match(regexp) ); // <a href="link" class="doc">Регулярное выражение работает. Но давайте посмотрим, что произойдёт, если в тексте будет много ссылок?
let str = '...<a href="link1" class="doc">... <a href="link2" class="doc">...';
let regexp = /<a href=".*" class="doc">/g;
// Упс! Две ссылки в одном совпадении!
alert( str.match(regexp) ); // <a href="link1" class="doc">... <a href="link2" class="doc">В данном случае мы получили неправильный результат по той же причине, что в примере с «witches». Квантификатор .* забирает слишком много символов.
Совпадение будет выглядеть так:
Давайте изменим шаблон, сделав квантификатор ленивым .*?:
let str = '...<a href="link1" class="doc">... <a href="link2" class="doc">...';
let regexp = /<a href=".*?" class="doc">/g;
// Работает!
alert( str.match(regexp) ); // <a href="link1" class="doc">, <a href="link2" class="doc">Теперь кажется, что всё работает правильно. У нас есть два совпадения:
…Но давайте попробуем его на ещё одном тексте:
let str = '...<a href="link1" class="wrong">... <p style="" class="doc">...';
let regexp = /<a href=".*?" class="doc">/g;
// Неправильное совпадение!
alert( str.match(regexp) ); // <a href="link1" class="wrong">... <p style="" class="doc">Ну вот, ленивый квантификатор нас подвёл. В совпадении находится не только ссылка, но и текст после неё, включая <p...>.
Почему?
Происходит следующее:
- Первым делом регулярное выражение находит начало ссылки
<a href=". - Затем оно ищет
.*?, берёт один символ (лениво!) и проверяет, есть ли совпадение для" class="doc">(нет). - Затем берёт другой символ для
.*?, и так далее… пока не достигнет" class="doc">. Поиск завершён.
Но с этим есть проблема: конец совпадения находится уже за границей ссылки <a...>, вообще в другом теге <p>. Что нам не подходит.
Вот как совпадение выглядит по отношению к исходному тексту:
Итак, нужен шаблон для поиска <a href="...something..." class="doc">, но и с ленивым и с жадным режимами есть проблема.
Правильным вариантом может стать: href="[^"]*". Он найдёт все символы внутри атрибута href до ближайшей следующей кавычки, как раз то, что нам нужно.
Работающий пример:
let str1 = '...<a href="link1" class="wrong">... <p style="" class="doc">...';
let str2 = '...<a href="link1" class="doc">... <a href="link2" class="doc">...';
let regexp = /<a href="[^"]*" class="doc">/g;
// Работает!
alert( str1.match(regexp) ); // совпадений нет, всё правильно
alert( str2.match(regexp) ); // <a href="link1" class="doc">, <a href="link2" class="doc">Найти все HTML-комментарии в тексте:
Написать регулярку, чтобы из текста ... <!-- My -- commenttest --> .. <!----> .. получить результат: <!-- My -- commenttest -->', '<!---->'
Итого нам нужно найти комментария <!--...все что угодно...-->:
Решение
Создайте регулярное выражение, чтобы найти все (открывающие и закрывающие) HTML-теги с их атрибутами.
Пример использования:
let regexp = /ваше регулярное выражение/g;
let str = '<> <a href="/"> <input type="radio" checked> <b>';
alert( str.match(regexp) ); // '<a href="/">', '<input type="radio" checked>', '<b>'В этой задаче мы предполагаем, что теги выглядят как <...что угодно...>, и внутри тегов не может быть символов < и > (первый встреченный > закрывает тег).
Решение
<[^<>]+>/
1.3.4.1 Regex Theme | Back To iOSWiki Contents | 1.3.4.3 Regex In Swift Theme Folder


