14 KiB
Жадные и ленивые квантификаторы [todo]
Квантификаторы -- с виду очень простая, но на самом деле очень хитрая штука.
Необходимо очень хорошо понимать, как именно происходит поиск, если конечно мы хотим искать что-либо сложнее чем /\d+/
.
[cut]
Для примера рассмотрим задачу, которая часто возникает в типографике -- заменить в тексте кавычки вида "..."
(их называют "английские кавычки") на "кавычки-ёлочки": «...»
.
Для этого нужно сначала найти все слова в таких кавычках.
Соотверствующее регулярное выражение может выглядеть так: /".+"/g
, то есть мы ищем кавычку, после которой один или более произвольный символ, и в конце опять кавычка.
Однако, если попробовать применить его на практике, даже на таком простом случае...
//+ run
var reg = /".+"/g;
var str = 'a "witch" and her "broom" is one';
alert( str.match(reg) ); // "witch" and her "broom"
...Мы увидим, что оно работает совсем не так, как задумано!
Вместо того, чтобы найти два совпадения "witch"
и "broom"
, оно находит одно: "witch" and her "broom"
.
Это как раз тот случай, когда жадность -- причина всех зол.
Жадный поиск
Чтобы найти совпадение, движок регулярных выражений обычно использует следующий алгоритм:
- Для каждой позиции в поисковой строке
- Проверить совпадение на данной позиции
- Посимвольно, с учётом классов и квантификаторов сопоставив с ней регулярное выражение.
- Проверить совпадение на данной позиции
Это общие слова, гораздо понятнее будет, если мы проследим, что именно он делает для регэкспа ".+"
.
- Первый символ шаблона -- это кавычка
"
.Движок регулярных выражений пытается сопоставить её на 0й позиции в строке, но символ
a
, поэтому на 0й позиции соответствия явно нет.Далее он переходит 1ю, 2ю позицию в исходной строке и, наконец, обнаруживает кавычку на 3й позиции:
- Кавычка найдена, далее движок проверяет, есть ли соответствие для остальной части паттерна.
В данном случае следующий символ паттерна --
.
(точка). Она обозначает "любой символ", так что следующая буква строки'w'
вполне подходит: - Далее "любой символ" повторяется, так как стоит квантификатор
.+
. Движок регулярных выражений берёт один символ за другим, до тех пор, пока у него это получается.В данном случае это означает "до конца строки":
- Итак, текст закончился, движок регулярных выражений больше не может найти "любой символ", он закончил строить соответствие для
.+
и очень рад по этому поводу.Следующий символ шаблона -- это кавычка. Её тоже необходимо найти, чтобы соответствие было полным. А тут -- беда, ведь поисковый текст завершился!
Движок регулярных выражений понимает, что, наверное, взял многовато
.+
и начинает отступать обратно ("фаза бэктрекинга" -- backtracking на англ.).Иными словами, он сокращает текущее совпадение на один символ:
После этого он ещё раз пытается подобрать соответствие для остатка паттерна. Но кавычка
'"'
не совпадает с'e'
. - ...Так что движок уменьшает число повторений
.+
ещё раз:Кавычка
'"'
не совпадает с'n'
. Опять неудача. - Движок продолжает отступать, он уменьшает количество повторений точки
'.'
до тех пор, пока остаток паттерна не совпадёт: - Мы получили результат. Так как у регэкспа есть флаг `g`, то поиск продолжится, однако это произойдёт после первого совпадения и не даст новых результатов.
В жадном режиме (по умолчанию) регэксп повторяет квантификатор настолько много раз, насколько это возможно, чтобы найти соответствие.
Возможно, это не совсем то, что мы хотели, но так это работает.
Ленивый режим
Ленивый режим работы квантификаторов -- противоположность жадному, он означает "повторять минимальное количество раз".
Его можно включить, если поставить знак вопроса '?'
после квантификатора, так что он станет таким: *?
или +?
или даже ??
для '?'
.
Чтобы не возникло путаницы -- важно понимать: обычно ?
сам является квантификатором (ноль или один). Но если он стоит после другого квантификатора (или даже после себя), то обретает другой смысл -- в этом случае он меняет режим его работы на ленивый.
Регэксп /".+?"/g
работает, как задумано -- находит отдельно witch
и broom
:
//+ run
var reg = /".+?"/g;
var str = 'a "witch" and her "broom" is one';
alert( str.match(reg) ); // witch, broom
Чтобы в точности понять, что происходим, разберём в деталях, как ищется ".+?"
.
- Первый шаг -- тот же, кавычка
'"'
найдена на 3й позиции: - Второй шаг -- тот же, находим произвольный символ
'.'
: - А вот дальше -- так как стоит ленивый режим работы `+`, то движок пытается повторять точку (произвольный символ) *минимальное количество раз*.
Так что он тут же пытается проверить, достаточно ли повторить 1 раз -- и для этого пытается найти соответствие остальной части шаблона, то есть
'"'
:Нет, один раз повторить недостаточно. В данном случае, символ
'i' != '"'
, но если бы оставшаяся часть паттерна была бы более сложной -- алгоритм остался бы тем же. Если остаток шаблона не находится -- увеличиваем количество повторений. - Движок регулярных выражений увиличивает количество повторений точки на одно и пытается найти соответствие остатку шаблона ещё раз:
Опять неудача. Тогда поисковой движок увеличивает количество повторений ещё и ещё...
- Только на 5м шаге поисковой движок наконец находит соответствие для остатка паттерна:
- Так как поиск происходит с флагом `g`, то он продолжается с конца текущего совпадения, давая ещё один результат:
В примере выше продемонстрирована работа ленивого режима для +?
. Квантификаторы +?
и ??
ведут себя аналогично -- "ленивый" движок увеличивает количество повторений только в том случае, если для остальной части шаблона на данной позиции нет соответствия, в то время как жадный сначала берёт столько повторений, сколько возможно, а потом отступает назад.
Ленивость распространяется только на тот квантификатор, после которого стоит ?
.
Прочие квантификаторы остаются жадными.
Например:
//+ run
alert( "123 456".match ( /\d+ \d+?/g) ); // 123 4
- Подпаттерн
\d+
пытается найти столько символов, сколько возможно (работает жадно), так что он находит123
и останавливается, поскольку символ пробела' '
не подходит под\d
. - Далее идёт пробел, и в игру вступает
\d+?
.Он находит один символ
'4'
и пытатся проверить, есть ли совпадение с остатком шаблона (после\d+?
).Здесь мы ещё раз заметим -- ленивый режим без необходимости ничего не возьмёт.
Так как шаблон закончился, то поиск завершается и
123 4
становится результатом. - Следующий поиск продолжится с `5`, но ничего не найдёт.
[smart header="Конечные автоматы и не только"] Современные движки регулярных выражений могут иметь более хитрую реализацию внутренних алгоритмов, чтобы искать быстрее.
Однако, чтобы понять, как работает регулярное выражение, и строить регулярные выражения самому, знание этих хитрых алгоритмов ни к чему. Они служат лишь внутренней оптимизации способа поиска, описанного выше.
Кроме того, сложные регулярные выражения плохо поддаются всяким оптимизациям, так что поиск вполне может работать и в точности как здесь описано. [/smart]
Альтернативный подход
В данном конкретном случае, возможно искать строки в кавычках, оставаясь в жадном режиме, с использованием регулярного выражения "[^"]+"
:
//+ run
var reg = /"[^"]+"/g;
var str = 'a "witch" and her "broom" is one';
alert( str.match(reg) ); // witch, broom
Регэксп "[^"]+"
даст правильные результаты, поскольку ищет кавычку '"'
, за которой идут столько не-кавычек (исключающие квадратные скобки), сколько возможно. Так что вторая кавычка автоматически прекращает повторения [^"]+
и позволяет найти остаток шаблона "
.