Python · Синтаксис · Промежуточный

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

10 задач

Поиск по шаблону с помощью модуля re. Охватывает search, match, findall, группы и распространённые паттерны.

Модуль re: search, match, fullmatch, findall, sub

#
Регулярные выражения — это мини-язык для описания текстовых шаблонов. Модуль `re` компилирует эти шаблоны и применяет их к строкам. Четыре самые используемые функции отличаются тем, *где* они ищут: ```python import re text = "User: alice, age 30" re.search(r"\d+", text) # первое совпадение ГДЕ УГОДНО в строке -> Match "30" re.match(r"\d+", text) # совпадение только В НАЧАЛЕ -> None (начинается с "U") re.fullmatch(r"\d+", "42") # вся строка должна совпасть -> Match "42" re.findall(r"\d+", text) # все непересекающиеся совпадения -> ["30"] re.sub(r"\d+", "??", text) # заменить все совпадения -> "User: alice, age ??" ``` **Зачем raw-строки?** Обратный слеш в строке Python имеет особое значение: `\n` = перевод строки, `\t` = табуляция. Но regex тоже активно использует обратные слеши (`\d`, `\w`, `\s`). Без raw-строк пришлось бы удваивать каждый слеш: ```python re.search("\\d+", text) # работает, но нечитаемо re.search(r"\d+", text) # то же самое - всегда используй raw-строки ``` Правило: каждый паттерн regex должен быть raw-строкой `r"..."`. **Читаем паттерн посимвольно** Возьмём `r"(\w+)@(\w+\.\w+)"` (простой паттерн email): ``` ( открыть группу 1 \w+ один или более символов слова [a-zA-Z0-9_] ) закрыть группу 1 @ литеральный "@" ( открыть группу 2 \w+ один или более символов слова \. литеральная точка (экранированная - обычная . означает "любой символ") \w+ один или более символов слова ) закрыть группу 2 ``` ```python m = re.search(r"(\w+)@(\w+\.\w+)", "Отправить на [email protected] пожалуйста") m.group(0) # "[email protected]" - полное совпадение m.group(1) # "bob" - группа 1 m.group(2) # "example.com" - группа 2 ``` **Методы объекта Match** ```python m = re.search(r"\d{4}", "Год: 2024 или 2025") if m: m.group() # "2024" - найденный текст m.start() # 6 - начальный индекс m.end() # 10 - конечный индекс m.span() # (6, 10) - оба сразу ``` Всегда проверяй `if m:` перед вызовом методов - `search` возвращает `None`, если совпадений нет. **re.sub с функцией** Замена может быть функцией, получающей каждый объект совпадения: ```python def double(m): return str(int(m.group()) * 2) re.sub(r"\d+", double, "a=3, b=7") # "a=6, b=14" ``` Это мощнее статической замены строки.

Группы, не-захватывающие группы, флаги, re.compile, finditer

