Pythonregexочистка данных

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

2026-07-11 11 мин

Регулярные выражения (regex) нужны аналитику, чтобы находить и вытаскивать текст по шаблону: телефоны и email из грязных полей, id из логов, параметры из URL, чистить строки перед агрегацией. На практике 90% моих задач закрываются тремя функциями модуля research, findall, sub — плюс парой методов pandas .str.extract и .str.contains. Ниже разберу минимальный набор конструкций и реальные кейсы, которые я использую почти каждую неделю, чтобы не открывать Excel и не чистить данные руками.

Зачем аналитику вообще нужны регулярные выражения?

Потому что данные почти никогда не приходят чистыми. Телефоны записаны в пяти форматах, email вперемешку с текстом комментария, в логах nginx нужный кусок спрятан между IP и кодом ответа, в UTM-метках сидят опечатки. Обычные строковые методы (split, replace, in) работают, пока формат жёсткий. Как только вариантов становится много — код превращается в лапшу из условий. Regex описывает шаблон один раз и ловит все варианты сразу.

Второй момент — regex спрашивают на собеседованиях. Не наизусть все флаги, а понимание: как достать домен из email, как убрать всё кроме цифр, чем .+ отличается от .+?. Если готовишься к интервью, потренируйся заранее — я собрал типовые задачи в разделе задач по Python и в подборке вопросов с собеседований.

Чем отличаются search, findall и sub в модуле re?

Это три рабочие лошадки. search находит первое совпадение и возвращает объект (или None), findall возвращает список всех совпадений, sub заменяет совпадения на что-то другое. Разберём на одной строке:

import re

text = "Заказ #10432 оплачен, сумма 5 990 руб, клиент id=771"

# первое совпадение + доступ к группе в скобках
re.search(r"#(\d+)", text).group(1)   # '10432'

# все числа подряд
re.findall(r"\d+", text)              # ['10432', '5', '990', '771']

# схлопнуть повторяющиеся пробелы в один
re.sub(r"\s+", " ", text)            # 'Заказ #10432 оплачен, сумма 5 990 руб, клиент id=771'

Обрати внимание на r"..." — это raw-строка. Она отключает интерпретацию бэкслеша Python-ом, поэтому \d доходит до regex-движка как есть, а не как escape-последовательность. Всегда пиши шаблоны через r"...", это спасает от тихих багов.

Ещё пара полезных: re.match проверяет совпадение только с начала строки, re.fullmatch — совпадение со всей строкой целиком (удобно для валидации). А если один и тот же шаблон гоняешь по миллиону строк — скомпилируй его один раз через pattern = re.compile(r"\d+") и вызывай pattern.findall(...), так быстрее.

Минимальный набор конструкций, который реально нужен

Не надо учить весь синтаксис. Вот то, что покрывает почти все задачи аналитика.

Классы символов — что искать:

Квантификаторы — сколько раз:

Якоря и группы:

Этого хватит, чтобы собрать 95% рабочих шаблонов. Остальное гуглится по ходу. Если хочется держать шпаргалку под рукой, у меня есть справочник по Python с примерами.

Как достать телефоны и email из грязных строк?

Классика чистки данных. Телефоны в базе выглядят так: +7 (999) 123-45-67, 8-912-345-67-89, тел. 79161234567. Приводим к единому формату. Мой рабочий приём — не пытаться поймать все варианты одним монстро-шаблоном, а сначала выкинуть всё кроме цифр, потом нормализовать:

import re

phones = ["+7 (999) 123-45-67", "8-912-345-67-89", "тел. 79161234567", "звоните 555"]

def normalize_phone(s):
    digits = re.sub(r"\D", "", s)        # оставить только цифры
    if digits.startswith("8"):
        digits = "7" + digits[1:]        # 8 -> 7 в начале
    if len(digits) == 11 and digits.startswith("7"):
        return "+" + digits
    return None                          # мусор отбрасываем

[normalize_phone(p) for p in phones]
# ['+79991234567', '+79123456789', '+79161234567', None]

Здесь \D — «всё, что не цифра». Одна строка re.sub(r"\D", "", s) убирает скобки, дефисы, пробелы и буквы разом. Проверка длины отсекает обрезанные номера — их лучше пометить как None, чем молча испортить.

С email похоже, но тут нужен именно поиск по шаблону, потому что адрес сидит внутри текста:

email_re = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"

comment = "Пишите на anna@shop.ru или в поддержку sales@shop.co.uk, спасибо!"
re.findall(email_re, comment)
# ['anna@shop.ru', 'sales@shop.co.uk']

Разберём шаблон по кускам: [a-zA-Z0-9._%+-]+ — имя до собаки (буквы, цифры, точки, плюсы), потом литерал @, потом домен [a-zA-Z0-9.-]+, потом обязательная точка \. и зона [a-zA-Z]{2,} из минимум двух букв. Точку экранируем через \., иначе . означала бы «любой символ». Идеальный по RFC email-regex огромный и на практике не нужен — этот ловит все реальные адреса из пользовательских данных.

Как вытащить нужное из логов и URL?

Логи — это где regex окупается мгновенно. Строка access-лога nginx выглядит так, и руками её не распарсить:

log = '95.24.11.7 - - [11/Jul/2026:10:15:42] "GET /catalog?page=2 HTTP/1.1" 200 4213'

# IP-адрес
re.search(r"(\d+\.\d+\.\d+\.\d+)", log).group(1)      # '95.24.11.7'

