Бывало у вас такое: нужно вытащить из лог-файла все email-адреса, отфильтровать строки по хитрому паттерну или заменить телефонные номера на заглушки, и вы начинаете городить конструкции из split(), find(), циклов и кучи if-ов? Мы через это прошли. А потом открыли для себя модуль re и поняли: одно регулярное выражение заменяет десять строк ручного парсинга.
Этот материал покрывает полный цикл работы с re - от метасимволов до именованных групп и компиляции шаблонов. С реальными примерами кода, граблями и практическими советами.
Модуль re: что внутри и зачем
Модуль re входит в стандартную библиотеку Python, ничего ставить не нужно. Импортируем и работаем: import re.
Вот основные функции, которые вы будете использовать каждый день:
re.search(): ищем первое совпадение
Функция re.search() принимает шаблон и строку. Нашла совпадение - возвращает Match-объект, не нашла - None. Пример - ищем строки, которые начинаются с «From:»:
import re
hand = open("mbox-short.txt")
for line in hand:
line = line.rstrip()
if re.search("^From:", line):
print(line)
Символ ^ (якорь начала строки) гарантирует, что совпадение сработает только если «From:» стоит в самом начале.
re.match() vs re.search(): классический источник багов
Это ловушка, в которую попадают многие. re.match() проверяет совпадение только в начале строки, а re.search() ищет в любой позиции:
import re
line = "Subject: From: someone"
print(re.match("From:", line)) # None - строка не начинается с From:
print(re.search("From:", line)) # <re.Match object> - найдено в середине
Практическое правило: используйте re.search() с ^, когда нужно привязать к началу. Это нагляднее, чем re.match().
re.findall(): рабочая лошадка для извлечения данных
re.findall() возвращает список всех подстрок, соответствующих шаблону. Нет совпадений - пустой список, никаких исключений. Именно эту функцию вы будете использовать чаще всего при парсинге.
import re
text = "My email is user@example.com and another@test.org"
emails = re.findall(r"\S+@\S+", text)
print(emails) # ['user@example.com', 'another@test.org']
re.finditer(): когда нужны позиции
Когда важны не только значения, но и где именно в строке они нашлись, берём re.finditer():
import re
text = "Цены: $10.00, $25.50 и $3.99"
for match in re.finditer(r"\$[0-9]+\.[0-9]{2}", text):
print(f"Найдено: {match.group()} на позиции {match.span()}")
# Найдено: $10.00 на позиции (7, 13)
# Найдено: $25.50 на позиции (15, 21)
# Найдено: $3.99 на позиции (24, 29)
Match-объект: group(), groups(), span()
re.search() и re.match() возвращают Match-объект с полезными методами:
- group(0) или group() - вся совпавшая подстрока
- group(n) - содержимое n-й группы захвата
- groups() - кортеж всех групп
- span(n) - кортеж (start, end) позиций группы
import re
text = "From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008"
m = re.search(r"^From (\S+@\S+) .* (\d{2}):(\d{2}):(\d{2})", text)
if m:
print(m.group()) # Вся совпавшая строка
print(m.group(1)) # 'stephen.marquard@uct.ac.za'
print(m.group(2)) # '09'
print(m.groups()) # ('stephen.marquard@uct.ac.za', '09', '14', '16')
print(m.span(1)) # (5, 31) - позиции первой группы
Метасимволы и символьные классы
Вот таблица, которую стоит держать под рукой:
Квадратные скобки определяют символьный класс. Запись [a-zA-Z0-9] означает «любая латинская буква или цифра». Важный нюанс: внутри квадратных скобок точка теряет специальное значение и обозначает обычную точку.
Жадное сопоставление: грабли, на которые наступают все
Квантификаторы * и + по умолчанию работают в жадном режиме (greedy) - захватывают максимально длинную подстроку:
import re
x = "From: Using the : character"
y = re.findall("^F.+:", x)
print(y)
# ['From: Using the :']
Видите? Вместо «From:» жадный режим захватил всё до последнего двоеточия. Добавляем ? после квантификатора - переключаемся в нежадный (ленивый) режим:
import re
x = "From: Using the : character"
y = re.findall("^F.+?:", x)
print(y)
# ['From: Using the :']
Жадный режим подходит, когда граница определена чётким символом (конец строки). Нежадный - когда внутри строки встречаются дублирующиеся разделители.
Группы захвата: извлекаем именно то, что нужно
Круглые скобки создают группу захвата. Вся строка проверяется на соответствие полному шаблону, но findall() возвращает только содержимое групп.
Извлечение чисел из X-DSPAM заголовков
import re
hand = open("mbox-short.txt")
for line in hand:
line = line.rstrip()
x = re.findall(r"^X\S*: ([0-9.]+)", line)
if x:
print(x)
# ['0.8475']
# ['0.6178']
Шаблон ^X\S*: фильтрует X-заголовки, а ([0-9.]+) захватывает число.
Замечание: шаблон [0-9.]+ технически неточен - он совпадёт с ... или 1.2.3.4. Для строгой валидации числа с плавающей точкой используйте (\d+\.\d+) или (\d+(?:\.\d+)?).
Именованные группы: код, который читается сам
Для сложных шаблонов с несколькими группами удобнее использовать именованные группы (?P<name>...):
import re
text = "From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008"
m = re.search(
r"^From (?P<email>\S+@\S+) \w+ \w+ \d+ (?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})",
text,
)
if m:
print(m.group("email")) # stephen.marquard@uct.ac.za
print(m.group("hour")) # 09
print(m.groupdict()) # {'email': 'stephen.marquard@uct.ac.za', 'hour': '09', ...}
group('email') читается в разы лучше, чем group(1). В продакшен-коде это критично.
Извлечение email-адресов: строим шаблон по шагам
Первая версия: грубый захват
import re
line = "<stephen.marquard@uct.ac.za>; <alice@example.com>"
x = re.findall(r"\S+@\S+", line)
print(x) # ['<stephen.marquard@uct.ac.za>;', '<alice@example.com>']
Результат содержит мусор: угловые скобки, точки с запятой.
Уточнённая версия: символьные классы на краях
import re
line = "<stephen.marquard@uct.ac.za>; <alice@example.com>"
x = re.findall(r"[a-zA-Z0-9]\S*@\S*[a-zA-Z]", line)
print(x) # ['stephen.marquard@uct.ac.za', 'alice@example.com']
[a-zA-Z0-9] на старте требует букву или цифру, [a-zA-Z] на конце - букву. Это отсекает скобки и пунктуацию. Итеративный подход «грубо → точнее → ещё точнее» - самый надёжный способ строить регулярки.
re.sub(): замена по шаблону
re.sub() заменяет все совпадения шаблона на указанную строку:
import re
text = "Звоните: 8-800-555-35-35 или 8-495-123-45-67"
# Замена номеров на заглушку
cleaned = re.sub(r"8-\d{3}-\d{3}-\d{2}-\d{2}", "[ТЕЛЕФОН]", text)
print(cleaned)
# 'Звоните: [ТЕЛЕФОН] или [ТЕЛЕФОН]'
# Замена с обратной ссылкой - оставить только код города
result = re.sub(r"8-(\d{3})-\d{3}-\d{2}-\d{2}", r"код: \1", text)
print(result)
# 'Звоните: код: 800 или код: 495'
\1 в строке замены ссылается на первую группу захвата. Мощная штука для трансформации текста.
re.sub() с функцией
Вместо строки замены можно передать функцию, которая принимает Match-объект:
import re
text = "Цены: $10, $25, $3"
def double_price(match):
price = int(match.group(1))
return f"${price * 2}"
result = re.sub(r"\$(\d+)", double_price, text)
print(result)
# 'Цены: $20, $50, $6'
re.split(): когда str.split() не справляется
str.split() разбивает только по фиксированной подстроке. А если разделители разнородные? Тут приходит re.split():
import re
text = "слово1, слово2; слово3 слово4"
# str.split не справится
print(text.split()) # ['слово1,', 'слово2;', 'слово3', 'слово4']
# re.split разбивает по любому набору разделителей
print(re.split(r"[,;\s]+", text)) # ['слово1', 'слово2', 'слово3', 'слово4']
re.compile(): ускоряемся на больших файлах
При многократном использовании одного шаблона (например, в цикле по миллионам строк) компиляция ускоряет выполнение:
import re
pattern = re.compile(r"^From (\S+@\S+)")
with open("mbox-short.txt") as f:
for line in f:
m = pattern.search(line)
if m:
print(m.group(1))
re.compile() возвращает объект Pattern, у которого есть те же методы: .search(), .findall(), .sub(), .split(). Внутри Python кэширует последние использованные шаблоны, поэтому для единичных вызовов разница минимальна. Но для обработки больших файлов компиляция - хорошая практика.
Флаги модуля re: тонкая настройка поведения
import re
# Поиск без учёта регистра
print(re.findall(r"from", "From: user@mail.com", re.IGNORECASE))
# ['From']
# Многострочный режим
text = "Строка 1\nFrom: alice\nСтрока 3"
print(re.findall(r"^From:.*", text, re.MULTILINE))
# ['From: alice']
# Verbose-режим для сложных шаблонов
email_pattern = re.compile(
r"""
[a-zA-Z0-9._%+-]+ # Имя пользователя
@ # Символ @
[a-zA-Z0-9.-]+ # Домен
\.[a-zA-Z]{2,} # Домен верхнего уровня (TLD: Top-Level Domain)
""",
re.VERBOSE,
)
emails = [
"user@example.com",
"first.last@sub.domain.co.uk",
"invalid@.com",
"test@example.c",
]
for email in emails:
if email_pattern.findall(email):
print(f"✓ {email}")
else:
print(f"✗ {email}")
# ✓ user@example.com
# ✓ first.last@sub.domain.co.uk
# ✗ invalid@.com
# ✗ test@example.
re.VERBOSE позволяет разбивать сложные шаблоны на строки с комментариями. Читаемость вырастает в разы.
Экранирование: обратный слеш и raw-строки
Метасимволы имеют специальное значение в шаблоне. Для поиска литерального символа ставим обратный слеш:
import re
x = "We just received $10.00 for cookies."
y = re.findall(r"\$\d+\.\d{2}", x)
print(y)
# ['$10.00']
В продакшен-коде шаблоны записывают как raw-строки с префиксом r: r"\$\d+" вместо "\\$\\d+". Префикс r отключает интерпретацию escape-последовательностей Python. Для сложных шаблонов это обязательно.
re.escape(): если шаблон формируется динамически
Когда шаблон приходит из пользовательского ввода, re.escape() экранирует все спецсимволы:
import re
user_input = "price is $10.00 (USD)"
safe_pattern = re.escape(user_input)
# 'price\\ is\\ \\$10\\.00\\ \\(USD\\)'
re.search(safe_pattern, "The price is $10.00 (USD) today")
Обработка ошибок в шаблонах
Некорректный шаблон вызывает re.error. При динамическом построении шаблонов обязательно оборачивайте в try/except:
import re
user_pattern = "[invalid"
try:
re.compile(user_pattern)
except re.error as e:
print(f"Ошибка в регулярном выражении: {e}")
# Ошибка в регулярном выражении: unterminated character set at position 0
Типичные грабли, на которые наступают все
Забытый raw-строковый префикс
import re
# Проблема: \b в обычной строке - это backspace (ASCII 8)
re.findall("\bword\b", "a word here") # [] - ничего не найдено
# Решение: raw-строка
re.findall(r"\bword\b", "a word here") # ['word']
Избыточное экранирование внутри [ ]
Внутри символьного класса большинство метасимволов теряют значение. [.] - это обычная точка, не нужно писать [\.].
Использование re для тривиальных задач
Если задача решается через str.startswith(), in или str.split() - регулярные выражения избыточны. Они дороже по производительности и труднее для чтения. Не стреляйте из пушки по воробьям.
Практический пример: извлечение часа из временной метки
Классическое упражнение - вытащить час из строки "From user@mail.com Sat Jan 5 09:14:16 2008":
import re
line = "From user@mail.com Sat Jan 5 09:14:16 2008"
# Без regex: разбиение на слова, затем извлечение часа
# (уязвимо к изменению формата строки, отсутствует проверка длины)
words = line.split() # ['From', 'user@mail.com', 'Sat', 'Jan', '5', '09:14:16', '2008']
time_parts = words[5].split(":") # ['09', '14', '16']
hour = time_parts[0] # '09'
# С regex: захват часа при условии, что строка начинается с 'From'
match = re.findall(r"^From .* (\d{2}):\d{2}:\d{2}", line) # '09' or []
Регулярное выражение выполняет фильтрацию и извлечение атомарно: если строка не соответствует шаблону, findall() возвращает пустой список без исключений.



.svg.webp)





