Регулярные выражения (regex) нужны аналитику, чтобы находить и вытаскивать текст по шаблону: телефоны и email из грязных полей, id из логов, параметры из URL, чистить строки перед агрегацией. На практике 90% моих задач закрываются тремя функциями модуля re — search, 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(...), так быстрее.
Минимальный набор конструкций, который реально нужен
Не надо учить весь синтаксис. Вот то, что покрывает почти все задачи аналитика.
Классы символов — что искать:
\d— цифра,\D— не цифра\w— буква/цифра/подчёркивание,\W— наоборот\s— пробельный символ (пробел, таб, перенос строки),\S— не пробельный.— любой символ кроме переноса строки[abc]— один из перечисленных,[^abc]— любой кроме перечисленных,[0-9]— диапазон
Квантификаторы — сколько раз:
+— один или больше,*— ноль или больше,?— ноль или один{3}— ровно три,{2,4}— от двух до четырёх
Якоря и группы:
^— начало строки,$— конец строки,\b— граница слова(...)— группа, из которой потом достаём значение через.group(n)(?:...)— группа без захвата (когда скобки нужны для логики, но значение не нужно)a|b— «или»
Этого хватит, чтобы собрать 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?
Собрал ошибки, которые сам ловил и вижу у джунов чаще всего:
- Забытая raw-строка. Пишешь
"\d+"вместоr"\d+"— иногда работает, иногда нет, потому что\dдля Python не спецсимвол, а\b— уже да (это backspace). Всегдаr"...". - Неэкранированная точка. В
email\.ruточка означает «любой символ», так чтоemailXruтоже совпадёт. Нужен literal — экранируй:email\.ru. group(0)противgroup(1).group(0)— всё совпадение целиком,group(1)— содержимое первой скобки. Когда достаёшь id из#(\d+), тебе нуженgroup(1), а неgroup(0)(там будет#10432с решёткой).searchвернулNone. Если совпадения нет,re.search(...)отдаётNone, и.group(1)падает сAttributeError. Проверяй результат перед обращением:m = re.search(...); if m: ....- Кириллица и
\w. В Python 3\wпо умолчанию понимает и русские буквы, но если явно указать флагre.ASCII, кириллица выпадет. Для русских имён проще писать[А-Яа-яЁё]явно. - Жадность там, где не ждёшь. Разобрали выше — держи в голове при каждом
.+.
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 до автоматизма.