en.javascript.info/10-regular-expressions-javascript/8-regexp-greedy-and-lazy/article.md
2015-02-27 13:21:58 +03:00

179 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Жадный и ленивый режимы
Теперь время залезть "под капот" регулярных выражений и посмотреть, как происходит поиск.
Это понимание необходимо для того, чтобы искать что-либо сложнее чем <code class="pattern">/\d+/</code>.
[cut]
Для примера рассмотрим задачу, которая часто возникает в типографике -- заменить кавычки вида `"..."` на "кавычки-лапки": `«...»`.
Шаблон, который мы будем использовать для поиска подстрок в кавычках, будет выглядеть так:
```js
//+ run
var reg = /".+"/g;
var str = 'a "witch" and her "broom" is one';
alert( str.match(reg) );
```
Запустите этот пример...
Упс! Он не работает! Вместо того, чтобы найти два совпадения <code class="match">"witch"</code> и <code class="match">"broom"</code>, он находит одно: <code class="match">"witch" and her "broom"</code>.
Это как раз тот случай, когда *жадность* -- причина всех зол.
## Алгоритм поиска
Движок регулярных выражений пытается проверить строку на соответствие шаблону, начиная с самой первой, нулевой позиции. Если не получается, он идёт вперёд и пытается найти с 1й позиции и так далее.
Чтобы сделать происходящее максимально наглядным и понятным, проследим, что именно он делает для паттерна <code class="pattern">".+"</code>.
<ol>
<li>Первый символ шаблона -- это кавычка <code class="pattern">"</code>.
Движок регулярных выражений пытается найти её на 0й позиции, но там совсем другой символ, поэтому на 0й позиции соответствия явно нет.
Далее он переходит 1ю, 2ю позицию в исходной строке и, наконец, обнаруживает кавычку на 3й позиции:
<img src="witch_greedy1.png">
</li>
<li>Кавычка найдена, далее движок проверяет, есть ли соответствие для остальной части паттерна.
В данном случае следующий символ паттерна -- `.` (точка). Она обозначает "любой символ", так что следующая буква строки <code class="match">'w'</code> вполне подходит:
<img src="witch_greedy2.png">
</li>
<li>Далее "любой символ" повторяется, так как стоит квантификатор <code class="pattern">.+</code>. Движок регулярных выражений берёт один символ за другим, до тех пор, пока у него это получается.
В данном случае это означает "до конца строки":
<img src="witch_greedy3.png">
</li>
<li>Итак, текст закончился, движок регулярных выражений больше не может найти "любой символ", он закончил строить соответствие для <code class="pattern">.+</code> и очень рад по этому поводу.
Следующий символ шаблона -- это кавычка. Её тоже необходимо найти, чтобы соответствие было полным. А тут -- беда, ведь поисковый текст завершился!
Движок регулярных выражений понимает, что, наверное, взял многовато <code class="pattern">.+</code> и начинает отступать обратно ("фаза бэктрекинга" -- backtracking на англ.).
Иными словами, он сокращает текущее совпадение на один символ:
<img src="witch_greedy4.png">
После этого он ещё раз пытается подобрать соответствие для остатка паттерна. Но кавычка <code class="pattern">'"'</code> не совпадает с <code class="subject">'e'</code>.</li>
<li>...Так что движок уменьшает число повторений <code class="pattern">.+</code> ещё раз:
<img src="witch_greedy5.png">
Кавычка <code class="pattern">'"'</code> не совпадает с <code class="subject">'n'</code>. Опять неудача.</li>
<li>Движок продолжает отступать, он уменьшает количество повторений точки <code class="pattern">'.'</code> до тех пор, пока остаток паттерна не совпадёт:
<img src="witch_greedy6.png">
</li>
<li>Мы получили результат. Так как у регэкспа есть флаг `g`, то поиск продолжится, однако это произойдёт после первого совпадения и не даст новых результатов.</li>
</ol>
**В жадном режиме (по умолчанию) регэксп повторяет квантификатор настолько много раз, насколько это возможно, чтобы найти соответствие.**
Возможно, это не совсем то, что мы хотели, но так это работает.
## Ленивый режим
Ленивый режим работы квантификаторов -- противоположность жадному, он означает "повторять минимальное количество раз".
Его можно включить, если поставить знак вопроса <code class="pattern">'?'</code> после квантификатора, так что он станет таким: <code class="pattern">*?</code> или <code class="pattern">+?</code> или даже <code class="pattern">??</code> для <code class="pattern">'?'</code>.
Чтобы не возникло путаницы -- важно понимать: обычно `?` сам является квантификатором (ноль или один). Но если он стоит *после другого квантификатора (или даже после себя)*, то обретает другой смысл -- в этом случае он меняет режим его работы на ленивый.
Регэксп <code class="pattern">/".+?"/g</code> работает, как задумано -- находит отдельно <code class="match">witch</code> и <code class="match">broom</code>:
```js
//+ run
var reg = /".+?"/g;
var str = 'a "witch" and her "broom" is one';
alert( str.match(reg) ); // witch, broom
```
Чтобы в точности понять, что происходим, разберём в деталях, как ищется <code class="pattern">".+?"</code>.
<ol>
<li>Первый шаг -- тот же, кавычка <code class="pattern">'"'</code> найдена на 3й позиции:
<img src="witch_greedy1.png">
</li>
<li>Второй шаг -- тот же, находим произвольный символ <code class="pattern">'.'</code>:
<img src="witch_greedy2.png">
</li>
<li>А вот дальше -- так как стоит ленивый режим работы `+`, то движок пытается повторять точку (произвольный символ) *минимальное количество раз*.
Так что он тут же пытается проверить, достаточно ли повторить 1 раз -- и для этого пытается найти соответствие остальной части шаблона, то есть <code class="pattern">'"'</code>:
<img src="witch_lazy3.png">
Нет, один раз повторить недостаточно. В данном случае, символ `'i' != '"'`, но если бы оставшаяся часть паттерна была бы более сложной -- алгоритм остался бы тем же. Если остаток шаблона не находится -- увеличиваем количество повторений.
</li>
<li>Движок регулярных выражений увиличивает количество повторений точки на одно и пытается найти соответствие остатку шаблона ещё раз:
<img src="witch_lazy4.png">
Опять неудача. Тогда поисковой движок увеличивает количество повторений ещё и ещё...
</li>
<li>Только на 5м шаге поисковой движок наконец находит соответствие для остатка паттерна:
<img src="witch_lazy5.png">
</li>
<li>Так как поиск происходит с флагом `g`, то он продолжается с конца текущего совпадения, давая ещё один результат:
<img src="witch_lazy6.png">
</li>
</ol>
В примере выше продемонстрирована работа ленивого режима для <code class="pattern">+?</code>. Квантификаторы <code class="pattern">+?</code> и <code class="pattern">??</code> ведут себя аналогично -- "ленивый" движок увеличивает количество повторений только в том случае, если для остальной части шаблона на данной позиции нет соответствия, в то время как жадный сначала берёт столько повторений, сколько возможно, а потом отступает назад.
**Ленивость распространяется только на тот квантификатор, после которого стоит `?`.**
Прочие квантификаторы остаются жадными.
Например:
```js
//+ run
alert( "123 456".match ( /\d+ \d+?/g) ); // 123 4
```
<ol>
<li>Подпаттерн <code class="pattern">\d+</code> пытается найти столько символов, сколько возможно (работает жадно), так что он находит <code class="match">123</code> и останавливается, поскольку символ пробела <code class="pattern">' '</code> не подходит под <code class="pattern">\d</code>.</li>
<li>Далее идёт пробел, и в игру вступает <code class="pattern">\d+?</code>.
Он находит один символ <code class="match">'4'</code> и пытатся проверить, есть ли совпадение с остатком шаблона (после <code class="pattern">\d+?</code>).
Здесь мы ещё раз заметим -- ленивый режим без необходимости ничего не возьмёт.
Так как шаблон закончился, то поиск завершается и <code class="match">123 4</code> становится результатом.</li>
<li>Следующий поиск продолжится с `5`, но ничего не найдёт.</li>
</ol>
[smart header="Конечные автоматы и не только"]
Современные движки регулярных выражений могут иметь более хитрую реализацию внутренних алгоритмов, чтобы искать быстрее.
Однако, чтобы понять, как работает регулярное выражение, и строить регулярные выражения самому, знание этих хитрых алгоритмов ни к чему. Они служат лишь внутренней оптимизации способа поиска, описанного выше.
Кроме того, сложные регулярные выражения плохо поддаются всяким оптимизациям, так что поиск вполне может работать и в точности как здесь описано.
[/smart]
## Альтернативный подход
В данном конкретном случае, возможно искать строки в кавычках, оставаясь в жадном режиме, с использованием регулярного выражения <code class="pattern">"[^"]+"</code>:
```js
//+ run
var reg = /"[^"]+"/g;
var str = 'a "witch" and her "broom" is one';
alert( str.match(reg) ); // witch, broom
```
Регэксп <code class="pattern">"[^"]+"</code> даст правильные результаты, поскольку ищет кавычку <code class="pattern">'"'</code>, за которой идут столько не-кавычек (исключающие квадратные скобки), сколько возможно. Так что вторая кавычка автоматически прекращает повторения <code class="pattern">[^"]+</code> и позволяет найти остаток шаблона <code class="pattern">"</code>.