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

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

И почему их не нужно использовать слишком часто

Язык программирования Python — один из самых популярных, потому что удобный и быстрый. В нём много интересных и полезных встроенных инструментов: одни из них применяются почти в каждом проекте, а другие — только при крайней необходимости.

Сегодня расскажем про один из этих инструментов — регулярные выражения в Python. Эта технология может сделать жизнь разработчика проще или, наоборот, усложнить. А результат уже зависит от мастерства программиста и ситуации.

Что такое регулярные выражения и зачем они нужны

«Регулярный» — не совсем точное слово в русском языке для описания работы этой технологии. Регулярные выражения в Python (regular expressions, regex или просто регулярки) — шаблоны для поиска нужных фрагментов текста.

Работает это так: программист составляет шаблон и отдаёт компьютеру. А машина смотрит в него и понимает, что нужно искать.

Вот пример: регулярное выражение "([А-ЯЁа-яё]+)" находит все слова в кавычках. Попробуем вставить это выражение в предварительно подготовленный текст и посмотрим, что найдётся:

В тексте подсветились все названия из одного слова. Если бы нам понадобилось найти и остальные названия, например с пробелами, регулярное выражение было бы немного другим.

Это мощный инструмент, но разобраться в регулярных выражениях может быть сложно. Даже для такой простой задачи, как названия из одного слова в кавычках, понадобилось учесть много деталей, смотрите сами:

  • " находит открывающую кавычку.
  • Круглые скобки () обозначают группу символов.
  • [А-ЯЁа-яё] внутри круглых скобок находит диапазон русских букв. Буквы в большом и малом регистре нужно искать отдельно, букву Ё — тоже.
  • + означает, что символов может быть больше одного.
  • " находит закрывающую кавычку.

👉 В таких выражениях легко запутаться, они трудночитаемы и могут сильно меняться в зависимости от задачи. Поэтому их применяют только когда они действительно могут принести пользу.

Модуль re в Python

Для работы с регулярными выражениями в Python используется библиотека re.

Импортирование модуля. Этот модуль встроенный, для работы его достаточно импортировать в проект. В файле скрипта пишем такую команду:

import re

Основные функции модуля работают так: нужно написать ключевое слово re и добавить нужный метод через точку. 

Само регулярное выражение состоит из нескольких частей:

  • В начале стоит буква r. Она нужна, чтобы Python не обрабатывал знаки \. Благодаря этому можно использовать управляющие символы, например \d, иначе слэш в строке не прочитается.
  • После r идёт строка — набор символов, из которых состоит регулярное выражение.
  • После регулярного выражения ставят переменную с текстом или сам текст в кавычках.
  • Иногда после текста ставят ещё один параметр для более тонкой настройки — переменную-флаг.

Разберём каждый из методов на примерах.

Re.match() ищет совпадение в начале строки. Если совпадение найдено, возвращает объект типа Match, если не найдено — None. Про Match-объекты объясним ниже.

Так это выглядит в коде:

# импортируем модуль
import re

# создаём строку
string = "Привет, мир!"
# пишем регулярное выражение, ищем совпадение в начале строки
match = re.match(r'Привет', string)
# выводим на экран результат
print(match.group() if match else "Совпадений нет")

Запускаем код и видим, что регулярное выражение нашло подходящий фрагмент:

Привет

Как работает:

  • re.match() проверяет только начало строки.
  • Если строка начинается с «Привет», group() возвращает совпадение.
  • Если бы слово «Привет» было не в начале, re.match() ничего бы не нашёл.

Re.search() работает похоже, но ищет первое совпадение в любой части строки. Возвращает объект Match или None.

В примере ниже мы ищем часть строки «мир», при этом она находится в конце текста:

# импортируем модуль
import re

# создаём строку
use = "Привет, мир!"
# пишем регулярное выражение, ищем первое совпадение в строке
match = re.search(r'мир', use)
# выводим на экран результат
print(match.group() if match else "Совпадений нет")

