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

Ноутбук с размытым кодом на экране рядом с длинными распечатками журнала Изучение

Регулярные выражения в Python лежат в модуле re стандартной библиотеки: для работы с ними устанавливать ничего не нужно, хватает строки import re. Вы задаёте шаблон (pattern) фрагмента текста, а встроенный модуль находит его, заменяет и разрезает строки по найденному. Примеры запускались на Python 3.14.3, вывод скопирован из консоли.

Ноутбук с размытым кодом на экране рядом с длинными распечатками журнала

Что такое регулярные выражения и когда они не нужны

Регулярное выражение (regular expression, в разговоре регулярка или regex) состоит из обычных символов и метасимволов. Обычные символы означают сами себя, а метасимволы задают условие: любая цифра, начало строки, три повторения. Запись \d{4}-\d{2}-\d{2} читается так: четыре цифры, дефис, две цифры, дефис, две цифры. Под неё подходит дата 2026-03-14 в любом месте текста.

Регулярные выражения используют для поиска по формату, когда точного значения вы заранее не знаете: номера заказов в логе, даты, телефоны, адреса электронной почты. Ими же проверяют формат данных из формы. Если нужна фиксированная подстрока, модуль re лишний. Методы строки "ERROR" in line, line.startswith() и line.replace() обычно работают намного быстрее и читаются проще. HTML регулярными выражениями тоже не разбирают: простое выражение справляется с частым случаем и ломается на особых, а для этой задачи есть парсеры.

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

Синтаксис регулярных выражений: шпаргалка

Запись Что означает Пример Совпадёт с
. любой символ, кроме новой строки к.т кот, кит
^ начало строки ^Error Error в самом начале
$ конец строки \.txt$ .txt в конце
\d любая цифра \d\d 42
\D символ, кроме цифры \D а, пробел
\s пробельный символ: пробел, табуляция, перевод строки и другие a\sb a b
\S любой символ, кроме пробельного \S+ слово
\w буква, цифра или нижнее подчёркивание \w+ user_42
\W любой символ, кроме \w \W запятая
\b граница слова \bкот\b кот, но не котёнок
\B не граница слова \Bкот кот в слове «скот»
[abc] один символ из набора [аеё] а, е или ё
[a-z], [A-Za-z0-9] диапазон символов [0-5] цифра от 0 до 5
[^0-9] символ вне набора [^0-9]+ ab в строке 12ab34
\A, \Z начало и конец всего текста \A\d+\Z только цифры
(...) группа с захватом (\d+) число, доступное отдельно
(?:...) группа без захвата (?:ab)+ abab
(?P<name>...) именованная группа (?P<year>\d{4}) год по имени year
Квантификатор Количество повторений Ленивый вариант
* 0 и больше *?
+ 1 и больше +?
? 0 или 1 ??
{3} ровно три нет смысла: число задано
{2,4} 2, 3 или 4 раза {2,4}?
{2,} два и больше {2,}?

Символ вертикальной черты означает «или»: кот|пёс найдёт любое из двух слов. Точка, скобки, плюс и другие специальные символы, которые нужны как обычные, экранируют обратной косой чертой: \. соответствует только точке. Символ $ совпадает ещё и перед завершающим переводом строки, а записи \A и \Z смотрят на границы всего текста; на Python 3.14 и новее конец текста можно написать и как \z.

В строках типа str классы \d, \s и \w работают по таблицам Unicode. Метасимвол \w ловит кириллицу, \s учитывает и неразрывный пробел, а \d принимает и цифры других письменностей, например восточноарабские.

Читайте также:  Задачи по SQL с решениями: от SELECT до оконных функций

Лупа над стопкой бумажных полос с размытыми строками

Основные функции re: search, match и fullmatch

Три функции принимают регулярное выражение и строку, а различаются местом поиска. При удаче функция возвращает объект Match, при неудаче None.

import re

text = "2026-03-14 09:26:53 ОШИБКА Оплата заказа 4417 не прошла"

result = re.search(r"заказа \d+", text)
print(result)
print(result.group(), result.span(), result.start(), result.end())

print(re.match(r"\d{4}", text))
print(re.match(r"ОШИБКА", text))
print(re.search(r"ОШИБКА", text))