# метод и путь запроса
m = re.search(r'"(GET|POST|PUT|DELETE) (\S+) HTTP', log)
m.group(1), m.group(2)                                 # ('GET', '/catalog?page=2')

# код ответа: три цифры после закрывающей кавычки
re.search(r'"\s(\d{3})\s', log).group(1)              # '200'
\S+ — «один или больше не-пробельных символов», удобно для путей, где заранее не знаешь длину. Группы (...) вытаскивают ровно то, что нужно, а не всю строку.

С URL то же самое — доставать параметры руками через split('&') можно, но regex короче и надёжнее, когда параметров много:

url = "https://shop.ru/item/55?utm_source=yandex&utm_medium=cpc&utm_campaign=summer"

re.search(r"utm_source=([^&]+)", url).group(1)   # 'yandex'
re.search(r"/item/(\d+)", url).group(1)          # '55'

Ключевой приём здесь — [^&]+, то есть «всё до следующего амперсанда». Так значение параметра не «перельётся» в соседний. Это частая история: люди пишут utm_source=(.+) и удивляются, что в источник попал весь хвост URL. Про эту жадность — ниже.

Как применять regex прямо в pandas?

Когда данные уже в DataFrame, не нужно гонять apply с циклом — у pandas есть векторные строковые методы, которые принимают regex. Два главных — .str.extract (вытащить) и .str.contains (отфильтровать).

import pandas as pd

df = pd.DataFrame({
    "raw": ["Заказ 10432 — Иванов А.", "Заказ 10510 — Петров Б.", "возврат 10432"]
})

# извлечь первое число в новый столбец (группа обязательна)
df["order_id"] = df["raw"].str.extract(r"(\d+)")

# отфильтровать строки, где встречается слово
df[df["raw"].str.contains(r"Заказ", na=False)]
.str.extract требует хотя бы одну группу (...) — она и станет содержимым нового столбца. Можно вытащить сразу несколько полей именованными группами, что удобно читать:
contacts = pd.DataFrame({"line": ["Иван, ivan@mail.ru, +79991112233"]})

parsed = contacts["line"].str.extract(
    r"(?P<name>[А-Яа-я]+),\s*(?P<email>[\w.+-]+@[\w.-]+\.\w+)"
)
# столбцы name и email

У .str.contains не забывай na=False, иначе на пропусках (NaN) вылетит ошибка или вернутся NaN вместо False, и фильтр сломается. А для замены прямо в столбце есть .str.replace(pattern, repl, regex=True) — тот же sub, только векторный. Погонять эти методы на живых датафреймах можно в Python-тренажёре, а связку с SQL-выборками — в SQL-тренажёре, когда данные приходят из базы.

Почему regex захватывает слишком много текста?

Потому что квантификаторы + и * по умолчанию жадные: они хватают максимально длинную строку, а не первое подходящее совпадение. Это ошибка номер один у новичков. Смотри:

html = "<b>жирный</b> и <i>курсив</i>"

re.findall(r"<.+>", html)     # ['<b>жирный</b> и <i>курсив</i>']  — сожрал всё
re.findall(r"<.+?>", html)    # ['<b>', '</b>', '<i>', '</i>']     — по-тегово
re.findall(r"<[^>]+>", html)  # ['<b>', '</b>', '<i>', '</i>']     — тоже верно

Шаблон <.+> прочитал «открывающая скобка, потом как можно больше любых символов, потом закрывающая». «Как можно больше» дотянулось до самой последней > в строке. Два способа починить: добавить ? после квантификатора (.+? — ленивый, берёт минимум), или сузить класс символов через [^>]+ («что угодно, кроме закрывающей скобки»). Второй вариант обычно быстрее и понятнее — он вообще не даёт движку зайти на чужую территорию.

Тот же баг ломает разбор URL из примера выше: utm_source=(.+) жадно захватит yandex&utm_medium=cpc&..., а utm_source=([^&]+) остановится ровно на нужном. Правило простое: как только пишешь .+ или .* — остановись и подумай, не сузить ли класс символов.

Как не наступить на частые грабли с regex?

Собрал ошибки, которые сам ловил и вижу у джунов чаще всего:

Regex — не серебряная пуля. Если структура данных стабильная (например, CSV с фиксированными колонками), обычный split читается лучше и работает быстрее. Regex достаёт из строки то, что не имеет жёсткой структуры, а когда структура есть — пользуйся ей.

Короткая шпаргалка на память

Если унести из текста одно: чистку данных закрывают четыре приёма. re.sub(r"\D", "", s) — оставить только цифры (телефоны, суммы). re.findall(email_re, text) — вытащить все адреса из текста. [^разделитель]+ вместо .+ — забирать значение до ограничителя, не жадничая. И .str.extract / .str.contains в pandas — чтобы не писать циклы по строкам.

Дальше набивается только практикой. Возьми свой реальный грязный столбец и почисти его — это лучше любого учебника. Потренироваться на подготовленных данных можно в тренажёре Python, а на бизнес-логике — в разборах кейсов и практических заданиях, где regex часто нужен как первый шаг перед агрегацией.

Если хочешь системно прокачать Python и SQL под собеседование — в Pro открыты все 400+ Python-задач, кейсы и AI-мок-собеседования без лимитов. Но и на бесплатном тарифе хватит, чтобы отработать regex до автоматизма.

Закрепи Python на задачах
402 Python-задачи через pandas/numpy/scipy — попробуй бесплатно.
Python-тренажёр →