Регулярные выражения

⏱ 40 мин · 🧩 заданий: 5

Зачем регулярные выражения

Регулярное выражение — шаблон для поиска текста. Помогает, когда простых in, startswith, split не хватает: найти все email в тексте, проверить формат, разобрать строку лога.

import re

text = "Заказы: #1024 от Анны, #77 от Бори, #3001 от Веры"
print(re.findall(r"#(\d+)", text))
print(re.sub(r"#(\d+)", r"№\1", text))

Всегда raw-строки

Пишите шаблоны как r"...": иначе \b (граница слова) превратится в символ backspace, а \d вызовет предупреждение. В raw-строке обратный слэш остаётся обратным слэшем.

Функции модуля re

import re

s = "Python 3.12 вышел в 2023, Python 3.13 — в 2024"

print(re.search(r"\d{4}", s))              # первое совпадение где угодно → Match или None
print(re.match(r"\d{4}", s))               # только в начале строки
print(re.fullmatch(r"\d{4}", "2024"))      # вся строка целиком — для валидации
print(re.findall(r"\d{4}", s))             # все совпадения списком строк
for m in re.finditer(r"3\.(\d+)", s):      # все совпадения как Match-объекты
    print("  ", m.group(), m.group(1), m.span())
print(re.sub(r"\d{4}", "????", s))         # замена
print(re.split(r"[,—]\s*", s))             # разбиение по шаблону
Функция Ищет Возвращает
search первое совпадение в любом месте Match / None
match совпадение с начала строки Match / None
fullmatch совпадение всей строки Match / None
findall все совпадения список строк (или кортежей групп)
finditer все совпадения итератор Match
sub / subn и заменяет новая строка (и число замен)
split разделители список частей

Синтаксис

Символы и классы