print(re.fullmatch(r"\d{4}-\d{2}-\d{2}", text))
print(re.fullmatch(r"\d{4}-\d{2}-\d{2}", "2026-03-14"))

result = re.search(r"заказа \d+", "Оплата прошла")
if result:
    print(result.group())
else:
    print("Совпадения нет")

try:
    re.search(r"заказа \d+", "Оплата прошла").group()
except AttributeError as exc:
    print("AttributeError:", exc)

Вывод:

<re.Match object; span=(34, 45), match='заказа 4417'>
заказа 4417 (34, 45) 34 45
<re.Match object; span=(0, 4), match='2026'>
None
<re.Match object; span=(20, 26), match='ОШИБКА'>
None
<re.Match object; span=(0, 10), match='2026-03-14'>
Совпадения нет
AttributeError: 'NoneType' object has no attribute 'group'

re.search(pattern, string) ищет по всей строке и возвращает первое совпадение. У объекта Match метод group() отдаёт найденный фрагмент текста, span() возвращает кортеж из начала и конца, а start() и end() дают эти позиции по отдельности.

Регулярные выражения встречаются и в тестах: pytest.raises(..., match=...) в pytest проверяет строковое представление исключения через re.search.

re.match проверяет только начало строки. \d{4} нашёлся, ведь строка начинается с года, а ОШИБКА дала None, потому что слово стоит на позиции 20. re.search его находит. re.fullmatch требует совпадения со всей строкой: длинная запись лога не прошла, отдельная дата прошла.

Последняя строка вывода показывает падение, которое ждёт код без проверки: если совпадения нет, функция вернула None, и .group() падает с AttributeError. Нужна проверка if result:, как в примере, и только затем обращение к группам.

Монитор с размытым кодом, одна строка выделена янтарной полосой

Что изменилось в Python 3.15

В версии 3.15 у match появилось второе имя: re.prefixmatch. Старое имя мягко устарело, то есть удалять его не собираются, а новому коду советуют писать prefixmatch. На Python 3.14 и более ранних такой функции нет (на 3.14.3 hasattr(re, "prefixmatch") дал False), поэтому код, который должен работать и на старых версиях, остаётся на match.

findall, finditer, sub и split: все совпадения, замена и разделение

В отличие от search, эти четыре функции при отсутствии совпадений не возвращают None.

import re

text = "Сдача 14.03.2026, проверка 21.03.2026; релиз 01.04.2026"
pattern = r"\d{2}\.\d{2}\.\d{4}"

print(re.findall(pattern, text))

for result in re.finditer(pattern, text):
    print(result.group(), "с позиции", result.start())

print(re.sub(pattern, "[дата]", text))
print(re.sub(pattern, "[дата]", text, count=1))

print(re.split(r"[,;]\s*", text))
print(re.split(r"[,;]\s*", text, maxsplit=1))

Вывод:

['14.03.2026', '21.03.2026', '01.04.2026']
14.03.2026 с позиции 6
21.03.2026 с позиции 27
01.04.2026 с позиции 45
Сдача [дата], проверка [дата]; релиз [дата]
Сдача [дата], проверка 21.03.2026; релиз 01.04.2026
['Сдача 14.03.2026', 'проверка 21.03.2026', 'релиз 01.04.2026']
['Сдача 14.03.2026', 'проверка 21.03.2026; релиз 01.04.2026']
Функция Что возвращает Если совпадений нет
re.findall список строк или кортежей пустой список
re.finditer итератор объектов Match итератор без элементов
re.sub новую строку исходную строку без изменений
re.split список частей список из одной исходной строки

Руки раскладывают карточки с незаполненными полями по разным стопкам

re.findall(pattern, string) удобна, когда нужен только текст. Если требуются ещё и позиции, идите через re.finditer: цикл for получает полноценные объекты Match. В регулярном выражении точка экранирована, иначе запись \d{2}.\d{2} приняла бы и 14-03.

re.sub(pattern, repl, string) заменяет все вхождения, а count=1 ограничил замену первым. Строка в Python неизменяема, поэтому результат нужно сохранить в переменную. re.split разделяет строку по заданному выражению: здесь разделители указаны в наборе [,;], за ними идёт любое количество пробелов.