Результат в консоли:

мир

Почему так получилось: код работает точно так же, как re.match, только ищет совпадение по всему тексту.

Re.findall() возвращает все совпадения в виде списка. 

Например, нам нужно извлечь из текста все числа и сохранить их в список. Понадобится такой код:

# импортируем модуль
import re

# создаём строку
used = "Цена: 120 рублей, скидка 30 рублей"
# пишем регулярное выражение, получаем все совпадения в виде списка
numbers = re.findall(r'\d+', used)
# выводим на экран результат
print(numbers)

В консоли после выполнения получаем список:

['120', '30']

Что означает синтаксис:

  • \d+ означает «одна или несколько цифр»: означает \d цифру, + —  что может быть больше одного совпадения. 
  • findall() ищет все такие совпадения и возвращает список.

Re.split() разделяет строку по заданному шаблону. Такой же встроенный метод-функция есть у строк: нужно задать символ, и когда машина встречает его в тексте, то создаёт разрыв.

Для примера мы разобьём строку сразу по нескольким символам: запятой, точке с запятой и пробелу. Если Python встретит любой из этих символов в тексте, он возьмёт всё, что было до split-символа, и положит в список как новый элемент. А потом продолжит, пока не дойдёт до конца строки.

# импортируем модуль
import re

# создаём строку
text = "яблоко, банан; груша апельсин"
# пишем регулярное выражение, разделяем строку по шаблону
words = re.split(r'[,; ]+', text)
# выводим на экран результат
print(words)

Запускаем и получаем список:

['яблоко', 'банан', 'груша', 'апельсин']

Как мы это сделали:

  • [,; ]+ означает «запятая, точка с запятой или пробел». Символов может быть несколько или один.
  • split() разрезает строку по этим разделителям.

Re.sub() заменяет найденные совпадения с регулярным выражением на указанный текст.

Это работает как замена выбранного фрагмента текста в гугл-документах или вордовских файлах:

  • Вводим то, что надо заменить.
  • Прописываем то, на что нужно заменить.

В коде реализовывается так:

# импортируем модуль
import re

# создаём строку
text = "Цвет: красный, цвет: синий"
# пишем регулярное выражение, заменяем совпадения на другой текст
new_text = re.sub(r'цвет', 'ИМЯ', text, flags=re.IGNORECASE)
# выводим на экран результат
print(new_text)

В консоли получаем:

ИМЯ: красный, ИМЯ: синий

Почему это работает:

  • Re.sub() ищет в строке text все слова «цвет» и заменяет их на «ИМЯ».
  • Среди аргументов есть дополнительный флаг re.IGNORECASE. Он делает поиск регистронезависимым (неважно, большие буквы или маленькие), поэтому регулярное выражение находит и «цвет», и «Цвет».

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

Для примера возьмём текст, с которым мы уже работали:

text = "Цена: 120 рублей, скидка 30 рублей"

В прошлый раз мы применили регулярное выражение напрямую к этому тексту, когда использовали метод re.findall. В результате получался один список, в который сохранялись все совпадения.

В этот раз мы сначала сохраним ту же самую регулярку в переменную, а потом подключим её через цикл:

# импортируем модуль
import re

# создаём регулярное выражение и компилируем шаблон
pattern = re.compile(r'\d+')
# создаём строку
texts = ["Цена: 120 рублей", "Скидка: 30%"]
# применяем текст к шаблону: находим числа и выводим их на экран
for text in texts:
   print(pattern.findall(text))

Теперь после запуска каждое вхождение будет являться как отдельный список:

['120']

['30']

Работа с Match-объектами. Если re.match() или re.search() находят совпадение, они возвращают объект Match, который содержит информацию о найденном тексте.

Некоторые методы Match-объектов:

  • Group() возвращает найденное совпадение с шаблоном-регуляркой.
  • Start() и end() находят, где в строке начинается и заканчивается совпадение.
  • Span() тоже возвращает начало и конец совпадения, но помещает их в кортеж.

