Регулярные выражения в Python: модуль re и шаблоны

Python Автор: Среда и версия: CPython 3.14.5
содержание

Регулярное выражение описывает не конкретный текст, а его форму: «четыре цифры подряд», «что-то до собаки, потом что-то с точкой». Модуль re ищет по такому описанию. Платить за это приходится читаемостью, поэтому половина статьи о том, когда шаблон не нужен. Код выполнен на CPython 3.14.5.

Python / 01

search возвращает первое совпадение

search возвращает первое совпадение01 Шаблон r"\d+" Одна или несколько цифр подряд. 02 Строка заказ 1042 от 03.06.2026 Поиск идёт слева: до даты дело не доходит. 03 Match group(): "1042" start(): 6 · end(): 10 Правая граница не входит в найденный фрагмент.01Шаблонr"\d+"Одна или несколько цифр подряд.02Строказаказ 1042 от 03.06.2026Поиск идёт слева: до даты дело не доходит.03Matchgroup(): "1042"start(): 6 · end(): 10Правая граница не входит в найденный фрагмент.
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 проверки собираются из простых правил, а шаблон подключается только там, где форма действительно переменная. Проверить себя можно на задаче про пароль, где как раз видно, что четыре простых проверки читаются лучше одного длинного выражения.

Источники