Перейти к основному содержимому

Регулярные выражения — проверки вокруг совпадения

Разработчику Аналитику

Предыдущий: группы и замена.
Следующий: флаги и жадность.
Lookahead для пароля — готовый шаблон в Regex — готовые паттерны.


Идея простыми словами

Иногда нужно найти текст только при условии, что рядом что-то есть (или наоборот — чего-то нет). При этом соседний кусок не всегда нужно включать в результат и не всегда нужно запоминать в группе.

Примеры из практики:

  • цена после знака $, сам $ в ответ не нужен;
  • пароль — минимум 8 символов, и есть цифра, и есть заглавная буква;
  • слово cat, за которым не идёт s (чтобы отсечь cats);
  • номер заказа, перед которым стоит префикс ORD-, но в отчёт нужен только номер.

Для этого служат проверки вокруг совпадения (lookaround): движок "подглядывает" вперёд или назад, не сдвигая основной указатель и не добавляя проверенный фрагмент в итоговое совпадение (в отличие от обычных захватывающих ()).


Четыре вида

ЗаписьНазваниеСмысл
(?=...)опережающая, положительнаядальше по тексту должно совпасть ...
(?!...)опережающая, отрицательнаядальше не должно совпасть ...
(?<=...)ретроспективная, положительнаяперед текущей позицией было ...
(?<!...)ретроспективная, отрицательнаяперед текущей позицией не было ...

Скобки (?:...) из прошлой статьи — обычная группа без захвата: она входит в совпадение и участвует в жадном сопоставлении. Lookaround только проверяет контекст и не расширяет текст совпадения.

МеханизмПопадает в результат?Захват в группу?
(...)дада
(?:...)данет
(?=...), (?!...), (?<=...), (?<!...)нетнет

Опережающая положительная (?=...)

Смысл: "с текущей позиции дальше по строке должно начаться совпадение с ...", при этом символы из ... не входят в итоговый match.


Пароль с несколькими условиями

Требования (упрощённо):

  • длина от 8;
  • хотя бы одна заглавная латинская;
  • хотя бы одна цифра;
  • хотя бы один спецсимвол @$!%*?&;
  • только разрешённые символы.
^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$

Читаем по блокам:

БлокЧто проверяет
^начало строки
(?=.*[A-Z])где-то дальше есть заглавная
(?=.*\d)где-то дальше есть цифра
(?=.*[@$!%*?&])где-то дальше есть спецсимвол
[A-Za-z\d@$!%*?&]{8,}основная часть — 8 и более разрешённых символов
$конец строки

Каждый (?=...) не потребляет символы — только проверяет. Поэтому три условия можно поставить подряд в начале шаблона.

ПарольРезультат
Abcdef1!подходит
abcdef1!нет заглавной
Abcdefghнет цифры и спецсимвола
Ab1!слишком короткий
Пароль в продакшене

Для реальной формы часто проще и понятнее проверить длину, цифру и спецсимвол тремя отдельными условиями в коде, а regex оставить для "только разрешённые символы". Так проще показывать пользователю, чего именно не хватает.


"Должно быть дальше" без захвата префикса

Текст:

id=42; name=admin; role=user

Нужно найти значение после id=, без самого id= в совпадении:

(?<=id=)\d+

Если lookbehind с переменной шириной недоступен, используют опережающую проверку и захват:

id=(\d+)

Тогда в группе 1 будет 42, а полное совпадение — id=42.


Опережающая отрицательная (?!...)

Смысл: "с текущей позиции дальше не должно начинаться совпадение с ...".


Слово без продолжения

Пример: найти The, после которого не идёт пробел и fat:

\b[Tt]he\b(?!\sfat)

В строке The fat cat первое The отсечётся (после него как раз fat). Другие вхождения the без fat останутся.


Запретить расширение файла

Найти имена файлов без .tmp:

\b[\w.-]+(?!\.\w+\.tmp)\.\w+\b

Учебный приём: отрицательная опережающая проверка (?!\.\w+\.tmp) перед финальным расширением отсекает двойные "временные" суффиксы. На реальных путях надёжнее разбирать путь в коде.


Начало строки, которая не комментарий

^(?!\s*#).+

С флагом m — "непустая строка, которая не начинается с # после пробелов" (удобно для фильтрации строк конфига).


Ретроспективная положительная (?<=...)

Смысл: "непосредственно слева от текущей позиции должно стоять ...", при этом ... не входит в совпадение.


Цена после доллара

Текст:

Цена $42.50 и скидка $9

Нужны числа после $, без самого $ в результате:

(?<=\$)\d+(?:\.\d{2})?
ЧастьЗначение
(?<=\$)прямо слева должен быть $
\d+одна и более цифр
(?:\.\d{2})?необязательные копейки .50

Совпадения: 42.50 и 9.

Запасной вариант (если lookbehind недоступен): \$(\d+(?:\.\d&#123;2&#125;)?) — тогда $ попадёт в полное совпадение, а цифры — в группу 1.


Слова после артикля

Текст:

The fat cat sat on the mat.

Найти fat или mat, если перед ними было The или the :

(?<=[Tt]he\s)(?:fat|mat)
ЧастьЗначение
(?<=[Tt]he\s)слева The или the
`(?:fatmat)`

Ретроспективная отрицательная (?<!...)

Смысл: "слева не должно стоять ...".

Найти cat, перед которым нет The / the :

(?<![Tt]he\s)cat

В The cat sat on cat подойдёт второе cat, первое — нет.


Рубль, а не доллар

(?<!\$)\d+(?:\.\d{2})?\s*

Число с , перед которым нет $ (грубая эвристика для смешанных прайс-листов).


Lookaround и обычный захват — когда что выбирать

СитуацияПриём
Несколько независимых условий к одной строкенесколько (?=...) в начале
"Только если после идёт X"(?=X) или \X(?=Y) с захватом только Y
"Только если перед был X"(?<=X)
Нужен X в результате заменызахват (...) или lookbehind + основная часть
Не засорять список групп(?:...) + lookaround

Ограничения движков

  • В старых движках (?<=...) и (?<!...) требуют фиксированной ширины слева: (?<=ab) — да, (?<=a+) — нет. В современном JavaScript, .NET и PCRE переменная ширина lookbehind поддерживается.
  • Сложные цепочки (?=...)(?=...)(?=...) трудно читать и сопровождать — для команды важнее ясный код, чем "всё в одном regex".
  • Lookaround не делает шаблон "умнее" в смысле вложенных структур: JSON, HTML и сбалансированные скобки по-прежнему требуют парсера.

Мини-практика

ЗадачаПодсказка
UUID после id=(?<=id=)[0-9a-f-]{36}
Слово не в начале строки(?<!^)\bword\b с флагом m
Пароль без пробелов^(?=\S{8,}$)\S+$ плюс отдельные проверки на классы символов

Проверьте шаблоны в лаборатории на 5–10 своих строках.


Дальше: Флаги и жадность →