Сайт использует сookies для хранения данных. Продолжая использовать сайт, вы даёте согласие на работу с этими файлами.

ОК
🐍
Основы
Опубликовано:
26.05.2026
Обновлено:
03.06.2026

Извлечение данных из текста с помощью регулярных выражений в Python

Ищенко Тимофей

Бывало у вас такое: нужно вытащить из лог-файла все email-адреса, отфильтровать строки по хитрому паттерну или заменить телефонные номера на заглушки, и вы начинаете городить конструкции из split(), find(), циклов и кучи if-ов? Мы через это прошли. А потом открыли для себя модуль re и поняли: одно регулярное выражение заменяет десять строк ручного парсинга.​

Этот материал покрывает полный цикл работы с re - от метасимволов до именованных групп и компиляции шаблонов. С реальными примерами кода, граблями и практическими советами.

Модуль re: что внутри и зачем

Модуль re входит в стандартную библиотеку Python, ничего ставить не нужно. Импортируем и работаем: import re.​

Вот основные функции, которые вы будете использовать каждый день:

Функция Возвращает Назначение
re.search() Match-объект или None Первое совпадение в строке
re.match() Match-объект или None Совпадение только в начале строки
re.findall() Список строк Все совпадения
re.finditer() Итератор Match-объектов Все совпадения с позициями
re.sub() Строка Замена совпадений
re.split() Список строк Разбиение по шаблону
re.compile() Compiled pattern Компиляция для повторного использования

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) - позиции первой группы

Метасимволы и символьные классы

Вот таблица, которую стоит держать под рукой:

Символ Значение Пример
^ Начало строки ^From - строка начинается с From
$ Конец строки end$ - строка заканчивается на end
. Любой символ (кроме \n) F..m - From, Film и т.д.
* 0 или более повторений a* - пустая строка, a, aa...
+ 1 или более повторений a+ - a, aa, aaa (не пустая)
? 0 или 1 повторение colou?r - color или colour
\s Пробельный символ Разделение токенов
\S Непробельный символ \S+ - слово без пробелов
\d Цифра (эквивалент [0-9]) \d+ - целое число
\w Буква, цифра или _ \w+ - «слово»
\b Граница слова \bword\b - точное слово
[ ] Символьный класс [a-z] - строчная буква
{n,m} От n до m повторений \d{2,4} - от 2 до 4 цифр

Квадратные скобки определяют символьный класс. Запись [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: тонкая настройка поведения

Флаг Краткая форма Эффект
re.IGNORECASE re.I Поиск без учёта регистра
re.MULTILINE re.M ^ и $ работают для каждой строки
re.DOTALL re.S . совпадает и с \n
re.VERBOSE re.X Разрешает комментарии и пробелы в шаблоне
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() возвращает пустой список без исключений.

Итоговая шпаргалка по спецсимволам

Символ Описание Контекст применения
^ Якорь начала строки Фильтрация строк по префиксу
$ Якорь конца строки Проверка расширений файлов
. Любой символ Универсальный заполнитель
\s, \S Пробельный / непробельный Разделение токенов
\d, \D Цифра / не-цифра Извлечение чисел
\w, \W Буква-цифра-_ / остальное Извлечение слов
\b Граница слова Точное совпадение слова
* / *? 0+ жадный / ленивый Опциональные фрагменты
+ / +? 1+ жадный / ленивый Обязательные фрагменты
? 0 или 1 повторение Опциональный символ
{n,m} От n до m повторений Числа фиксированной длины
( ) Группа захвата Извлечение подстроки
(?P) Именованная группа Самодокументируемые шаблоны
(?:...) Группа без захвата Группировка без извлечения
Это авторская статья, основанная на личном опыте и субъективном взгляде автора. Заметили ошибку или битую ссылку? Сообщите нам: info@codesrc.ru - мы оперативно исправим. Спасибо, что помогаете делать блог лучше.
Следите за нами в соцсетях:

Читайте также