#
**Захватывающие группы** позволяют извлекать части совпадения. Оберни скобками то, что хочешь захватить. ```python import re log = "2024-03-15 ERROR: disk full" m = re.search(r"(\d{4}-\d{2}-\d{2}) (\w+): (.+)", log) m.group(1) # "2024-03-15" m.group(2) # "ERROR" m.group(3) # "disk full" ``` **Именованные группы** делают код самодокументированным: `(?P<name>...)` определяет группу, `m.group('name')` её получает. ```python m = re.search(r"(?P<date>\d{4}-\d{2}-\d{2}) (?P<level>\w+): (?P<msg>.+)", log) m.group("date") # "2024-03-15" m.group("level") # "ERROR" m.group("msg") # "disk full" ``` **Не-захватывающие группы** `(?:...)` группируют без создания нумерованного захвата - удобны для чередования или квантификаторов, когда не нужно извлекать: ```python # Найти 'colour' или 'color', не захватывая необязательную 'u' re.findall(r"colou?r", "colour and color") # ['colour', 'color'] # Не-захватывающая группа для чередования re.findall(r"(?:cat|dog)s?", "cats and dogs") # ['cats', 'dogs'] ``` **Флаги** меняют поведение движка: ```python re.search(r"hello", "Hello World", re.IGNORECASE) # без учёта регистра re.findall(r"^\w+", text, re.MULTILINE) # ^ совпадает с началом каждой строки re.search(r"begin.+end", text, re.DOTALL) # . совпадает и с символами новой строки re.search(r"\d+", text, re.IGNORECASE | re.MULTILINE) # комбинация через | ``` Встроенный флаг в паттерн: `(?i)hello` означает без учёта регистра (то же что `re.IGNORECASE`). **re.compile** - предварительная компиляция, если паттерн используется много раз: ```python phone_pat = re.compile(r"\+?\d[\d\s\-]{7,14}\d") for line in big_list: m = phone_pat.search(line) # быстрее, чем re.search(pattern, line) каждый раз ``` **re.finditer vs re.findall** `findall` возвращает список строк. `finditer` возвращает итератор объектов Match - более экономный по памяти для больших текстов, и даёт информацию о позиции: ```python for m in re.finditer(r"\d+", "a1 b22 c333"): print(m.group(), m.start()) # "1" 1 / "22" 4 / "333" 7 ``` **re.split** делит по паттерну (не только по фиксированному разделителю): ```python re.split(r"[,;\s]+", "one, two; three") # ['one', 'two', 'three'] # Захватывающая группа в паттерне -> разделитель включается в результат re.split(r"(,)", "a,b,c") # ['a', ',', 'b', ',', 'c'] ```

Жадные vs ленивые квантификаторы, re.VERBOSE и справочник синтаксиса

#
**Жадные vs ленивые квантификаторы** По умолчанию квантификаторы (`*`, `+`, `{n,m}`) являются *жадными* - они захватывают как можно больше. Добавь `?`, чтобы сделать их *ленивыми* - захватывать как можно меньше. ```python import re html = "<b>bold</b> and <i>italic</i>" re.findall(r"<.+>", html) # жадный -> ['<b>bold</b> and <i>italic</i>'] re.findall(r"<.+?>", html) # ленивый -> ['<b>', '</b>', '<i>', '</i>'] ``` Распространённая ошибка: использование `.+` внутри угловых скобок и захват слишком большого количества. Всегда спрашивай: должно ли это остановиться на первой возможной позиции? **re.VERBOSE - читаемые сложные паттерны** Режим verbose игнорирует пробелы и комментарии `#` внутри паттерна: ```python date_pat = re.compile(r''' (?P<year> \d{4} ) # 4-значный год - (?P<month> \d{2} ) # 2-значный месяц - (?P<day> \d{2} ) # 2-значный день ''', re.VERBOSE) m = date_pat.search("Дедлайн: 2024-12-31") m.group("year") # "2024" m.group("month") # "12" m.group("day") # "31" ``` Используй VERBOSE для любого паттерна длиннее ~40 символов. **Справочник синтаксиса паттернов** ``` . любой символ кроме новой строки (re.DOTALL включает новую строку) ^ начало строки (или строки в тексте с re.MULTILINE) $ конец строки \d цифра [0-9] \D не-цифра \w символ слова [a-zA-Z0-9_] \W не-символ слова \s пробельный символ (пробел, табуляция, новая строка) \S не-пробельный символ * 0 или более (жадный) *? ленивый + 1 или более (жадный) +? ленивый ? 0 или 1 {n} ровно n {n,m} от n до m (жадный) {n,m}? ленивый [abc] класс символов: a, b или c [^abc] отрицательный класс: всё кроме a, b, c [a-z] диапазон (...) захватывающая группа (?:...) не-захватывающая группа (?P<name>...) именованная захватывающая группа | чередование: a|b совпадает с a или b \ экранирует следующий символ ``` **Типичные ловушки** - `.` совпадает с любым символом - используй `\.` для литеральной точки - `^` и `$` ведут себя иначе с `re.MULTILINE` - проверяй явно - `re.match` проверяет только начало; `re.fullmatch` проверяет всю строку - не путай их при валидации - Откат в сложных паттернах может быть медленным - тестируй на длинных строках, если важна производительность

