Регулярное выражение (regex, regexp) — это шаблон, который описывает множество строк: движок сверяет шаблон с текстом и находит подходящие участки. С его помощью ищут, проверяют, извлекают и заменяют фрагменты текста: даты в логе, номера в заявке, лишние пробелы в выгрузке.
Содержание
- Четыре термина, которые путают
- Первый пример целиком: ошибки из лога
- Синтаксис: из чего собирается шаблон
- Жадные и ленивые квантификаторы
- Модуль re в Python: какую функцию брать
- Группы в замене и lookahead
- Ловушки валидации ввода
- Диалекты коротко
- Катастрофический бэктрекинг (ReDoS)
- Если не получилось: симптомы и причины
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — синтаксис (классы, квантификаторы, жадность, якоря, группы, 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 символов отработал за доли миллисекунды.
Как защищаться:
- Убирать неоднозначность: не вкладывать квантификаторы друг в друга, не делать альтернативы, которые совпадают с одним и тем же текстом (
(a|a)*,(\w|\d)+). - С Python 3.11 в
reесть атомарные группы(?>...)и притяжательные квантификаторыa++: они запрещают возврат внутрь.re.match(r"^(a++)+$", "a" * 30 + "!")сразу возвращаетNone. - Ограничивать длину входа до применения шаблона.
- У
reнет параметра тайм-аута. Для недоверенных шаблонов или входа нужны другие движки или изоляция: выполнение в отдельном процессе с ограничением времени либо движок без возвратов, например RE2 с гарантией линейного времени (без обратных ссылок и lookaround). - Никогда не подставлять ввод пользователя в шаблон как есть: сначала
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 полезен в первую очередь для читаемости и повторного использования объекта.



