Квантификаторы в Java Regex
Как работают квантификаторы Java regex (*, +, ?, {n,m}) в жадном, ленивом и собственническом режимах.
Квантификатор указывает, сколько раз предшествующий элемент может повторяться. * означает «ноль или более», + — «один или более», ? — «ноль или один», а {n,m} задаёт точный диапазон. Это общее для всех разновидностей регулярных выражений. Что сбивает с толку в Java — это то, что каждый квантификатор существует в трёх режимах — жадном, ленивом и собственническом — и именно режим, а не счётчик, определяет, как движок регулярных выражений в java.util.regex обходит входную строку.
На этой странице рассмотрены четыре базовых квантификатора, три режима сопоставления, причины сверхсовпадений жадных шаблонов и то, как собственнические квантификаторы защищают от катастрофического возврата. Предполагается, что вы уже знаете, как скомпилировать Pattern и запустить Matcher; если нет — начните со страницы Pattern и Matcher. О метасимволах, которые будут повторяться, читайте в разделе символьные классы, а о захвате повторяющегося текста — в разделе группы.
Четыре базовых квантификатора
Прикрепите квантификатор к одному символу, символьному классу или группе, и он будет управлять повторением элемента, стоящего непосредственно перед ним:
| Квантификатор | Повторяет предшествующий элемент | Пример | Совпадения |
|---|---|---|---|
* | ноль или более раз | ab*c | ac, abc, abbbc |
+ | один или более раз | ab+c | abc, abbc (но не ac) |
? | ноль или один раз | colou?r | color, colour |
{n} | ровно n раз | \d{4} | четырёхзначный год |
{n,} | n или более раз | \d{2,} | два или более цифр |
{n,m} | от n до m раз | \d{3,5} | от 3 до 5 цифр |
Pattern.matches("ab*c", "abbbc"); // true — three b's
Pattern.matches("ab+c", "ac"); // false — '+' needs at least one b
Pattern.matches("colou?r", "color");// true — the 'u' is optional
Pattern.matches("\\d{3,5}", "1234");// true — four digits is within 3..5Жадный режим по умолчанию
Квантификатор без дополнительного суффикса является жадным: он потребляет как можно больше входных данных, затем откатывается назад — отдавая символы по одному, — пока остаток шаблона не сможет совпасть. Именно поэтому наивный шаблон <.+> применительно к HTML захватывает гораздо больше одного тега:
String html = "<b>one</b>";
Matcher m = Pattern.compile("<.+>").matcher(html);
m.find();
m.group(); // "<b>one</b>" — '.+' ate everything, then backed up to the last '>'Движок сначала захватил всю строку, не нашёл завершающего > и пошёл назад, пока не встретил > — оказавшись на самом последнем из них.
Ленивый режим: добавьте ?, чтобы захватывать как можно меньше
Добавьте ? к любому квантификатору (*?, +?, ??, {n,m}?), и он станет ленивым (также называемым reluctant): он сопоставляет минимальное число повторений и расширяется только при необходимости. Это именно то, что обычно нужно при сканировании ограниченных токенов:
String html = "<b>one</b>";
Matcher m = Pattern.compile("<.+?>").matcher(html);
m.find();
m.group(); // "<b>" — stopped at the first '>'Тот же шаблон, один лишний символ, противоположное поведение: жадный <.+> возвращает всю строку, а ленивый <.+?> — только первый тег.
Собственнический режим: добавьте + и не отдавайте ничего обратно
Добавьте + (*+, ++, ?+, {n,m}+), и квантификатор станет собственническим: он захватывает столько, сколько может, как жадный, но никогда не выполняет откат. Если остаток шаблона при этом не совпадает, всё сопоставление завершается неудачей — нет никакого «хода назад», чтобы спасти ситуацию.
// Possessive '.++' eats the final '>' too and won't return it, so no '>' is left
Pattern.compile("<.++>").matcher("<b>one</b>").find(); // falseЗачем отказываться от гибкости? Ради скорости и безопасности. Поскольку собственнический квантификатор никогда не пересматривает своё решение, он не может впасть в катастрофический откат — экспоненциальное замедление, которое зависает в потоке на шаблонах вроде (a+)+b при длинной последовательности a без b. Собственнический a++b отвечает «нет совпадения» практически мгновенно.
| Режим | Синтаксис | Стратегия | Выполняет откат? |
|---|---|---|---|
| Жадный | X* | захватить максимум, затем отступать по мере необходимости | да |
| Ленивый | X*? | захватить минимум, затем расширять по мере необходимости | да |
| Собственнический | X*+ | захватить максимум и удержать | нет |
Практический пример: все три режима рядом
Следующая программа применяет одни и те же входные данные к жадному, ленивому и собственническому квантификаторам, а затем демонстрирует диапазон {n,m} и квантификатор группы. Всё это — чистый java.util.regex из JDK.
Что можно извлечь из этого примера:
- Жадный
<.+>вывел<b>one</b><i>two</i>— всю строку. Он потребил всё, затем откатился до последнего>— именно поэтому жадные шаблоны сверхсовпадают через разделители. - Ленивый
<.+?>вывел<b>на тех же входных данных. Единственный?переключил стратегию с «максимум» на «минимум», остановившись на первом>— решение для посекундного сканирования тегов. - Собственнический
<.++>вывелmatches=false. Он поглотил завершающий>и отказался его вернуть, поэтому для>в конце шаблона ничего не осталось, и вся попытка провалилась — цена отсутствия отката. \d{3,5}отклонил12(no match, слишком мало цифр), принял123и12345целиком, а на1234567совпал только12345(длина 5) — верхняя граница5ограничила его, даже несмотря на наличие большего числа цифр.- Шаблон группы
(\w+\s*){2,3}совпал сalpha beta gamma— три слова, максимум, — доказав, что квантификатор применяется ко всей группе в скобках, аa++bмгновенно вернулfalseпри длинной последовательностиaбезb, демонстрируя, как собственнические квантификаторы обходят катастрофический откат.
Какой режим использовать?
- Используйте ленивый (
*?,+?), когда сопоставляете содержимое между разделителями — кавычками, тегами, скобками, ограниченными блоками. Он останавливается на первом закрывающем разделителе, а не на последнем, что почти всегда и нужно. - Оставайтесь в жадном режиме (по умолчанию), когда вам действительно нужно наидлиннейшее совпадение или когда существует только один возможный вариант совпадения и дополнительный
?/+был бы просто лишним. - Используйте собственнический (
*+,++) как инструмент производительности и безопасности при работе с неконтролируемыми входными данными. Поскольку он никогда не выполняет откат, он не может спровоцировать катастрофический откат, однако будет завершаться неудачей там, где жадный квантификатор смог бы спасти ситуацию — поэтому применяйте его только там, где вы уверены, что откат не нужен.
Распространённое реальное исправление: регулярное выражение, которое работает на малых входных данных, но зависает на больших, — это обычно жадный квантификатор внутри группы, например (\w+)+. Замена внутреннего квантификатора на собственнический ((\w++)+ или реструктуризация шаблона) устраняет экспоненциальное замедление.