Что такое регулярные выражения
Регулярные выражения (regex, regexp) — это язык шаблонов для поиска и обработки текста. Вместо того чтобы писать сложный код с циклами и условиями, вы описываете шаблон, а движок регулярных выражений ищет совпадения за вас.
# Без regex: проверка, является ли строка номером телефона
def is_phone(s):
if len(s) != 12:
return False
if s[0] != '+':
return False
for ch in s[1:]:
if not ch.isdigit():
return False
return True
# С regex: один шаблон решает всё
import re
def is_phone(s):
return bool(re.fullmatch(r'\+\d{11}', s))
В Python регулярные выражения доступны через встроенный модуль re.
Сырые строки и regex
Перед строками с шаблонами всегда ставьте r (raw string), чтобы обратный слеш \ не интерпретировался как escape-символ:
pattern = r'\d+\.\d+' # Правильно: ищет число с плавающей точкой
pattern = '\\d+\\.\\d+' # Без r пришлось бы экранировать каждый слеш
Базовые метасимволы
Метасимволы — это специальные знаки, которые не совпадают сами с собой, а задают правило поиска:
| Символ | Значение | Пример |
|---|---|---|
. |
Любой символ, кроме \n |
к.т → «кот», «кит» |
^ |
Начало строки | ^Привет → строка начинается с «Привет» |
$ |
Конец строки | конец$ → строка заканчивается на «конец» |
* |
0 или более повторений | a* → '', 'a', 'aa', 'aaa'... |
+ |
1 или более повторений | a+ → 'a', 'aa', 'aaa'... |
? |
0 или 1 повторение | colou?r → 'color', 'colour' |
{n} |
Ровно n повторений | \d{3} → ровно 3 цифры |
{n,m} |
От n до m повторений | \d{2,4} → 2, 3 или 4 цифры |
{n,} |
n или более повторений | \d{2,} → 2 и более цифр |
[] |
Один символ из набора | [aeiou] → любая гласная |
[^] |
Один символ НЕ из набора | [^0-9] → не цифра |
| |
ИЛИ (альтернатива) | кот|пёс → 'кот' или 'пёс' |
() |
Группировка и захват | (ab)+ → 'ab', 'abab'... |
\ |
Экранирование метасимвола | \. → точка как символ |
Символьные классы (shorthand)
Это сокращённые обозначения для часто используемых наборов символов:
| Класс | Значение | Противоположность |
|---|---|---|
\d |
Цифра [0-9] |
\D — не цифра |
\w |
Буква, цифра или _ [a-zA-Z0-9_] |
\W — не буква/цифра |
\s |
Пробельный символ (пробел, таб, перенос) | \S — не пробельный |
\b |
Граница слова | \B — не граница слова |
\A |
Начало строки (аналог ^) |
\Z — конец строки (аналог $) |
import re
text = "Кот №42 съел 3 рыбки и рис."
print(re.findall(r'\d+', text)) # ['42', '3']
print(re.findall(r'\w+', text)) # ['Кот', '42', 'съел', '3', 'рыбки', 'рис']
print(re.findall(r'\b\w{3}\b', text)) # ['Кот', 'рис'] — слова из 3 букв
re.search() — первый найденный
Ищет первое совпадение шаблона в любом месте строки. Возвращает объект Match или None:
import re
text = "Мой номер: +7-999-123-45-67"
match = re.search(r'\+\d-\d{3}-\d{3}-\d{2}-\d{2}', text)
if match:
print(match.group()) # +7-999-123-45-67
print(match.start()) # 11 (индекс начала)
print(match.end()) # 27 (индекс конца)
print(match.span()) # (11, 27)
# Если не найдено — возвращает None
result = re.search(r'z{10}', "zzz")
print(result) # None
re.match() — только с начала строки
Проверяет совпадение строго с начала строки. Не ищет в середине:
text = "Python — отличный язык"
print(re.match(r'Python', text)) # Match — начинается с Python
print(re.match(r'язык', text)) # None — 'язык' не в начале
print(re.search(r'язык', text)) # Match — search найдёт в любом месте
Фактически re.match(pat, text) эквивалентен re.search(r'^' + pat, text).
re.fullmatch() — полное совпадение
Вся строка должна соответствовать шаблону от начала до конца:
print(re.fullmatch(r'\d+', "12345")) # Match
print(re.fullmatch(r'\d+', "123abc")) # None — есть буквы
print(re.fullmatch(r'\d+', "123 45")) # None — есть пробел
# fullmatch автоматически добавляет ^ в начале и $ в конце
# Это эквивалентно: re.search(r'^\d+$', "12345")
re.findall() — все совпадения
Возвращает список всех найденных совпадений:
text = "Цены: 100 руб, 250 руб, 50 руб."
prices = re.findall(r'\d+', text)
print(prices) # ['100', '250', '50']
# Если в шаблоне есть группы, findall возвращает список кортежей
text = "Иван: 25 лет, Анна: 30 лет"
pairs = re.findall(r'(\w+): (\d+)', text)
print(pairs) # [('Иван', '25'), ('Анна', '30')]
re.finditer() — итератор по совпадениям
Возвращает итератор объектов Match. Полезно, когда нужны позиции или когда данных много:
text = "Первый, второй, третий"
for m in re.finditer(r'\w+', text):
print(f"'{m.group()}' на позиции {m.start()}-{m.end()}")
# 'Первый' на позиции 0-6
# 'второй' на позиции 8-14
# 'третий' на позиции 16-22
re.sub() — замена по шаблону
Заменяет все совпадения на указанную строку. Четвёртый аргумент count ограничивает количество замен:
text = "Кот ловит мышь. Кот спит."
# Замена всех вхождений
print(re.sub(r'Кот', 'Пёс', text))
# Пёс ловит мышь. Пёс спит.
# Замена только первого вхождения
print(re.sub(r'Кот', 'Пёс', text, count=1))
# Пёс ловит мышь. Кот спит.
# Удаление всех цифр
print(re.sub(r'\d', '', "a1b2c3")) # abc
# Удаление лишних пробелов
print(re.sub(r'\s+', ' ', "Много лишних пробелов"))
# Много лишних пробелов
Ссылки на группы в замене — \1, \2 и т.д. позволяют использовать найденные группы:
# Меняем порядок слов
print(re.sub(r'(\w+) (\w+)', r'\2 \1', "Hello World"))
# World Hello
# Форматируем дату: ДД-ММ-ГГГГ → ГГГГ.ММ.ДД
date = "25-12-2023"
print(re.sub(r'(\d{2})-(\d{2})-(\d{4})', r'\3.\2.\1', date))
# 2023.12.25
# Маскируем середину номера телефона
phone = "+7-999-123-45-67"
print(re.sub(r'(\d{3}-\d{2})-\d{2}$', r'\1-XX', phone))
# +7-999-123-45-XX
Можно также передать функцию, которая будет вызвана для каждого совпадения:
def double(match):
return str(int(match.group()) * 2)
text = "Было 5 яблок и 3 груши"
print(re.sub(r'\d+', double, text))
# Было 10 яблок и 6 груш
re.split() — разделение по шаблону
Разбивает строку там, где шаблон совпадает:
# Разбить по любому количеству пробелов и запятых
text = "яблоко, груша,банан, апельсин"
print(re.split(r'[,\s]+', text))
# ['яблоко', 'груша', 'банан', 'апельсин']
# Обычный split со сложным разделителем
text = "один; два: три, четыре"
print(re.split(r'[;:,]\s*', text))
# ['один', 'два', 'три', 'четыре']
# maxsplit ограничивает количество разбиений
print(re.split(r'[;:,]\s*', text, maxsplit=2))
# ['один', 'два', 'три, четыре']
Группы и захват
Круглые скобки () создают группу захвата. Содержимое, совпавшее с группой, можно извлечь:
text = "Email: ivan@mail.ru, Контакт: anna@gmail.com"
match = re.search(r'(\w+)@(\w+\.\w+)', text)
if match:
print(match.group(0)) # ivan@mail.ru — всё совпадение
print(match.group(1)) # ivan — первая группа (имя)
print(match.group(2)) # mail.ru — вторая группа (домен)
print(match.groups()) # ('ivan', 'mail.ru') — все группы
# Именованные группы: (?P<name>...)
match = re.search(r'(?P<user>\w+)@(?P<domain>\w+\.\w+)', text)
if match:
print(match.group('user')) # ivan
print(match.group('domain')) # mail.ru
print(match.groupdict()) # {'user': 'ivan', 'domain': 'mail.ru'}
Незахватывающие группы (?:...)
Если скобки нужны только для группировки, но не для захвата — используйте (?:...):
# С захватом: findall вернёт только содержимое групп
print(re.findall(r'(кот|пёс) спит', "кот спит и пёс спит"))
# ['кот', 'пёс']
# Без захвата: findall вернёт полные совпадения
print(re.findall(r'(?:кот|пёс) спит', "кот спит и пёс спит"))
# ['кот спит', 'пёс спит']
Жадность и ленивость
По умолчанию квантификаторы *, +, ?, {} — жадные (захватывают максимум). Добавление ? после квантификатора делает его ленивым (захватывает минимум):
text = "<h1>Заголовок</h1><p>Текст</p>"
# Жадный захват — от первого < до последнего >
print(re.findall(r'<.*>', text))
# ['<h1>Заголовок</h1><p>Текст</p>']
# Ленивый захват — от первого < до ближайшего >
print(re.findall(r'<.*?>', text))
# ['<h1>', '</h1>', '<p>', '</p>']
# Извлечение содержимого тегов
print(re.findall(r'<.*?>(.*?)</.*?>', text))
# ['Заголовок', 'Текст']
re.compile() — компиляция шаблона
Если шаблон используется многократно, скомпилируйте его один раз:
import re
# Компилируем один раз
email_pattern = re.compile(r'[\w.-]+@[\w.-]+\.\w+')
emails = ["ivan@mail.ru", "bad@@mail", "anna@gmail.com", "no-at"]
for e in emails:
if email_pattern.fullmatch(e):
print(f"{e} — корректный email")
else:
print(f"{e} — НЕ email")
# У скомпилированного шаблона есть те же методы:
# pattern.search(), pattern.match(), pattern.findall(), pattern.sub(), ...
Флаги (модификаторы)
Флаги меняют поведение регулярных выражений. Передаются вторым аргументом в функции re или в компиляцию:
import re
text = """Python — лучший язык.
PYTHON используют миллионы.
python прост в изучении."""
# re.IGNORECASE (или re.I) — игнорировать регистр
print(re.findall(r'python', text, re.IGNORECASE))
# ['Python', 'PYTHON', 'python']
# re.MULTILINE (или re.M) — ^ и $ работают с началом/концом каждой строки
# По умолчанию ^ и $ привязаны к началу/концу всего текста
lines = "строка1\nстрока2\nстрока3"
print(re.findall(r'^строка\d', lines, re.MULTILINE))
# ['строка1', 'строка2', 'строка3']
# re.DOTALL (или re.S) — точка . совпадает и с переносом строки \n
html = "<div>\n <span>текст</span>\n</div>"
print(re.findall(r'<div>.*</div>', html)) # [] — . не проходит \n
print(re.findall(r'<div>.*</div>', html, re.DOTALL)) # ['<div>\n <span>текст</span>\n</div>']
# re.VERBOSE (или re.X) — позволяет писать шаблоны с комментариями и отступами
phone_pattern = re.compile(r"""
\+7 # код страны
[-\s]? # необязательный разделитель
\d{3} # код оператора
[-\s]? # необязательный разделитель
\d{3} # первая часть
[-\s]? # необязательный разделитель
\d{2} # вторая часть
[-\s]? # необязательный разделитель
\d{2} # третья часть
""", re.VERBOSE)
# Флаги можно комбинировать через |
pattern = re.compile(r'python', re.IGNORECASE | re.MULTILINE)
Lookahead и Lookbehind (опережающая и ретроспективная проверки)
Проверяют, что перед или после совпадения находится определённый шаблон, но не включают его в результат:
text = "100 руб, $50, 200 руб, $75"
# (?=...) — позитивный lookahead: после цифр идёт "руб"
print(re.findall(r'\d+(?=\s*руб)', text))
# ['100', '200']
# (?!...) — негативный lookahead: после цифр НЕ идёт "руб"
print(re.findall(r'\d+(?!\s*руб)', text))
# ['10', '0', '20', '0', '50', '75']
# (?<=...) — позитивный lookbehind: перед цифрами стоит "$"
print(re.findall(r'(?<=\$)\d+', text))
# ['50', '75']
# (?<!...) — негативный lookbehind: перед цифрами НЕ стоит "$"
# Полезно для исключения определённых совпадений
Практические примеры
Проверка email
import re
def is_valid_email(email):
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.fullmatch(pattern, email))
print(is_valid_email("ivan@mail.ru")) # True
print(is_valid_email("bad-email@")) # False
print(is_valid_email("name@sub.dom.org")) # True
Извлечение URL из текста
text = "Сайты: https://python.org и http://example.com/page"
urls = re.findall(r'https?://[^\s,;]+', text)
print(urls) # ['https://python.org', 'http://example.com/page']
Проверка надёжности пароля
def is_strong_password(password):
"""
Пароль должен быть >= 8 символов,
содержать хотя бы одну цифру, заглавную и строчную букву.
"""
checks = [
(r'.{8,}', 'Минимум 8 символов'),
(r'\d', 'Хотя бы одна цифра'),
(r'[A-ZА-ЯЁ]', 'Хотя бы одна заглавная буква'),
(r'[a-zа-яё]', 'Хотя бы одна строчная буква'),
]
for pattern, msg in checks:
if not re.search(pattern, password):
return False, msg
return True, "Пароль надёжный"
print(is_strong_password("qwerty")) # (False, 'Минимум 8 символов')
print(is_strong_password("Qwerty123")) # (True, 'Пароль надёжный')
Очистка текста от HTML-тегов
def strip_html(text):
return re.sub(r'<[^>]+>', '', text)
html = "<p>Это <b>важный</b> текст.</p>"
print(strip_html(html)) # Это важный текст.
Когда использовать regex, а когда — строковые методы
Регулярные выражения мощны, но не всегда нужны. Для простых операций строковые методы быстрее и читаемее:
text = " Hello World "
# Строковый метод (лучше для простого)
print(text.strip()) # Убрать пробелы по краям
# Regex (избыточен)
print(re.sub(r'^\s+|\s+$', '', text))
# Строковый метод
text = "раз,два,три"
parts = text.split(",")
# Regex (оправдан для сложного разделителя)
text = "раз; два, три: четыре"
parts = re.split(r'[;,:]\s*', text)
# Строковый метод
print("abc" in "abcdef") # Проверить вхождение подстроки
# Regex (оправдан для поиска по шаблону)
print(bool(re.search(r'\d{3}-\d{2}-\d{2}', "Мой СНИЛС: 123-45-67")))
Итоги
- Модуль
re— встроенный инструмент Python для работы с регулярными выражениями - Сырые строки
r'...'обязательны при написании шаблонов re.search()— первое совпадение в любом месте;re.match()— совпадение с начала;re.fullmatch()— полное совпадениеre.findall()— список всех совпадений;re.finditer()— итератор с позициямиre.sub()— замена по шаблону;re.split()— разделение по шаблону- Группы
(...)захватывают части совпадения;(?:...)— группировка без захвата - Флаги (
re.IGNORECASE,re.MULTILINE,re.DOTALL,re.VERBOSE) управляют поведением - Компилируйте шаблоны через
re.compile(), если используете их многократно - Не усложняйте: для простой проверки подстроки используйте
in, а не regex