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