Регулярные выражения
Зачем регулярные выражения
Регулярное выражение — шаблон для поиска текста. Помогает, когда простых 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
Напишите 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") == []
import re
PHONE = re.compile(r"(?:\+7|8)[\s(-]*\d{3}[\s)-]*\d{3}[\s-]*\d{2}[\s-]*\d{2}")
def extract_phones(text):
result = []
for match in PHONE.finditer(text):
digits = re.sub(r"\D", "", match.group())
result.append("+7" + digits[1:])
return result
Разберите строку лога веб-сервера: 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
import re
LOG = re.compile(
r'(?P<ip>\d{1,3}(?:\.\d{1,3}){3}) \S+ \S+ \[[^\]]+\] '
r'"(?P<method>[A-Z]+) (?P<path>\S+) [^"]*" (?P<status>\d{3})'
)
def parse_log(line):
m = LOG.match(line)
if not m:
return None
data = m.groupdict()
data["status"] = int(data["status"])
return data
Напишите 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"
import re
def snake_case(name):
name = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
name = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
return name.lower()
Совпадёт ли шаблон с целой строкой (re.fullmatch)?
| да | нет | |
|---|---|---|
\d{3}-\d{2} и 123-45 |
||
[a-z]+ и Hello |
||
colou?r и color |
||
(ab)+ и ababa |
||
\w+@\w+\.ru и anna@mail.ru |
||
^.*$ и пустая строка |
Что вернёт re.findall(r"<.+>", "<b>жирный</b>")?
Войдите, чтобы отмечать пройденные уроки.