W3docs

Жадные и ленивые квантификаторы

Жадные и ленивые квантификаторы в регулярных выражениях JavaScript: жадный берёт максимум и отступает, ленивый (*?, +?, ??, {n,m}?) берёт минимум.

Квантификатор вроде *, + или ? указывает регулярному выражению, сколько раз может повторяться предшествующий шаблон. Но когда сразу несколько вариантов длины текста удовлетворяют шаблону, движок должен решить, какой из них взять. Именно это и контролируют жадные и ленивые квантификаторы.

По умолчанию каждый квантификатор жадный: он захватывает как можно больше символов. Добавьте ? после квантификатора — и он становится ленивым: захватывает как можно меньше. Выбор неправильного режима — одна из самых распространённых ошибок при работе с регулярными выражениями; классический симптом — шаблон, который «совпадает слишком много». Эта страница объясняет механизм обоих режимов, чтобы вы могли осознанно выбирать нужный.

Как на самом деле работает жадное совпадение: захват, затем откат

Жадный квантификатор не знает магическим образом, где остановиться. Он работает в два этапа:

  1. Захватить максимум. .* сначала поглощает весь остаток строки.
  2. Откат. Если шаблон, следующий за .*, больше не может совпасть (потому что всё уже поглощено), движок возвращает символы по одному, каждый раз повторяя попытку, пока весь шаблон не совпадёт.

Таким образом, жадный режим означает «взять всё, а затем неохотно отдавать минимально необходимое для того, чтобы остальная часть шаблона совпала». Понимание шага отката — ключ к предсказанию поведения жадных шаблонов.

Жадный * в действии


javascript— editable

Здесь .* совпадает с любым символом ноль или более раз. Поскольку после него ничего не требуется, откат не нужен, и он захватывает всё до конца строки, возвращая "ABCD*E".

Жадный + в действии


javascript— editable

C+ совпадает с "C" один или более раз и жадно берёт все три, поэтому совпадение — "ABCCC".

Как работает ленивое совпадение: захват минимума, затем расширение

Ленивый квантификатор инвертирует стратегию:

  1. Захватить минимум. .*? начинает с совпадения с ничем.
  2. Расширение. Только если остальная часть шаблона не может совпасть, движок позволяет ленивому квантификатору взять ещё один символ, затем повторяет попытку — и так до тех пор, пока весь шаблон не совпадёт.

Таким образом, ленивый режим означает «брать как можно меньше и расширяться только когда вынуждены». Квантификатор становится ленивым при добавлении ?.

Ленивый *? в действии


javascript— editable

Это сбивающий с толку случай. Результат — просто "AB". Почему? Потому что .*? разрешено совпасть с ничем, и после него в шаблоне нет ничего, что вынуждает захватывать больше. Как только AB совпало, шаблон уже выполнен, поэтому ленивый квантификатор с удовольствием останавливается на нуле символов. Ленивый квантификатор расширяется только тогда, когда что-то после него — разделитель, литерал или якорь — требует этого.

Ленивый +? в действии


javascript— editable

C+? должен совпасть хотя бы с одним "C" (это требование +), и ничто после него не требует большего, поэтому он останавливается на первом: "ABC".

Канонический пример: <.*> против <.*?>

Разницу между жадным и ленивым режимами легче всего увидеть, когда есть что-то после квантификатора. Совпадение с HTML-тегами — хрестоматийный пример. Запустите оба шаблона против одной строки:


javascript— editable
  • Жадный /<.*>/ совпадает с "<p>Hello</p>" — всей строкой. .* поглощает всё, затем откатывается ровно настолько, чтобы оставить > для финального > в шаблоне, останавливаясь на последнем >.
  • Ленивый /<.*?>/ совпадает только с "<p>" — одним тегом. .*? расширяется посимвольно и останавливается, как только достигает первого >.

Когда цель — «совпасть с одним тегом», «совпасть с одной строкой в кавычках» или «совпасть до следующего разделителя», ленивый режим почти всегда то, что нужно.

Полное семейство ленивых квантификаторов

У каждого жадного квантификатора есть ленивый двойник, образуемый добавлением ?:

ЖадныйЛенивыйСмысл ленивой формы
**?Ноль или более, как можно меньше
++?Один или более, как можно меньше
???Ноль или один, предпочитать ноль
{2,5}{2,5}?От 2 до 5, предпочитать 2
{2,}{2,}?Не менее 2, предпочитать 2

Обратите внимание, что ?? — не опечатка: первый ? — квантификатор (ноль или один), а второй делает его ленивым, поэтому при наличии выбора он предпочитает не совпадать ни с чем.


javascript— editable

Когда следует использовать ленивые квантификаторы?

Практическое правило:

Используйте ленивый квантификатор, когда хотите совпасть до первого вхождения разделителя, и жадный — когда нужно всё до последнего.

Типичные ситуации, где ленивый режим является правильным выбором:

  • Извлечение одного HTML/XML-тега: /<.*?>/.
  • Захват содержимого одной пары кавычек или скобок: /".*?"/, /\(.*?\)/.
  • Получение текста до следующего разделителя без захвата следующей записи.

Два важных предостережения:

  • Разделитель часто понятнее, чем ленивость. Вместо /".*?"/ можно написать /"[^"]*"/ с отрицательным классом символов. Это полностью избегает отката и обычно работает быстрее и предсказуемее.
  • Ленивому нужна точка остановки. Как показал пример с AB.*?, ленивый квантификатор в конце шаблона (когда ничто не толкает его вперёд) сжимается до минимума и почти ничего не совпадает. Сочетайте его со следующим литералом, якорем или захватывающей группой.

Итоги

  • Квантификаторы по умолчанию жадные — они берут максимум, затем откатываются, чтобы остальная часть шаблона совпала.
  • Добавьте ?, чтобы сделать квантификатор ленивым — он берёт минимум, затем расширяется только по необходимости.
  • Ленивый квантификатор без ничего после него совпадает как можно меньше (часто ничего), поэтому всегда давайте ему разделитель или якорь для остановки.
  • Полное семейство ленивых: *?, +?, ?? и {n,m}?.
  • Используйте ленивый режим, когда нужно «до первого» разделителя; в других случаях отрицательный класс символов зачастую является более чистым и быстрым выбором.

Практика

Практика
Дана строка '<p>Hi</p>'. Что совпадёт при использовании ленивого шаблона /<.*?>/ ?
Дана строка '<p>Hi</p>'. Что совпадёт при использовании ленивого шаблона /<.*?>/ ?
Was this page helpful?