Регулярное выражение описывает не конкретный текст, а его форму: «четыре цифры подряд», «что-то до собаки, потом что-то с точкой». Модуль re ищет по такому описанию. Платить за это приходится читаемостью, поэтому половина статьи о том, когда шаблон не нужен. Код выполнен на CPython 3.14.5.
search возвращает первое совпадение
search начинает слева, пропускает обычный текст и останавливается на первой цепочке цифр. Объект Match хранит и текст 1042, и его границы 6:10.Четыре функции, которых хватает почти всегда
search ищет первое совпадение в любом месте строки и возвращает объект с позициями:
import re
text = "заказ 1042 от 03.06.2026"
m = re.search(r"\d+", text)
print(m.group(), m.start(), m.end())
1042 6 10
match отличается ровно одним: он цепляется к началу строки.
import re
text = "заказ 1042 от 03.06.2026"
print(re.match(r"\d+", text))
print(re.match(r"заказ", text).group())
None
заказ
Первый вызов вернул None, потому что строка начинается не с цифры. match проверяет начало строки, search ищет внутри, а fullmatch проверяет строку целиком.
Когда совпадения нет, возвращается None, а не пустая строка. Обращаться к .group() без проверки нельзя:
import re
m = re.search(r"[A-Z]+", "заказ 1042")
print(m, bool(m))
None False
findall собирает все совпадения списком, sub заменяет:
import re
text = "заказ 1042 от 03.06.2026"
print(re.findall(r"\d+", text))
print(re.sub(r"\d+", "N", text))
print(re.sub(r"\d+", "N", text, count=1))
['1042', '03', '06', '2026']
заказ N от N.N.N
заказ N от 03.06.2026
Из чего собирают шаблон
Три кирпича покрывают большинство задач.
Классы символов. \d цифра, \w буква, цифра или подчёркивание, \s пробельный символ. Заглавные варианты означают отрицание: \D — всё, кроме цифры. Свой класс пишут в квадратных скобках, а крышка внутри них инвертирует набор:
import re
print(re.findall(r"[аеёиоуыэюя]", "заказ"))
print(re.findall(r"[^0-9 ]+", "заказ 1042"))
['а', 'а']
['заказ']
Кванторы. + один и больше, * ноль и больше, ? ноль или один, {4} ровно четыре, {2,} два и больше.
import re
print(re.findall(r"\d{4}", "заказ 1042 заааказ"))
print(re.findall(r"за+каз", "заказ 1042 заааказ"))
['1042']
['заказ', 'заааказ']
Якоря. ^ начало строки, $ конец, \b граница слова. Без якорей шаблон ищет подстроку где угодно, с ними — привязывается к краям.
Группы: как достать части найденного
Скобки выделяют часть шаблона, и её потом можно взять отдельно:
import re
dates = "03.06.2026 и 20.07.2026"
m = re.search(r"(\d{2})\.(\d{2})\.(\d{4})", dates)
print(m.group(0), "|", m.group(1), m.group(3))
03.06.2026 | 03 2026
Нулевая группа содержит всё совпадение целиком, дальше идут скобки слева направо. При трёх и более группах номера путаются, поэтому им дают имена:
import re
dates = "03.06.2026 и 20.07.2026"
m = re.search(r"(?P<day>\d{2})\.(?P<month>\d{2})\.(?P<year>\d{4})", dates)
print(m.group("day"), m.group("year"))
print(m.groupdict())
03 2026
{'day': '03', 'month': '06', 'year': '2026'}
Группы работают и в замене: \1 в строке замены подставляет первую группу.
import re
dates = "03.06.2026 и 20.07.2026"
print(re.sub(r"(\d{2})\.(\d{2})\.(\d{4})", r"\3-\2-\1", dates))
2026-06-03 и 2026-07-20
Отдельная ловушка: как только в шаблоне появляются скобки, findall начинает возвращать группы, а не совпадения целиком.
import re
dates = "03.06.2026 и 20.07.2026"
print(re.findall(r"\d{2}\.\d{2}\.\d{4}", dates))
print(re.findall(r"(\d{2})\.(\d{2})\.(\d{4})", dates))
['03.06.2026', '20.07.2026']
[('03', '06', '2026'), ('20', '07', '2026')]
Скобки добавили ради группировки, а результат сменил форму со строк на кортежи, и дальше код падает на неожиданном типе. Когда нужны и целое совпадение, и части, берут finditer с объектами вместо findall.
Жадность: почему шаблон съел слишком много
Кванторы по умолчанию берут максимум возможного:
import re
html = "<b>жирный</b> и <i>курсив</i>"
print(re.findall(r"<.*>", html))
print(re.findall(r"<.*?>", html))
['<b>жирный</b> и <i>курсив</i>']
['<b>', '</b>', '<i>', '</i>']
Первый шаблон нашёл одно совпадение от первой < до последней >, проглотив весь текст. Знак вопроса после квантора включает ленивый режим: брать минимум, достаточный для совпадения. Разница видна только там, где искомый символ встречается дважды, поэтому на коротком тексте ошибка не проявляется и всплывает на реальных данных.
Та же природа у другой ловушки: точка означает любой символ, кроме перевода строки, а не саму точку. С флагом re.DOTALL она захватывает и перевод строки.
import re
print(re.findall(r"03.06", "03.06 и 03x06"))
print(re.findall(r"03\.06", "03.06 и 03x06"))
['03.06', '03x06']
['03.06']
Первый шаблон нашёл лишнее. Специальные символы . ^ $ * + ? { } [ ] \ | ( ) экранируют обратной чертой, а готовую строку целиком экранирует re.escape.
Зачем перед шаблоном пишут r
Обратная черта что-то значит и для Python, и для регулярных выражений, поэтому без префикса r шаблон читается дважды. Иногда это сходит с рук, иногда ломает всё молча:
import re
print(re.findall("\\d+", "год 2026"))
print(re.findall("\b" + "год", "год 2026"))
print(re.findall(r"\bгод", "год 2026"))
['2026']
[]
['год']
В первом примере обратная черта удвоена: Python превращает "\\d+" в шаблон \d+. А \b в обычной строке — это символ забоя, код 8, и до шаблона доехал он, а не граница слова:
print(repr("\b"), len("\b"))
print(repr(r"\b"), len(r"\b"))
'\x08' 1
'\\b' 2
Отсюда правило без исключений: шаблон всегда пишут сырой строкой, r"...". Заодно Python 3.14 предупреждает о неизвестных escape-последовательностях вроде "\d" при компиляции файла, так что забытый префикс станет заметнее.
Флаги: регистр и многострочность
Флаг передают третьим аргументом:
import re
print(re.findall(r"аня", "Аня и АНЯ"))
print(re.findall(r"аня", "Аня и АНЯ", re.IGNORECASE))
[]
['Аня', 'АНЯ']
re.MULTILINE заставляет ^ и $ совпадать с началом и концом каждой строки, а не всего текста:
import re
print(re.findall(r"^\w+", "первая\nвторая"))
print(re.findall(r"^\w+", "первая\nвторая", re.MULTILINE))
['первая']
['первая', 'вторая']
Шаблон, который используется много раз, компилируют один раз:
import re
digits = re.compile(r"\d+")
print(digits.findall("а1 б22"), digits.sub("#", "а1 б22"))
['1', '22'] а# б#
Ошибка в самом шаблоне выясняется в момент компиляции, а не поиска:
import re
try:
re.compile(r"(\d+")
except re.error as exc:
print(f"re.error: {exc}")
re.error: missing ), unterminated subpattern at position 0
Когда регулярное выражение не нужно
Чаще, чем кажется. Обычные методы строк читаются лучше и работают быстрее:
line = "имя: Аня"
print(line.startswith("имя:"), line.split(": ")[1])
True Аня
Правило выбора простое:
- проверка начала или конца —
startswithиendswith; - разбиение по постоянному разделителю —
split; - замена известного текста —
replace.
Регулярное выражение берут, когда форма переменная: цифры неизвестной длины, необязательные части, несколько допустимых разделителей сразу.
Отдельно про почту: полная проверка адреса по стандарту не помещается в читаемый шаблон, и попытки её написать порождают выражения на сотни символов, которые всё равно ошибаются. Практичный вариант проверяет форму грубо, а существование адреса подтверждает письмом:
import re
pattern = re.compile(r"[^@\s]+@[^@\s]+\.[^@\s]+")
for v in ["anya@example.com", "anya@example", "две@собаки@ru", ""]:
print(f"{v!r:22} {bool(pattern.fullmatch(v))}")
'anya@example.com' True
'anya@example' False
'две@собаки@ru' False
'' False
Шаблон честно отсекает адрес без точки в домене и с двумя собаками. Дальше проверяет только письмо.
Частые вопросы
Чем search отличается от match
match ищет только с начала строки, search в любом месте. Для проверки «строка целиком имеет такой вид» берут fullmatch.
Почему findall вернул кортежи вместо строк
В шаблоне есть скобки. findall отдаёт группы, а не совпадения целиком. Если скобки нужны только для группировки, пиши (?:...) — такая группа не захватывающая и в результат не попадает.
Как найти все совпадения с их позициями
re.finditer возвращает объекты, у каждого есть .group(), .start() и .end(). findall позиции не хранит.
Как проверить строку целиком
re.fullmatch(шаблон, строка). Обычный match проверяет только начало, а $ допускает совпадение перед завершающим переводом строки: re.match(r"\d+$", "42\n") найдёт число, но re.fullmatch(r"\d+", "42\n") вернёт None.
Что учить дальше
Регулярные выражения почти всегда идут в паре с методами строк и с перехватом ошибок при разборе чужих данных. В пути «Python: валидаторы» на Koddo проверки собираются из простых правил, а шаблон подключается только там, где форма действительно переменная. Проверить себя можно на задаче про пароль, где как раз видно, что четыре простых проверки читаются лучше одного длинного выражения.