Продвинутые регулярные выражения: lookahead, lookbehind и группы
Продвинутое3 минРедакция Innovialabs
Коротко
Lookaround проверяет контекст, не захватывая его, именованные группы делают совпадения читаемыми, а вложенные квантификаторы вроде (a+)+ — главный источник катастрофического backtracking и ReDoS.
Что дают продвинутые конструкции
Базовые регулярки отвечают на вопрос «есть ли такой текст». Продвинутые конструкции позволяют сказать точнее:
- lookaround — «совпадение должно стоять перед или после чего-то», но это «что-то» не входит в результат;
- именованные группы — достаём части совпадения по имени, а не по номеру;
- незахватывающие группы — группируем без лишних захватов;
- ленивые квантификаторы — берём минимум вместо максимума;
- обратные ссылки — требуем повторения уже найденного текста.
Lookahead и lookbehind
Это проверки нулевой ширины: они смотрят на соседний текст, но не «съедают» его.
| Конструкция | Значение |
|---|---|
(?=X) | дальше идёт X |
(?!X) | дальше не идёт X |
(?<=X) | перед этим стоит X |
(?<!X) | перед этим не стоит X |
Классический пример — разделение разрядов числа пробелами:
"1234567".replace(/\B(?=(\d{3})+(?!\d))/g, " "); // "1 234 567"
Позиция подходит, если после неё до конца числа идут группы ровно по три цифры. Сами цифры не захватываются, поэтому замена вставляет пробел, ничего не удаляя.
Несколько lookahead подряд удобно использовать для проверки условий в любом порядке: ^(?=.*\d)(?=.*[a-z]).{8,}$ — минимум 8 символов, есть цифра и строчная буква.
Lookbehind извлекает значение после маркера: (?<=\$)\d+ найдёт 25 в $25. Важное отличие: в Python модуль re требует lookbehind фиксированной длины, а в современных движках JavaScript длина может быть переменной.
Именованные и незахватывающие группы
Синтаксис различается между языками:
const m = "2026-10-19".match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/);
m.groups.y; // "2026"
"2026-10-19".replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, "$<d>.$<m>.$<y>");
import re
re.sub(r"(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})", r"\g<d>.\g<m>.\g<y>", "2026-10-19")
Если группа нужна только для | или квантификатора, делайте её незахватывающей: (?:https?|ftp)://. Номера остальных групп не съедут, а движку меньше работы.
Жадные и ленивые квантификаторы
По умолчанию *, +, {n,} жадные — берут как можно больше. На строке <b>x</b> выражение <.+> захватит всё целиком, а ленивое <.+?> — только <b>.
Часто лучше вместо ленивости описать допустимые символы точно: <[^>]+>. Это и понятнее, и быстрее, потому что движку не нужно откатываться.
Обратные ссылки
Обратная ссылка требует, чтобы текст повторился: \b(\w+)\s+\1\b находит повтор слова вроде «the the». Для именованных групп: \k<word> в JavaScript и (?P=word) в Python.
Катастрофический backtracking и ReDoS
Движки JavaScript, Python, Java и многих других языков работают через backtracking: при неудаче перебирают другие варианты разбиения. Выражение ^(a+)+$ на строке из многих a с символом ! в конце перебирает экспоненциальное число вариантов и «вешает» процесс. Если такое выражение проверяет пользовательский ввод, это ReDoS — отказ в обслуживании одной строкой.
Как защититься:
- избегайте вложенных квантификаторов над одним и тем же текстом:
(a+)+,(\w*)*,(.*,)*; - не делайте альтернативы, которые пересекаются:
(a|a)*,(\d|\d\d)+; - ограничивайте длину входа до запуска регулярки;
- там, где доступны, используйте атомарные группы
(?>...)и притяжательные квантификаторыa++— они запрещают откат (в новых версиях Python они появились вre); - для недоверенных шаблонов или ввода берите движки без backtracking, например RE2.
Типичные ошибки
- Забытый флаг
gв JavaScript при замене всех вхождений. - Отсутствие якорей
^и$в валидации — проверка проходит по подстроке. - Обычная строка вместо raw-строки
r"..."в Python, из-за чего\bпревращается в backspace. - Одна огромная регулярка там, где проще парсер или две простые проверки.