count и maxsplit передавайте только по имени. Начиная с Python 3.13 позиционная передача устарела и в будущем перестанет работать. На 3.14.3 вызов re.sub("a", "b", "aa", 1) при запуске с python -W default выдаёт (путь к файлу опущен):

DeprecationWarning: 'count' is passed as positional argument

Группы и именованные группы

Круглые скобки вырезают из совпадения отдельные части, например день, месяц и год из даты. Группы нумеруются слева направо, а group(0) возвращает совпадение целиком.

import re

text = "Дата рождения: 07.11.1998"
result = re.search(r"(?P<day>\d{2})\.(?P<month>\d{2})\.(?P<year>\d{4})", text)
print(result.group(0))
print(result.group(1), result.group("year"))
print(result.groups())
print(result.groupdict())

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

phones = "Анна +79125550123, склад 84955550145"
print(re.findall(r"(\+7|8)\d{10}", phones))
print(re.findall(r"(?:\+7|8)\d{10}", phones))
print(re.findall(r"(\+7|8)(\d{10})", phones))

def hide_number(match):
    return match.group("prefix") + "*" * 7 + match.group("tail")

print(re.sub(r"(?P<prefix>\+7|8)\d{7}(?P<tail>\d{3})", hide_number, phones))

print(re.split(r"(,)\s*", phones))

Вывод:

07.11.1998
07 1998
('07', '11', '1998')
{'day': '07', 'month': '11', 'year': '1998'}
Дата рождения: 1998-11-07
['+7', '8']
['+79125550123', '84955550145']
[('+7', '9125550123'), ('8', '4955550145')]
Анна +7*******123, склад 8*******145
['Анна +79125550123', ',', 'склад 84955550145']

Запись (?P<year>...) даёт группе имя, и код result.group("year") понятнее, чем result.group(3). Метод groups() вернул кортеж всех групп, groupdict() словарь именованных. В замене \3-\2-\1 ссылается на группы по номеру, и дата перевернулась в формат ГГГГ-ММ-ДД. На именованную группу в строке замены ссылаются записью \g<year>.

Органайзер с отдельными ячейками, в которых лежат разноцветные детали

Функция findall меняет форму результата по числу групп. С одной группой список содержит только её текст: первый вызов вернул ['+7', '8']. Группа без захвата (?:...) на результат не влияет, и второй вызов отдал номера целиком. Две группы дали список кортежей.

Читайте также:  Хеш-таблица: как работает, коллизии и реализация на Python

Вместо строки замены re.sub принимает функцию: она вызывается для каждого совпадения, получает объект Match и возвращает новый фрагмент текста. Так номер спрятан за звёздочками, а префикс и три последние цифры остались. Захватывающие скобки влияют и на re.split: разделитель, попавший в группу, остаётся в списке.

Жадные и ленивые квантификаторы, флаги регулярных выражений

По умолчанию *, + и ? забирают как можно больше текста, это жадное поведение. Знак вопроса после квантификатора делает его ленивым: совпадение будет минимальным. Именованный аргумент flags включает дополнительные режимы поиска.

import re

html = '<a href="/docs">Справка</a> и <a href="/blog">Блог</a>'
print(re.findall(r'href=".*"', html))
print(re.findall(r'href=".*?"', html))

text = "Ошибка: диск заполнен\nИнфо: запуск\nошибка: нет сети"
print(re.findall(r"^ошибка", text))
print(re.findall(r"^ошибка", text, flags=re.IGNORECASE))
print(re.findall(r"^ошибка", text, flags=re.IGNORECASE | re.MULTILINE))

print(re.search(r"диск.*сети", text))
print(re.search(r"диск.*сети", text, flags=re.DOTALL).span())

pattern = re.compile(r"""
    (?P<hours>[01]\d|2[0-3])   # часы от 00 до 23
    :
    (?P<minutes>[0-5]\d)       # минуты от 00 до 59
""", flags=re.VERBOSE)
print(pattern.findall("Старт 09:26, стоп 25:10, отчёт 23:59"))

Вывод:

