Регулярные выражения: что это, синтаксис и примеры на Python

Регулярные выражения: что это, синтаксис и примеры на Python Полезное

Регулярное выражение (regex, regexp) — это шаблон, который описывает множество строк: движок сверяет шаблон с текстом и находит подходящие участки. С его помощью ищут, проверяют, извлекают и заменяют фрагменты текста: даты в логе, номера в заявке, лишние пробелы в выгрузке.

Ниже — синтаксис (классы, квантификаторы, жадность, якоря, группы, lookahead), примеры на модуле re в Python 3.14, короткое сравнение диалектов и катастрофический бэктрекинг как проблема безопасности. Весь код прогнан в Python 3.14.

Четыре термина, которые путают

  • Шаблон (pattern) — сама запись, например \d{4}-\d{2}.
  • Совпадение (match) — конкретный участок текста, который подошел под шаблон: 2026-09.
  • Группа — часть шаблона в круглых скобках; движок отдельно запоминает, какой кусок совпадения она захватила.
  • Диалект (flavor) — конкретная реализация: Python re, JavaScript, PCRE, POSIX в grep/sed. Основа общая, но детали различаются.

Первый пример целиком: ошибки из лога

import re

log = """2026-09-24 10:15:02 ERROR db timeout after 30s
2026-09-24 10:15:07 INFO request /api/users 200
2026-09-24 10:16:41 ERROR disk /dev/sda1 95%"""

pattern = re.compile(
    r"^(?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}) ERROR (?P<msg>.+)$",
    re.MULTILINE,
)

for m in pattern.finditer(log):
    print(m["time"], "|", m["msg"])

Скрипт печатает две строки — только записи с уровнем ERROR:

10:15:02 | db timeout after 30s
10:16:41 | disk /dev/sda1 95%

Разбор: ^ и $ — начало и конец строки (с флагом re.MULTILINE — каждой строки лога, а не всего текста). \d{4} — ровно четыре цифры. (?P<time>...) — именованная группа, к ней обращаемся m["time"]. .+ — один или больше любых символов до конца строки. Префикс r у строки — raw-строка, о ней ниже.

Синтаксис: из чего собирается шаблон

Символы и классы