Шаблон Значение
. любой символ, кроме перевода строки
\d / \D цифра / не цифра
\w / \W буква, цифра или _ (включая кириллицу) / обратное
\s / \S пробельный символ / не пробельный
[abc] один из символов
[a-zа-яё] диапазон
[^0-9] любой, кроме перечисленных
\. \( \+ сам символ (экранирование)

Квантификаторы

Шаблон Сколько раз
* 0 и более
+ 1 и более
? 0 или 1
{3} ровно 3
{2,4} от 2 до 4
{2,} от 2

Якоря

Шаблон Позиция
^ / $ начало / конец строки (с re.M — каждой строки)
\b граница слова
\A / \Z начало / конец всего текста
import re

print(re.findall(r"\bкот\b", "кот, котёнок, скот и кот"))   # только отдельное слово
print(re.findall(r"^\w+", "раз два\nтри четыре", flags=re.M))
print(bool(re.fullmatch(r"[A-Z]{2}\d{4}", "AB1234")), bool(re.fullmatch(r"[A-Z]{2}\d{4}", "AB12345")))

Жадность

import re

html = "<b>жирный</b> и <i>курсив</i>"
print(re.findall(r"<.+>", html))      # жадный: от первого < до последнего >
print(re.findall(r"<.+?>", html))     # ленивый: как можно меньше
print(re.findall(r"<[^>]+>", html))   # класс-отрицание — часто лучше ленивого

Группы

import re

m = re.search(r"(\d{2})\.(\d{2})\.(\d{4})", "Дата: 25.10.2026")
print(m.group(0), m.group(1), m.groups())

m = re.search(r"(?P<day>\d{2})\.(?P<month>\d{2})\.(?P<year>\d{4})", "Дата: 25.10.2026")
print(m["year"], m.groupdict())

print(re.findall(r"(\w+)=(\d+)", "a=1, b=22, c=333"))           # с группами findall возвращает кортежи
print(re.findall(r"(?:ab)+", "ababab abab"))                     # (?:...) — группа без захвата
print(re.sub(r"(\d{2})\.(\d{2})\.(\d{4})", r"\3-\2-\1", "25.10.2026 и 01.01.2027"))  # обратные ссылки
print(re.findall(r"\b(\w+)\s+\1\b", "это это опечатка и и ещё"))  # повтор слова: \1 внутри шаблона

Альтернатива

import re
print(re.findall(r"\b(?:кот|пёс|хомяк)\w*", "коты, псы, пёсик, хомяки"))
print(re.fullmatch(r"(?:jpg|jpeg|png)", "jpeg") is not None)

Проверки (lookaround)

Проверяют, что рядом есть (или нет) шаблон, не включая его в совпадение:

import re

prices = "Кофе 250₽, чай 120₽, вес 300г"
print(re.findall(r"\d+(?=₽)", prices))         # (?=...) — дальше идёт ₽
print(re.findall(r"\d+(?!\d|₽)", prices))      # (?!...) — дальше НЕ ₽
print(re.findall(r"(?<=\$)\d+", "цена $40, скидка 5"))   # (?<=...) — перед этим $

def strong(password):                           # проверка пароля опережающими проверками
    return bool(re.fullmatch(r"(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}", password))
print(strong("Secret123"), strong("secret123"))
print(re.sub(r"\B(?=(\d{3})+$)", " ", "1234567890"))   # разряды числа

Флаги и компиляция

import re

EMAIL = re.compile(r"""
    [\w.+-]+        # имя пользователя
    @
    [\w-]+          # домен
    (?:\.[\w-]+)+   # .ru, .co.uk
""", re.VERBOSE | re.IGNORECASE)

text = "Пишите на Anna.Smith+shop@Mail.RU или support@example.co.uk"
print(EMAIL.findall(text))
print(re.findall(r"привет", "Привет, ПРИВЕТ", flags=re.I))
print(re.findall(r"a.c", "a\nc abc", flags=re.S))       # DOTALL: . включает \n
Флаг Действие
re.I (IGNORECASE) без учёта регистра
re.M (MULTILINE) ^ и $ для каждой строки
re.S (DOTALL) . совпадает и с \n
re.X (VERBOSE) пробелы и комментарии в шаблоне
re.A (ASCII) \w, \d только ASCII

re.compile полезен, когда шаблон используется много раз: код чище, а шаблон компилируется один раз (хотя модуль re и так кеширует последние шаблоны).

Замена с функцией

import re

def mask_card(m):
    digits = m.group().replace(" ", "")
    return "**** **** **** " + digits[-4:]

print(re.sub(r"\b(?:\d{4} ?){3}\d{4}\b", mask_card, "Карта 4276 1234 5678 9012 оплачена"))

prices = "чай 100, кофе 250"
print(re.sub(r"\d+", lambda m: str(int(m.group()) * 2), prices))

Когда не нужны регулярки

Задача Вместо regex
строка начинается с ... s.startswith(...)
разбить по одному символу s.split(",")
HTML / XML парсер: BeautifulSoup, lxml
JSON json.loads
URL urllib.parse.urlparse
email (строго) отправить письмо с подтверждением; email-validator
даты datetime.strptime

Катастрофический возврат

Шаблоны вроде (a+)+$ на строке "aaaaaaaaaaaaaaaaaaaaaaaaaaaa!" работают экспоненциальное время — так «кладут» сервисы (ReDoS). Избегайте вложенных квантификаторов над одним и тем же, ограничивайте длину входа.

Вопрос на собеседовании

«Чем re.match отличается от re.search?» — match ищет совпадение только в начале строки, search — в любом месте. Для проверки всей строки — fullmatch. Ещё спрашивают про жадные (.*) и ленивые (.*?) квантификаторы.

Итог

  • Шаблоны — в raw-строках r"..."; search/match/fullmatch/findall/finditer/sub/split.
  • Классы \d \w \s [..], квантификаторы * + ? {m,n}, якоря ^ $ \b.
  • Квантификаторы жадные; ? после них делает ленивыми.
  • Группы (...), именованные (?P<name>...), без захвата (?:...), проверки (?=...), (?<=...).
  • Для HTML, JSON, URL — парсеры, а не регулярки.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Напишите extract_phones(text): найти все российские номера телефонов вида +7 (912) 345-67-89 или 8 912 345 67 89 или +79123456789 и вернуть их в едином формате +79123456789 (список в порядке появления). Подсказка: найдите шаблоном 11 цифр с необязательными разделителями, затем оставьте только цифры.

Какие тесты проверяют код (3)
assert extract_phones("Звоните: +7 (912) 345-67-89 или 8 912 345 67 89!") == ["+79123456789", "+79123456789"]
assert extract_phones("тел +79001112233, факс нет") == ["+79001112233"]
assert extract_phones("номер 12345") == []
Задание 2 Программа на Python

Разберите строку лога веб-сервера: parse_log(line) возвращает словарь с ключами ip, method, path, status (int) для строк вида 192.168.1.10 - - [25/Oct/2026:14:05:00 +0300] "GET /api/users?id=5 HTTP/1.1" 200 512. Используйте именованные группы (?P<name>...). Если строка не подходит — верните None.

Какие тесты проверяют код (3)
assert parse_log('192.168.1.10 - - [25/Oct/2026:14:05:00 +0300] "GET /api/users?id=5 HTTP/1.1" 200 512') == {"ip": "192.168.1.10", "method": "GET", "path": "/api/users?id=5", "status": 200}
assert parse_log('10.0.0.1 - bob [01/Jan/2026:00:00:00 +0000] "POST /login HTTP/2" 401 0')["status"] == 401
assert parse_log("мусор") is None
Задание 3 Программа на Python

Напишите snake_case(name): перевести имя из CamelCase в snake_case с помощью re.sub: "HTTPServerError" → "http_server_error", "userId" → "user_id", "already_snake" не меняется.

Какие тесты проверяют код (3)
assert snake_case("HTTPServerError") == "http_server_error"
assert snake_case("userId") == "user_id" and snake_case("already_snake") == "already_snake"
assert snake_case("getHTTPResponseCode2") == "get_http_response_code2"
Задание 4 Заполнить таблицу

Совпадёт ли шаблон с целой строкой (re.fullmatch)?

данет
\d{3}-\d{2} и 123-45
[a-z]+ и Hello
colou?r и color
(ab)+ и ababa
\w+@\w+\.ru и anna@mail.ru
^.*$ и пустая строка
Отметьте ячейку — «+», пустая — «−».
Задание 5 Один вариант ответа

Что вернёт re.findall(r"<.+>", "<b>жирный</b>")?

Войдите, чтобы отмечать пройденные уроки.