Кулинарная книга распространённых regex-паттернов

#
Готовая коллекция паттернов для наиболее распространённых задач поиска. **Email-адрес (простой)** ```python import re EMAIL = re.compile(r"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}") EMAIL.findall("Контакты: [email protected] или [email protected]") # ['[email protected]', '[email protected]'] ``` **Номера телефонов (международный формат)** ```python PHONE = re.compile(r"\+?\d[\d\s\-().]{6,14}\d") PHONE.findall("Звоните +1 (800) 555-1234 или +44 20 7946 0958") # ['+1 (800) 555-1234', '+44 20 7946 0958'] ``` **ISO дата (YYYY-MM-DD)** ```python ISO_DATE = re.compile(r"\b(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\b") ISO_DATE.findall("Мероприятие 2024-03-15 и дедлайн 2024-12-31") # [('2024', '03', '15'), ('2024', '12', '31')] ``` **URL (базовый)** ```python URL = re.compile(r"https?://[^\s<>'"]+") URL.findall("Смотри https://docs.python.org и http://example.com/path?q=1") # ['https://docs.python.org', 'http://example.com/path?q=1'] ``` **Извлечь числа (целое или дробное)** ```python NUMBER = re.compile(r"-?\d+(?:\.\d+)?") NUMBER.findall("Темп: -3.5 C, высота: 1200 м") # ['-3.5', '1200'] ``` **Camel в snake_case** ```python def camel_to_snake(name): s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name) return re.sub(r"([a-z\d])([A-Z])", r"\1_\2", s).lower() camel_to_snake("HTTPSResponse") # "https_response" camel_to_snake("myVariableName") # "my_variable_name" ``` **Когда НЕ использовать regex** - Парсинг HTML/XML - используй `html.parser`, `lxml` или `BeautifulSoup` - Валидация email для продакшна - используй библиотеку вроде `email-validator` - Парсинг JSON/CSV - используй модули `json`, `csv` - Сложная вложенная структура - regex не может обрабатывать рекурсивную грамматику Regex идеально подходит для сканирования, извлечения и замены текстовых паттернов. Это неправильный инструмент, когда формат имеет иерархию или вложенность.
01

#

Используйте `re.search`, чтобы проверить, содержит ли строка последовательность из 3 или более цифр подряд. Верните True или False.

import re

def has_three_digits(s):
    # ваш код здесь
    pass

print(has_three_digits('abc123def'))  # True
print(has_three_digits('abc12def'))   # False
print(has_three_digits('1234'))       # True
Решение
import re

def has_three_digits(s):
    return bool(re.search(r'\d{3,}', s))
02

#

Используйте `re.match`, чтобы проверить, является ли строка допустимым идентификатором Python (буквы, цифры, подчёркивания; должен начинаться с буквы или подчёркивания).

import re

def is_identifier(s):
    # ваш код здесь
    pass

print(is_identifier('my_var'))    # True
print(is_identifier('_private'))  # True
print(is_identifier('2bad'))      # False
print(is_identifier('hello world')) # False
Решение
import re