Запись Что означает Пример
abc буквально эти символы подряд cat находит cat в concat
. любой символ, кроме перевода строки (с re.DOTALL — любой) c.t — cat, cot
[abc], [a-z] один символ из набора или диапазона [0-9] — одна цифра
[^abc] один символ не из набора [^,]+ — все до запятой
\d, \w, \s цифра, «словесный» символ, пробельный символ \w+ — слово
\D, \W, \S отрицание соответствующего класса \S+ — кусок без пробелов
\., \(, \$ экранирование: спецсимвол как обычный 3\.14

Внутри квадратных скобок большинство спецсимволов теряют особый смысл: [.] — это просто точка.

Квантификаторы

Запись Сколько раз
* 0 и больше
+ 1 и больше
? 0 или 1
{3} ровно 3
{2,5} от 2 до 5
{2,} 2 и больше

Квантификатор относится к одному предыдущему элементу: в ab+ повторяется только b. Чтобы повторить последовательность, ее берут в группу: (ab)+.

Якоря

Якорь проверяет позицию, а не символ: ^ — начало, $ — конец, \b — граница слова. Шаблон \bcat\b найдет слово cat, но не часть слова concat. В Python есть еще \A и \Z — абсолютные начало и конец строки, на них не влияет re.MULTILINE.

Группы и альтернация

(...) — захватывающая группа, (?:...) — группа без захвата (только для объединения), (?P<name>...) — именованная группа в Python, a|b — «или». Альтернация действует на всю группу: (?:jpg|png)$ — расширение jpg или png в конце.

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

По умолчанию *, +, ?, {n,m} жадные: захватывают как можно больше, а потом отступают, если остаток шаблона не совпал. Знак ? после квантификатора делает его ленивым: он берет как можно меньше.

import re

html = "<b>один</b> и <b>два</b>"

print(re.findall(r"<b>.*</b>", html))    # жадный
print(re.findall(r"<b>.*?</b>", html))   # ленивый
print(re.findall(r"<b>(.*?)</b>", html)) # только группа
['<b>один</b> и <b>два</b>']
['<b>один</b>', '<b>два</b>']
['один', 'два']

Жадный .* проглотил все до последнего </b>. Ленивый остановился на ближайшем. Третья строка показывает правило findall: если в шаблоне есть группа, возвращается ее содержимое, а не все совпадение. Граница примера: для простых фрагментов так можно, но разбирать HTML целиком регулярками ненадежно (вложенность, атрибуты, комментарии) — для этого есть парсеры.

Модуль re в Python: какую функцию брать

import re

s = "order 42, price 1500"

print(re.search(r"\d+", s))        # первое совпадение где угодно
print(re.match(r"\d+", s))         # только с начала строки
print(re.match(r"order", s))
print(re.fullmatch(r"\d+", "1500"))   # вся строка целиком
print(re.findall(r"\d+", s))       # все совпадения списком
print(re.sub(r"\d+", "N", s))      # замена
print(re.split(r",\s*", s))        # разбиение
<re.Match object; span=(6, 8), match='42'>
None
<re.Match object; span=(0, 5), match='order'>
<re.Match object; span=(0, 4), match='1500'>
['42', '1500']
order N, price N
['order 42', 'price 1500']
Задача Функция Что вернет
есть ли совпадение где-то в тексте re.search первый Match или None
текст начинается с шаблона re.match Match или None
вся строка соответствует формату (валидация) re.fullmatch Match или None
все совпадения re.findall / re.finditer список строк (при нескольких группах — кортежей) / итератор Match
замена re.sub новая строка
разбить по разделителю-шаблону re.split список

Частая ошибка новичка — re.match там, где нужен re.search: match смотрит только начало строки и на "order 42" для \d+ вернул None. Если шаблон используется многократно, его компилируют один раз через re.compile.

Зачем raw-строки r»…»

В обычной строке Python обратный слеш — начало escape-последовательности. "\b" — это символ backspace, а не граница слова:

import re

print(re.findall("\bcat\b", "cat and concat"))   # без r
print(re.findall(r"\bcat\b", "cat and concat"))  # с r
print(repr("\b"), repr(r"\b"))
[]
['cat']
'\x08' '\\b'

Без r шаблон искал символы backspace и ничего не нашел, причем без всякой ошибки. С "\d" Python 3.14 хотя бы предупреждает: SyntaxWarning: "\d" is an invalid escape sequence. Правило простое: шаблоны пишите raw-строками.

Флаги

re.IGNORECASE (re.I) — без учета регистра; re.MULTILINE (re.M) — ^/$ на каждой строке; re.DOTALL (re.S) — точка ловит и \n; re.VERBOSE (re.X) — можно писать шаблон с пробелами и комментариями; re.ASCII (re.A) — \d, \w, \s только ASCII. Флаги объединяют через |: re.I | re.M.

Группы в замене и lookahead

Захваченные группы можно подставить в замену: \1, \2 или \g<name>.

import re

print(re.sub(r"(\d{4})-(\d\d)-(\d\d)", r"\3.\2.\1", "2026-09-24"))

Результат: 24.09.2026.

Lookahead (?=...) и lookbehind (?<=...) проверяют, что справа или слева стоит нужный текст, но не включают его в совпадение. Негативные варианты — (?!...) и (?<!...).

import re

# пробел между разрядами: позиция, после которой до конца числа идут группы по 3 цифры
print(re.sub(r"(?<=\d)(?=(?:\d{3})+(?!\d))", " ", "Итого 1234567 руб., 950 шт."))

# пароль: минимум 8 символов, есть цифра и заглавная латинская буква
strong = re.compile(r"(?=.*\d)(?=.*[A-Z]).{8,}")
for p in ["qwerty12", "Qwerty12", "Qw1"]:
    print(p, bool(strong.fullmatch(p)))
Итого 1 234 567 руб., 950 шт.
qwerty12 False
Qwerty12 True
Qw1 False

В первом шаблоне совпадение имеет нулевую длину: это позиция между цифрами, и sub вставляет туда пробел. Во втором два lookahead-а проверяют условия, не сдвигая позицию. Пример с паролем учебный: реальная политика паролей строится на длине и проверке по утекшим базам, а не на обязательной заглавной букве.

Ограничение Python: lookbehind должен иметь фиксированную длину. re.search(r"(?<=\d+)x", "12x") падает с re.error: look-behind requires fixed-width pattern, а в JavaScript такой шаблон работает.

Ловушки валидации ввода

import re

print(bool(re.match(r"^\d+$", "123\n")))     # $ пропускает перевод строки в конце
print(bool(re.fullmatch(r"\d+", "123\n")))
print(bool(re.fullmatch(r"\d+", "١٢٣")))      # арабско-индийские цифры
print(bool(re.fullmatch(r"\d+", "١٢٣", re.ASCII)))
print(bool(re.fullmatch(r"[0-9]+", "١٢٣")))
True
False
True
False
False

Первая ловушка: в Python $ совпадает и перед завершающим \n, поэтому ^\d+$ пропускает "123\n". Для проверки формата берите re.fullmatch или \Z. Осторожно при переносе шаблона: в PCRE (PHP), Java и Ruby \Z тоже пропускает завершающий \n, строгий конец строки там — \z (в Python \z появился как синоним \Z в 3.14). Вторая: в строковых шаблонах \d — любая цифра Unicode, не только 0-9. Если дальше код ждет ASCII (ID в базе, порт), пишите [0-9] или ставьте re.ASCII.

Третья — кириллица. Диапазон [а-я] не включает букву «е с двумя точками»: она стоит в Unicode отдельно (U+0451). Шаблон [а-я]+ на слове «елка» с этой буквой вернет только лка. Добавляйте ее в класс явно или используйте [^\W\d_]+ — «буквы любого алфавита».

Диалекты коротко

Возможность Python re JavaScript PCRE (PHP, grep -P) POSIX ERE (grep -E)
именованная группа (?P<name>...) (?<name>...) оба варианта нет
lookahead / lookbehind да / только фиксированной длины да / переменной длины да / с ограничениями длины нет
ленивые квантификаторы да да да нет
флаги аргумент функции или (?i) после литерала: /.../gi модификаторы после разделителя опции команды (-i)

В базовом POSIX-синтаксисе (grep без -E, sed без -E) часть метасимволов пишется через слеш: a\{2\} вместо a{2}. В JavaScript флаг g — глобальный поиск, i — без регистра, m — многострочный режим, y — поиск с позиции lastIndex. Шаблон, написанный для одного диалекта, перед переносом в другой стоит прогнать на тестовых строках.

Катастрофический бэктрекинг (ReDoS)

Движки Python, JavaScript, PCRE и Java работают с возвратами (backtracking): если продолжение не совпало, движок откатывается и пробует другой вариант разбиения. У некоторых шаблонов число вариантов растет экспоненциально от длины строки. Классический случай — вложенные квантификаторы (a+)+.

import re, time

bad = re.compile(r"^(a+)+$")
good = re.compile(r"^a+$")

for n in (20, 22, 24):
    s = "a" * n + "!"
    t = time.perf_counter()
    bad.match(s)
    print(n, "bad ", f"{time.perf_counter() - t:.2f} c")

s = "a" * 100_000 + "!"
t = time.perf_counter()
good.match(s)
print("good", f"{time.perf_counter() - t:.4f} c")

Вывод одного прогона (абсолютные числа зависят от машины, важен рост):

20 bad  0.04 c
22 bad  0.15 c
24 bad  0.58 c
good 0.0003 c

Плюс два символа — время растет примерно в четыре раза. На строке в 40-50 символов такой шаблон занимает процесс надолго. Если регулярка стоит в обработке пользовательского ввода, злоумышленнику достаточно прислать подобную строку — это ReDoS, отказ в обслуживании через регулярное выражение. Эквивалентный шаблон ^a+$ на строке в 100 000 символов отработал за доли миллисекунды.

Как защищаться:

  1. Убирать неоднозначность: не вкладывать квантификаторы друг в друга, не делать альтернативы, которые совпадают с одним и тем же текстом ((a|a)*, (\w|\d)+).
  2. С Python 3.11 в re есть атомарные группы (?>...) и притяжательные квантификаторы a++: они запрещают возврат внутрь. re.match(r"^(a++)+$", "a" * 30 + "!") сразу возвращает None.
  3. Ограничивать длину входа до применения шаблона.
  4. У re нет параметра тайм-аута. Для недоверенных шаблонов или входа нужны другие движки или изоляция: выполнение в отдельном процессе с ограничением времени либо движок без возвратов, например RE2 с гарантией линейного времени (без обратных ссылок и lookaround).
  5. Никогда не подставлять ввод пользователя в шаблон как есть: сначала re.escape(user_text).

Эти меры снижают риск, но не делают любой шаблон безопасным: сложные выражения на критичном пути стоит проверять на худший вход.

Если не получилось: симптомы и причины

Симптом Частая причина Что сделать
match возвращает None, хотя текст есть match смотрит только начало search
шаблон с \b ничего не находит строка без r r"..."
захвачено слишком много жадный квантификатор *?, +? или класс [^...]
валидация пропускает "123\n" $ перед \n fullmatch или \Z
findall вернул не все совпадение в шаблоне есть группа (?:...) или finditer
программа зависла на одной строке катастрофический бэктрекинг переписать шаблон, лимит длины

Выводы

  • Регулярное выражение — шаблон, совпадение — найденный участок текста, группа — часть шаблона, которую движок запоминает отдельно.
  • Квантификаторы по умолчанию жадные; ? после них делает их ленивыми.
  • В Python шаблоны пишут raw-строками, для валидации используют re.fullmatch, а \d без re.ASCII принимает любые цифры Unicode.
  • Диалекты различаются синтаксисом групп, lookbehind и флагов — шаблон нужно проверять в той среде, где он будет работать.
  • Вложенные квантификаторы вроде (a+)+ открывают ReDoS; защита — переписанный шаблон, атомарные группы, лимит длины и изоляция.

Где применяется / связь с практикой

Освойте тему на практике

Регулярки нужны каждый день: разбор логов, очистка выгрузок, валидация форм, поиск по коду, правила в grep, sed и IDE. В Python они идут рядом со строками, файлами и обработкой ошибок — базой, которую разбирают на курсе по базовому Python. Отдельные приемы работы с текстом и данными удобно посмотреть на открытых уроках Otus.

FAQ

Можно ли регулярным выражением полностью проверить email?
Формат адреса по стандарту слишком сложен для короткого шаблона. Практичнее простая проверка вида «есть @ и точка в домене» плюс письмо с подтверждением.

Чем модуль regex отличается от встроенного re?
Это сторонняя библиотека с расширенным синтаксисом, например классами Unicode-свойств \p{...} и тайм-аутом. Встроенного re хватает для большинства задач.

Быстрее ли re.compile, чем вызов re.search с шаблоном-строкой?
Модуль кэширует недавно использованные шаблоны, поэтому разница обычно небольшая. compile полезен в первую очередь для читаемости и повторного использования объекта.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)