Пробуем на практике:

# импортируем модуль
import re

# получаем объект Match
match = re.search(r'\d+', 'Цена: 120 рублей')
# получаем совпадение
print('match.group, найденное совпадение:', match.group())
# позиции совпадения
print('match.start, где в строке начинается и заканчивается совпадение: ', match.start(), match.end())
# начало и конец совпадения
print('match.span, диапазон индексов совпадения', match.span())

Запускаем код и смотрим на результаты:

match.group, найденное совпадение: 120
match.start, где в строке начинается и заканчивается совпадение: 6 9
match.span, диапазон индексов совпадения (6, 9)

Синтаксис регулярных выражений

Регулярные выражения могут выглядеть сложно, потому что используют много разных элементов в синтаксисе. Вот основные из них.

Метасимволы — специальные символы, которые задают поведение при поиске.

МетасимволОписание и пример
.Находит любой одиночный символ, кроме символа новой строки \n.

a.c → находит abc, a1c, но не ac.
^Ищет совпадение в начале строки.

^Привет → находит "Привет, мир!", но не "Мир, привет!".
$Ищет совпадение в конце строки.

мир!$ → находит "Привет, мир!", но не "мир! Привет".
\dЛюбая цифра [0-9].

\d+ → в "Цена: 500$" находит 500.
В этом регулярном выражении есть символ +. Это символ-квантификатор, он означает, что искомый символ должен встречаться один и более раз.
\DЛюбой символ, кроме цифр.

\D+ → в "123abc456" находит "abc".
\wЛюбая буква, цифра или _ ([a-zA-Z0-9_]).

\w+ → в "Привет_123!" находит "Привет_123".
\WЛюбой символ, кроме \w.

\W+ → в "Привет, мир!" находит ", " (запятая и пробел).
\sЛюбой пробельный символ (пробел, \t, \n).

\s+ → в "Hello world" находит " " (три пробела).
\SЛюбой символ, кроме пробела.

\S+ → в "Hello patterns" находит "Hello" и "patterns".
\bГраница слова.

\bcat\b → находит cat, но не cats, потому что cat окружён пробелами.
\BНЕ граница слова.

\Bcat\B → находит scatter, но не cat, потому что cat окружён пробелами, а \B ищет только неограниченные символами вхождения.

Квантификаторы указывают, сколько раз должен повторяться символ или группа символов.

КвантификаторОписание и пример
*Ноль или более раз.

ba* → находит b, ba, baa, baaa и так далее.
+Один или более раз.

ba+ → находит ba, baa, baaa, но не b.
?Ноль или один раз.

ba? → находит b или ba, но не baa.
{n}Ровно n раз.

a{3} → находит aaa, но не aa или aaaa.
{n,}Не менее n раз.

a{2,} → находит aa, aaa, aaaa и т. д.
{n,m}От n до m раз.

a{2,4} → находит aa, aaa или aaaa, но не a или aaaaa.

Группировка и скобочные группы работают так: скобки используются для объединения частей выражения в одну группу.

СимволОписание и пример
()Группировка символов.

(ab)+ → находит ab, abab, ababab и любое другое количество повторяющихся символов ab.
(?:...)Негруппирующие скобки, которые не запоминают совпадения.

(?:ab)+ → работает так же, как (ab)+, но без сохранения ab в памяти.
(?P<name>...)Именованная группа.

(?P<module>\w+) → позволяет обращаться к совпадению по имени "module".

Жадные и ленивые квантификаторы. Жадные квантификаторы захватывают максимально возможное количество символов, а ленивые — минимальное.

КвантификаторОписание и пример
*Жадный: a.*b → в "axbxb" находит "axbxb".
*?Ленивый: a.*?b → в "axbxb" находит "axb".
+Жадный: a.+b → в "axbxb" находит "axbxb".
+?Ленивый: a.+?b → в "axbxb" находит "axb".
{n,m}Жадный: a{2,4} → в "aaaaa" находит "aaaa".
{n,m}?Ленивый: a{2,4}? → в "aaaaa" находит "aa".