def is_identifier(s):
    return bool(re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', s))
03

#

Извлеките все адреса электронной почты из многострочной строки. Верните их как список.

import re

def find_emails(text):
    # ваш код здесь
    pass

text = '''
Contact us at [email protected] or [email protected].
You can also reach [email protected] for questions.
'''
print(find_emails(text))
# ['[email protected]', '[email protected]', '[email protected]']
Решение
import re

def find_emails(text):
    return re.findall(r'[\w.+-]+@[\w-]+\.[\w.]+', text)
04

#

Замените все вхождения нескольких пробелов одним пробелом в строке.

import re

def normalize(s):
    # ваш код здесь
    pass

print(normalize('hello   world'))   # 'hello world'
print(normalize('  a  b  c  '))     # ' a b c '
Решение
import re

def normalize(s):
    return re.sub(r' {2,}', ' ', s)
05

#

Используйте группы захвата, чтобы извлечь год, месяц и день из строки даты в формате 'DD/MM/YYYY'.

import re

def parse_date(s):
    # Вернуть (day, month, year) как целые числа или None при неверном формате
    pass

print(parse_date('15/03/2024'))  # (15, 3, 2024)
print(parse_date('01/12/1999'))  # (1, 12, 1999)
print(parse_date('invalid'))     # None
Решение
import re

def parse_date(s):
    m = re.match(r'^(\d{2})/(\d{2})/(\d{4})$', s)
    if not m:
        return None
    return int(m.group(1)), int(m.group(2)), int(m.group(3))
06

#

Используйте `re.sub` с функцией для удвоения всех чисел, найденных в строке.

import re

def double_numbers(s):
    # ваш код здесь
    pass

print(double_numbers('I have 3 cats and 12 dogs'))  # 'I have 6 cats and 24 dogs'
print(double_numbers('price: 9.99'))                 # 'price: 19.98'
Решение
import re

def double_numbers(s):
    return re.sub(r'\d+\.?\d*', lambda m: str(float(m.group()) * 2), s)
07

#

Скомпилируйте шаблон regex один раз и используйте его для фильтрации списка строк, оставляя только те, что соответствуют шаблону номера телефона: `+X-XXX-XXX-XXXX` (где X — цифра).

import re

def filter_phones(strings):
    # ваш код здесь
    pass

items = ['+1-800-555-1234', 'not a phone', '+44-020-555-9876', '123-456']
print(filter_phones(items))  # ['+1-800-555-1234', '+44-020-555-9876']
Решение
import re

def filter_phones(strings):
    pattern = re.compile(r'^\+\d+-\d{3}-\d{3}-\d{4}$')
    return [s for s in strings if pattern.match(s)]
08

#

Используйте именованные группы для разбора строки лога в формате `[LEVEL] YYYY-MM-DD: message`. Верните словарь с ключами 'level', 'date' и 'message'.

import re

def parse_log(line):
    # ваш код здесь
    pass

print(parse_log('[ERROR] 2024-03-15: Connection refused'))
# {'level': 'ERROR', 'date': '2024-03-15', 'message': 'Connection refused'}
print(parse_log('[INFO] 2024-01-01: Server started'))
# {'level': 'INFO', 'date': '2024-01-01', 'message': 'Server started'}
Решение
import re

def parse_log(line):
    m = re.match(r'^\[(?P<level>\w+)\] (?P<date>\d{4}-\d{2}-\d{2}): (?P<message>.+)$', line)
    return m.groupdict() if m else None
09

#

Используйте `re.split` для разбивки строки на любой знак пунктуации (`.`, `,`, `!`, `?`, `;`, `:`). Отфильтруйте пустые строки из результата.

import re

def split_on_punctuation(s):
    # ваш код здесь
    pass

print(split_on_punctuation('Hello, world! How are you?'))
# ['Hello', ' world', ' How are you']
print(split_on_punctuation('one.two;three:four'))
# ['one', 'two', 'three', 'four']
Решение
import re

def split_on_punctuation(s):
    return [part for part in re.split(r'[.,!?;:]', s) if part]
10

#

Используйте флаг `re.VERBOSE` (или `re.X`) для написания читаемого regex с комментариями, проверяющего надёжный пароль: минимум 8 символов, минимум одна заглавная, одна строчная, одна цифра, один специальный символ (`!@#$%^&*`).

import re

def is_strong_password(s):
    # ваш код здесь
    pass

print(is_strong_password('Abc@1234'))    # True
print(is_strong_password('abcdefgh'))    # False (нет заглавной/цифры/спец.)
print(is_strong_password('Abc@12'))      # False (слишком короткий)
Решение
import re

def is_strong_password(s):
    pattern = re.compile(r'''
        ^                    # начало строки
        (?=.*[A-Z])          # минимум одна заглавная
        (?=.*[a-z])          # минимум одна строчная
        (?=.*\d)             # минимум одна цифра
        (?=.*[!@#$%^&*])    # минимум один спецсимвол
        .{8,}               # минимум 8 символов всего
        $                   # конец строки
    ''', re.VERBOSE)
    return bool(pattern.match(s))