['href="/docs">Справка</a> и <a href="/blog"']
['href="/docs"', 'href="/blog"']
[]
['Ошибка']
['Ошибка', 'ошибка']
None
(8, 51)
[('09', '26'), ('23', '59')]

Жадный .* дошёл до последней кавычки и захватил обе ссылки одним совпадением. Ленивый .*? остановился на первой подходящей кавычке, и ссылок в результате две. Строка здесь учебная: настоящую страницу разбирайте парсером.

Рука с ножницами отрезает короткий кусок длинной бумажной ленты

Флаг Короткое имя Что делает
re.IGNORECASE re.I игнорирует регистр
re.MULTILINE re.M ^ и $ работают для каждой строки текста
re.DOTALL re.S точка совпадает и с переводом строки
re.VERBOSE re.X разрешает пробелы и комментарии в шаблоне
re.ASCII re.A \w, \b, \d, \s и их пары с большой буквы работают только с ASCII

Флаги объединяет оператор |. Без флагов ^ошибка не нашла ничего, потому что первая строка начинается с заглавной буквы. С IGNORECASE заглавная «О» подошла, и совпадение одно, а MULTILINE добавил начало третьей строки лога. Без DOTALL точка не переходит через символ новой строки \n, и поиск от «диск» до «сети» вернул None.

С VERBOSE удобно написать длинное регулярное выражение: пробелы и переносы внутри него игнорируются, а после # идёт комментарий. Время 25:10 не прошло, потому что часы ограничены диапазоном до 23.

Raw-строки, compile и проверка формата данных

Перед регулярным выражением обычно ставят букву r. Обратная косая черта значит своё и в обычной строке Python, и в регулярном выражении, а в raw-строке Python её не обрабатывает.

import re

print(len("\n"), len(r"\n"))
print(re.findall("\bкот\b", "кот и котёнок"))
print(re.findall(r"\bкот\b", "кот и котёнок"))

print(re.escape("price(1+1).txt"))

email_pattern = re.compile(r"[\w.+-]+@[\w-]+(?:\.[\w-]+)+")
for address in ["anna@example.com", "anna@example", "a b@example.com"]:
    print(address, bool(email_pattern.fullmatch(address)))

print(bool(re.search(r"^\d{4}$", "2026\n")))
print(bool(re.fullmatch(r"\d{4}", "2026\n")))

digits = "٢٠٢٦"
print(bool(re.fullmatch(r"\d{4}", digits)))
print(bool(re.fullmatch(r"\d{4}", digits, flags=re.ASCII)))
print(bool(re.fullmatch(r"[0-9]{4}", digits)))

Вывод:

1 2
[]
['кот']
price\(1\+1\)\.txt
anna@example.com True
anna@example False
a b@example.com False
True
False
True
False
False

В обычной строке "\b" превращается в символ backspace, поэтому первый поиск слова вернул пустой список, причём молча. С "\d" Python хотя бы предупредит. На 3.14.3 для такой строки в файле Python напечатает (путь к файлу опущен) и продолжит работу:

SyntaxWarning: "\d" is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\d"? A raw string is also an option.

re.escape экранирует всё специальное: пригодится, если часть шаблона приходит от пользователя или содержит имя файла.

Регулярное выражение для адреса электронной почты ловит опечатки вроде пропущенной доменной зоны или пробела, но не скажет, существует ли ящик. Поля формы, телефоны и индексы проверяйте через fullmatch: $ совпадает и перед завершающим \n, поэтому ^\d{4}$ пропустил "2026\n", а fullmatch отказал.

Читайте также:  Калькулятор на Python: от консольной версии до окна на tkinter

Стопка бланков с пустыми полями и ручка рядом на столе

Последние три строки относятся к Unicode. Для \d подходят и восточноарабские цифры, поэтому год ٢٠٢٦ прошёл. Если нужны только цифры от 0 до 9, пишите [0-9] или ставьте флаг re.ASCII.

Нужен ли re.compile