Ленивым квантификатор делает знак ?.

  • Жадные *, +, {} → захватывают максимум возможного.
  • Ленивые *?, +?, {m,n}? → захватывают минимум возможного.

Примеры использования регулярных выражений

Вот несколько примеров, как можно применять регулярные выражения для типичных задач.

Поиск и замена

Регулярные выражения умеют находить в тексте заданные шаблоны и заменять их на другие. Это полезно для исправления опечаток и замены слов.

Заменяем слово в тексте:

# импортируем модуль
import re

# объявляем строку
text = "Жизнь не сложна, когда тебе нечего терять."
# заменяем слово "сложна" на "проста"
new_text = re.sub(r'сложна', 'проста', text)
# выводим на экран результат
print(new_text)

Проверяем:

Жизнь не проста, когда тебе нечего терять.

Извлечение данных

Можно извлекать из текста даты, числа или слова, соответствующие определённому шаблону.

Например, нужно найти в тексте год:

# импортируем модуль
import re

# объявляем строку
text = "В 1926 году вышел роман 'И восходит солнце'."
# извлекаем год из текста
year = re.search(r'\b\d{4}\b', text)
# выводим на экран результат
if year:
   print(year.group())

Запускаем код, получаем год:

1926

Проверка формата данных

Строки можно проверять на соответствие определённому формату. Например, это можно использовать для валидации адресов электронной почты или телефонных номеров.

Для примера проверим, начинается ли предложение с заглавной буквы и заканчивается ли точкой. В реальной жизни предложение может начинаться и заканчиваться по-другому, но мы для примера сделаем так.

# импортируем модуль
import re

# объявляем строку
text = "Моя жизнь имеет тенденцию распадаться на части, когда я просыпаюсь, знаете ли."
# проверяем, начинается ли предложение с заглавной буквы и заканчивается ли точкой
if re.match(r'^[А-Я].*\.$', text):
   print("Предложение имеет правильный формат.")
else:
   print("Предложение имеет неправильный формат.")

В консоли выводится результат проверки:

Предложение имеет правильный формат.

Разделение строк

С регулярными выражениями можно разбивать строки на части по заданному разделителю. Например, по запятым:

# импортируем модуль
import re

# объявляем строку
text = ("Париж никогда не кончается, и воспоминания каждого человека, который жил в нём, отличаются от воспоминаний любого другого.")
# делим текст на предложения по запятым
sentences = re.split(r',\s*', text)
# выводим на экран результат
print(sentences)

После запуска получаем список из нескольких строк. Запятые не включены:

['Париж никогда не кончается', 'и воспоминания каждого человека', 'который жил в нём', 'отличаются от воспоминаний любого другого.']

Отладка и оптимизация регулярных выражений

Для упрощения работы с regex можно использовать флаги, а ещё регулярные выражения можно отлаживать и оптимизировать.

Флаги в регулярных выражениях позволяют изменять поведение поиска. Они упрощают написание шаблонов и делают их более гибкими. Вот основные флаги, которые часто используются:

  • re.IGNORECASE (или re.I) игнорирует регистр символов при поиске.
  • re.MULTILINE (или re.M) даёт возможность символам ^ и $ работать с началом и концом каждой строки, а не всего текста. Это позволяет найти большее количество вхождений.
  • re.DOTALL (или re.S) позволяет символу . соответствовать любому символу, включая перенос строки \n.
  • re.VERBOSE (или re.X) включает комментарии и пробелы регулярных выражений для улучшения читаемости.

Для применения флагов их нужно указать в скобках нужного метода после переменной, в которой хранится текст. Такой код будет искать слово «мир» независимо от регистра:

result = re.findall(r"мир", text, flags=re.IGNORECASE)

В регулярных выражениях может быть довольно сложно отлаживать и искать ошибки. Вот несколько советов, как упростить этот процесс.

  • Если выражение получилось слишком сложное, разбейте его на меньшие части и тестируйте каждую отдельно.
  • Регулярные выражения можно тестировать в онлайн-сервисах, например regex101.com.
  • Проверьте выражение не только на идеальных данных, но и на граничных случаях. Например, пустые строки, строки с лишними пробелами или неожиданными символами.
  • Добавляйте вывод промежуточных результатов в консоль.

А вот что можно применить для оптимизации и увеличения скорости работы:

  • Жадные квантификаторы могут замедлять работу, особенно на больших текстах. По возможности лучше использовать ленивые.
  • Lookahead ((?=...)) и lookbehind ((?<=...)) полезны, но тоже замедляют выполнение программы. Используйте их только при необходимости.

Практические задачи

Для закрепления материала попробуйте решить несколько задач. Сначала идут сами задачи, а после них — ответы с правильным кодом.

Каждая задача направлена на какой-то один способ использования регулярных выражений.

Замена имён

Нужно заменить одно слово на другое. Можно взять любой пример, мы для решения взяли такую цитату:

Он был стариком, который рыбачил один в лодке в Гольфстриме, и вот уже восемьдесят четыре дня он не поймал ни одной рыбы.

В решении мы заменим слово «стариком» на «Сантьяго».

# импортируем модуль
import re

# объявляем строку
text = ("Он был стариком, который рыбачил один "
       "в лодке в Гольфстриме, и вот уже восемьдесят "
       "четыре дня он не поймал ни одной рыбы.")

# заменяем слово "стариком" на "Сантьяго"
result = re.sub(r"стариком", "Сантьяго", text)

# выводим результат
print(result)

Поиск адресов

Задача похожа на поиск слов, но посложнее. Нужно научиться находить адрес в тексте. В нашей задаче мы будем искать ул. Ленина, 123, Москва, Россия 101000.

# импортируем модуль
import re

# объявляем строку
text = ("Гостиница, где мы остановились, располагалась "
       "по адресу ул. Ленина, 123, Москва, Россия 101000. "
       "Мы провели там несколько дней, наслаждаясь прогулками по городу.")

# создаём регулярное выражение для поиска адреса
pattern = r"ул\.\sЛенина,\s\d+,\sМосква,\sРоссия\s\d{6}"

# ищем совпадение
match = re.search(pattern, text)

# проверяем результат
if match:
   print("Найденный адрес:", match.group())
else:
   print("Адрес не найден.")

Извлечение дат

Дата может быть в разных форматах. Например, попробуйте извлечь дату отсюда:

Осень в Мадриде была прохладной, и 15.10.2023 я сидел у окна в старом кафе на Гран-Виа, наблюдая, как дождь смывает следы летнего зноя с выцветшей мостовой.

# импортируем модуль
import re

# объявляем строку
text = ("Осень в Мадриде была прохладной, и 15.10.2023 "
       "я сидел у окна в старом кафе на Гран-Виа, наблюдая, "
       "как дождь смывает следы летнего зноя с выцветшей мостовой.")

# ищем дату по шаблону
date = re.search(r"\d{2}\.\d{2}\.\d{4}", text)

# выводим найденную дату
print(date.group())

Проверка телефонных номеров

Напишите проверку валидности телефонного номера на соответствие формату +X-XXX-XXX-XXXX.

# импортируем модуль
import re

# объявляем строку с номером телефона
phone_number = "+7-912-345-6789"

# проверяем, соответствует ли номер шаблону
is_valid = re.match(r"^\+\d{1}-\d{3}-\d{3}-\d{4}$", phone_number)

# выводим результат проверки
print("Валиден" if is_valid else "Невалиден")

Обложка:

Алексей Сухов

Корректор:

Елена Грицун

Вёрстка:

Кирилл Климентьев

Соцсети:

Юлия Зубарева

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Вам может быть интересно
hard
[anycomment]
Exit mobile version