re.compile создаёт объект Pattern с теми же методами search, match, findall, sub, split. Функции модуля кешируют последние регулярные выражения, так что повторной разборки при каждом вызове нет. Наш замер на 3.14.3 с регулярным выражением r"заказа \d+" и строкой из первого примера: миллион вызовов re.search занял 0,65-0,68 с, а миллион вызовов pattern.search занял 0,33-0,50 с. Разница меньше трети микросекунды на вызов заметна только в очень горячем цикле. Мы берём compile прежде всего ради имени, как у email_pattern, и ради VERBOSE.

Неверное регулярное выражение модуль отвергает исключением re.PatternError (до 3.13 оно называлось re.error, старое имя работает). Например, re.compile("(") падает с текстом missing ), unterminated subpattern at position 0.

Стойка серверов с индикаторами в тёмном машинном зале

Как регулярные выражения подвешивают программу

Опасны сложные выражения с вложенными квантификаторами. Поиск (a+)+$ в строке из букв a с восклицательным знаком на конце занял у нас 0,30 с при 22 буквах, 1,19 с при 24 и 4,95 с при 26: каждые две буквы увеличивают время примерно вчетверо. Если такое регулярное выражение применяется к вводу пользователя, одна длинная строка надолго займёт процесс.

Уберите вложенность: a+$ на 26 буквах отработал меньше чем за 0,0001 с. Начиная с Python 3.11 есть притяжательные квантификаторы *+, ++, ?+, которые не делают откат: (a++)+$ занял около 0,0001 с.

Практические задачи на регулярные выражения

Решите задачи сами и сверьте с таблицей.

Задача Решение Вход Результат
Извлечь первое слово re.search(r"\w+", text).group() "Привет, мир" Привет
Получить первые две буквы каждого слова re.findall(r"\b\w{2}", text) "регулярные выражения" ['ре', 'вы']
Найти все слова, начинающиеся на гласную re.findall(r"\b[аеёиоуыэюя]\w*", text, flags=re.I) "Окно и дверь" ['Окно', 'и']
Извлечь домены из списка адресов re.findall(r"@([\w.-]+)", text) "a@mail.ru, b@gmail.com" ['mail.ru', 'gmail.com']
Проверить формат телефонного номера bool(re.fullmatch(r"\+7[0-9]{10}", text)) "+79125550123" True
Разбить строку по нескольким разделителям re.split(r"[;,\s]+", text) "a, b;c d" ['a', 'b', 'c', 'd']
Заменить несколько пробелов одним re.sub(r"\s{2,}", " ", text) "много пробелов" много пробелов

В задаче с гласными граница слова \b не даёт совпасть серединам слов, а флаг снимает различие между заглавной и строчной буквой. В задаче с доменами группа оставляет в списке только часть после @.

Закрепить синтаксис помогут и задачи по Python для начинающих: в задаче о самых частых словах попробуйте выделить слова через re.findall(r"\w+", text.lower()) вместо split().

Частые вопросы

Что такое регулярные выражения в Python?

Регулярные выражения описывают, как выглядит нужный фрагмент текста: какие символы и сколько раз в нём встречаются. Встроенный модуль re принимает такое описание строкой и ищет совпадения, заменяет их или разбивает строку.

re.match проверяет только начало строки, re.search просматривает строку целиком и возвращает первое совпадение. Если проверить нужно всю строку, используйте re.fullmatch.

Как обозначить любое количество символов?

Запись .* означает любое количество символов, включая ноль; символ перевода строки в него не входит. Минимальное совпадение даёт .*?, а с флагом re.DOTALL точка переходит и через перевод строки.

Почему re.findall вернул кортежи вместо строк?

В шаблоне две группы или больше. С одной группой findall вернёт только её текст. Скобки, которые нужны лишь для «или» и повторений, запишите как (?:...), тогда в списке окажутся совпадения целиком.

Как найти совпадения без учёта регистра?

Передайте flags=re.IGNORECASE или сокращённо flags=re.I. С кириллицей флаг тоже работает: re.findall(r"привет", "ПРИВЕТ", flags=re.I) вернёт ['ПРИВЕТ'].

Где проверить регулярное выражение онлайн?

На regex101.com среди поддерживаемых диалектов есть Python. Окончательную проверку делайте в своём коде на нужной версии.

Оцените статью
bestprogrammer.ru
Добавить комментарий