Регулярные выражения
Источник
Материал взят с курса Skillbox "Профессия Python-разработчик".
Дополнительный материал для изучения.
Термины re, regex или RegExp
являются сокращением от английского Regular Expressions. Это своеобразный язык для написания шаблонов, которые используются в поиске подстроки(подстрок) в строке(тексте).
Зачем нужны такие сложности?
- Строки являются индексируемыми объектами и мы без проблем можем найти нужный нам символ или подстроку в строке, пройдясь по ней циклом.
- Можем проверить, что строка является email'ом при помощи функции split и разных сравнений. Можем заменить все "что то" на "что-то" и тд. Но это неудобно.
- С помощью синтаксиса регулярных выражений мы можем создать шаблон для поиска подстроки, даже если мы сами до конца не знаем какие именно символы нам нужно найти.
Пример №1 – В поисках Немо®
История:
Cреди прекрасных тропических морских стихий, в районе Большого барьерного рифа в уединении живет рыба-клоун по имени Марлин. Он растит своего единственного сыночка Немо.
Океан и существующие в нем опасности очень страшат Марлина, и он как может ограждает сына от них, но молодой Немо, страдающий излишним любопытством, очень хочет разузнать побольше о таинственном рифе, рядом с которым они живут.
...
Когда Немо по иронии судьбы оказывается вдалеке от дома, да и еще сталкивается с угрозой стать обедом рыбы-танка, Марлин отправляется на поиски сына.
Но Марлин, конечно же, понимает, что героический спасатель из него не получится и просит о помощи в поисках RegExp
.
deep_ocean = '''oCeAn Marlin OcEaN oceAN ocEAN oCEAN OCEAN OCEAn OCEan OCean Ocean ocean oCeAn OcEaN oceAN ocEAN
OCEAN OCEAn OCEan OCean Ocean ocean oCeAn OcEaN oceAN ocEAN oCEAN OCEAN OCEAn OCEan OCean Ocean
ocean oCeAn OcEaN nemaa ocEAN oCEAN OCEAN OCEAn OCEan OCean nemoO ocean oCeAn OcEaN oceAN ocEAN
oCEAN OCEAN OCEAn OCEan OCean Ocean ocean oNeMa OcEaN oceAN ocEAN oCEAN OCEAN OCEAn OCEan OCean
Ocean ocean oCeAn OcEaN oceAN nenemo oCEAN OCEAN OCEAn OCEan OCean Ocean Nemo ocean oCeAn OcEaN
oceAN ocEAN oCEAN OCEAN OCEAn OCEan OCean Ocean ocean oCeAn OcEaN oceAN ocEAN oCEAN OCEAN OCEAn
OCEan OCean Ocean ocean '''
Этапы решения
- Первым делом мы создаём шаблон для поиска. Сперва полезно сформировать его словами: Нам нужно найти все упоминания о Немо и учесть, что информаторы могли ошибиться в окончании его имени, так как океан большой и в нём полно разных диалектов.
- Таким образом задача выглядит так - нам нужны все слова, начинающиеся с
Nem
(N может быть и строчной), помимо этого возможны различные окончания размером в 1-2 буквы. - Как написать такое выражение? Официальная документация к библиотеке re
- В результате:
- нужно составить шаблон из 4-5 букв,
- первая может быть как строчной, так и заглавной, + окончание из 2 неизвестных букв.
- За буквы отвечает символ
\w
, количество повторов можно указать с помощью квантификатора{,2}
. - Первую букву мы помещаем в квадратные скобки, чтобы была выбрана одна из них
Обратите внимание на предшествующую r' '
в описании строки - это так называемые сырые строки.
Модуль re
Функции
В python с регулярными выражениями работает модуль re
И ознакомимся с его основными функциями: Первая из них match, которая зачастую работает быстрее других фукнций поиска, т.к. ищет совпадения только с начала строки
search()
же производит поиск по всему тексту, но только до первого совпадения.
matched = re.search(nemo_pattern, deep_ocean)
print(f'Возвращается объект {matched} внутри которого содержится информация о совпадении') # re.Match object
Эту информацию можно извлечь следующими методами:
print(f'Подстрока, совпавшая с паттерном поиска {matched.group()}') # type 'str'
print(f'Индекс начала этой подстроки {matched.start()}') # 'int'
print(f'И индекс её окончания {matched.end()}') # 'int'
Чтобы получить не только первый результат поиска мы можем воспользоваться итератором:
searching_iterator = re.finditer(nemo_pattern, deep_ocean)
for matched in searching_iterator:
# сам по себе итератор будет возвращать объекты класса re.Match, как делал re.search
print(f'Следующее совпадение: {matched.group()}') # 'str'
Обыскав почти весь океан, Марлин начал отчаиваться, но не RegExp, медленным козырем в рукаве была функция findall()
full_search = re.findall(nemo_pattern, deep_ocean)
print(f'Список со всеми совпадениями паттерна, найденными во всем тексте {full_search}') # 'list'
Прочесав весь океан, мы видим, что в наши "сети" попал и Nemo. Стоит обратить внимание на то, что данная функция, в отличие от предыдущих возвращает список из строк. Поэтому мы не можем применить методы для извлечения индексов. Однако мы можем использовать эту информацию для уточнения шаблона.
final_matched = re.search('Nemo', deep_ocean)
print(f'Возвращение объекта с нужным совпадением {final_matched}') # re.Match object
print(f'Уточнение индекса начала {final_matched.start()}')
print(f'И конца {final_matched.end()}')
Функция sub
позволяет не только осуществлять поиск, но и заменять найденное
transparent = re.sub(r'[Oo]\w{4}', '', deep_ocean)
print('Всё, что совпало с паттерном было заменено, осталось лишь')
print(transparent)
Всё, что совпало с паттерном было заменено, осталось лишь:
Чтобы избавиться от пустот и оставить лишь наших рыбок, можно немного изменить шаблон.
cleared = re.sub(r'[Oo]\w{4}\s+', '', deep_ocean)
print('Теперь в тексте остались лишь упоминания наших рыбок')
print(cleared)
В дополнение к этой функции, есть ещё одна, способная разделять текст как str.split()
только в качестве разделителя используя заданную подстроку
separation = re.split('nemoO', cleared)
print(f'Список из частей, между котороыми был найден наш паттерн {separation}') # 'list'
Функция fullmatch()
очень придирчива и возвращает результат только в случае полного совпадения
full_match = re.fullmatch('Marlin nemaa nemoO nenemo Nemo', cleared)
print(f'Полного совпадения найдено не было {full_match}') # None
full_match = re.fullmatch('Marlin nemaa nemoO nenemo Nemo ', cleared)
print(f'А вот теперь совпадение нашлось {full_match}') # re.Match object
А вы нашли отличие?
Особенности синтаксиса регулярных выражений
OR в регулярных выражениях
В регулярных выражениях существует аналог питоновскому or, это символ '|'
Он позволяет связать два шаблона в один, и если строка подойдёт хотя бы к одному из них, то она подойдёт этому новому шаблону. Так строка подходящая к шаблону А или к шаблону B подойдёт к шаблону A|B
Например, отдельные овощи в тексте можно искать при помощи шаблона 'морковк|св[её]кл|картошк|редиск'
Экранирование
В пайтоне, символ '\'
, который в обычных строках означает экранирование последующего символа.
Т.к. в регулярных выражениях большинство символов используют '\'
(\d,\w...)
Нам придётся экранировать их каждый раз (\\d, \\w...)
Чтобы избежать этого, перед строкой добавляют литерал r, который сообщает пайтону о том, что воспринимать '\'
можно не как экранирующий символ.
Однако!
Среди специальных символов регулярных выражений есть те, которые не используют '\'
Например '.'
, но вдруг нам в тексте необходимо будет найти именно точку?
Её придётся всё же экранировать слэшем '\.'
В примере \(.*\)
экранированы символы скобок.
Если бы мы этого не сделали, скобки были бы приняты за специальные символы, которые означают группировку, суть которой мы рассмотрим позже
Где можно отладить сложные регулярки?
https://regex101.com/#python
На этом сайте тоже есть отличный отладчик, который при этом учитывает синтаксис Питона + есть таблица с подсказками
https://pythex.org/
Самый простой из своих собратьев, для более уверенных пользователей
Примеры
Задача №1. Регистрационные знаки транспортных средств
В России применяются регистрационные знаки нескольких видов. Общего в них то, что они состоят из цифр и букв. Причём используются только 12 букв кириллицы, имеющие графические аналоги в латинском алфавите — А, В, Е, К, М, Н, О, Р, С, Т, У и Х. * У частных легковых автомобилях номера — это буква, три цифры, две буквы, затем две или три цифры с кодом региона. * У такси — две буквы, три цифры, затем две или три цифры с кодом региона. * Есть также и другие виды, но в этой задаче они не понадобятся.
# Задача в том, чтобы из перечня номеров найти номера частных автомобилей и номера такси:
license_plates = 'А578ВЕ777 ОР233787 К901МН666 СТ46599 СНИ2929П777 666АМР666'
# Напишем для каждого типа номеров свой шаблон:
private_template = r'[АВЕКМНОРСТУХ]\d{3}[АВЕКМНОРСТУХ]{2}\d{2,3}'
taxi_template = r'[АВЕКМНОРСТУХ]{2}\d{3}\d{2,3}'
private_cars = re.findall(private_template, license_plates)
taxi_cars = re.findall(taxi_template, license_plates)
print(f'Список номеров частным автомобилей {private_cars}') # ['А578ВЕ777', 'К901МН666']
print(f'Список номеров такси {taxi_cars}') # ['ОР233787', 'СТ46599']
Задача №2
В многопользовательской онлайн игре введены новые правила нейминга. Вам, как самом ответственному блюстителю порядка, предстоит проверить всю базу данных, хранящую имена персонажей и "заморозить" тех персонажей, имена которых не проходят новые правила.
Сами правила:
* Ограничение длины: от 3 до 17 символов
* Ограничение алфавита: все английские буквы, кроме xyz
* Первый символ обязательно должен быть буквой,
* в остальных случаях допускаются четные цифры, подчеркивания, слэши.
gamers = ''' Dep3kuu_CaMypau41 3a_6a3ap_oTBeTb19 kypuTe_6aM6yk16 XoJIogHbIu_TankucT9
BupTyaJlbHblu_BouH8 cepDuTblu_oxoTHuk6 TTaPHuLLIa6 Алмазик5 9I_ODun_Takou_KPyTou4 9l_aBTopuTeT4
ABToMaT_kaJlaLLlHukoBa4 cepb3Hblu_4eJl4 cepb3Hblu_napHuLLIa4 kpyTa9l_6a3yka4 TIpocTo_He_xaMu4 ÊÐÌÑÕRÕG3
3a_6a3ap_oTBe4al-o3 Cama_OTTacHocTb3 cepb3Hblu3 cJlblLLI_He_Tblkau3 M9TA3 MaJlo_BpeMeHu3
ToHupoBka_no_kpyry3 '''
naming_rules = r'\s[a-wA-W][a-wA-W0-9\/_]{2,16}\s'
# Начало и конец заключены в \s чтобы отделить совпадения друг от друга
survivors = re.findall(naming_rules, gamers)
print(f'Список ников, прошедших проверку {survivors}')
Задача №3. Количество слов в тексте.
В данной задаче словом будет считаться последовательность букв, внутри которой может быть дефис. В заданном тексте нужно найти количество слов(не учитывая союзы):
text_for_counting = '''– Они мясные.
– Мясные?
– Да. Они сделаны из мяса.
– Из мяса?!
– Ошибка исключена. Мы подобрали несколько экземпляров с разных частей планеты, доставили на борт нашего
корабля-разведчика и как следует протестировали. Они полностью из мяса.
– Но это невероятно! А как же радиосигналы? А послания к звездам?
– Для общения они используют радиоволны, но сигналы посылают не сами. Сигналы исходят от машин.
– Но кто строит эти машины? Вот с кем нужен контакт!
– Они и строят. О чем я тебе и толкую. Мясо делает машины.
– Что за чушь! Как может мясо изготовить машину? Ты хочешь, чтобы я поверил в мясо с памятью и чувствами?'''
rule_for_counting = r'[а-яА-Я-]{3,}'
# Чтобы не углубляться в правила русского языка будем искать слова длинной более 2 символов
words = re.findall(rule_for_counting, text_for_counting)
print(f' Длина списка найденных совпадений {len(words)}')
Группы в регулярном выражении
Как вы могли заметить, в этих мудреных примерах валидации почтовых адресов, часто используются круглые скобки (...)
Их значение заключается в двух функциях:
- Эти скобки призваны сократить повторяющиеся группы внутри шаблонов.
Например:
MAC-адрес сетевого устройства обычно записывается как шесть групп из двух шестнадцатиричных цифр, разделённых символами '-' или ':'
01:23:45:67:89:ab
Без применения скобочных групп шаблон будет выглядеть так
[0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}
Сгруппировав повторяющиеся части, можно с помощью квантификаторов задать количество их повторов:
[0-9a-fA-F]{2}(?:[:-][0-9a-fA-F]{2}){5}
Что даже на таком просто примере позволило значительно сократить размер регулярного выражения.
Ещё одна сильная сторона подобных группировок в том, что теперь мы можем писать шаблон даже не зная точного количества групп. Ведь в квантификаторе можно указать не только точное число, но и отрезок, на котором оно должно находится - Используя скобки (...) с функциями re.search(), re.fullmatch() и re.finditer() в возвращенных match-объектах мы сможем получить доступ к информации, по каждой группе, выделенной скобками, отдельно ( часть подстроки, которая совпала этой группой и индексы)
Пример
pattern = r'\s*([А-Яа-яЁё]+)(\d+)\s*'
string = r'--- Опять45 ---'
matched = re.search(pattern, string)
print(f'Совпадение вернулось объектом {matched}') # re.Match object
match[0] в обычном случае работает так же, как и match.group()
Но теперь с помощью match[1] и match[2] мы можем вернуть подстроки, совпавшие с первой группой - ([А-Яа-яЁё]+) - 'Опять' и со второй (\d+) - '45'.
print(f'Найдена подстрока >{matched[0]}< с позиции {matched.start(0)} до {matched.end(0)}')
# Найдена подстрока > Опять45 < с позиции 3 до 16
print(f'Группа букв >{matched[1]}< с позиции {matched.start(1)} до {matched.end(1)}')
# Группа букв >Опять< с позиции 6 до 11
print(f'Группа цифр >{matched[2]}< с позиции {matched.start(2)} до {matched.end(2)}')
# Группа цифр >45< с позиции 11 до 13
Задача №4
Вовочка подготовил одно очень важное письмо, но везде указал неправильное время. Поэтому нужно заменить все вхождения времени на строку (TBD). Время — это строка вида HH:MM:SS или HH:MM, в которой HH — число от 00 до 23, а MM и SS — число от 00 до 59.
letter = '''Уважаемые! Если вы к 09:00 не вернёте
чемодан, то уже в 09:00:01 я за себя не отвечаю.
PS. С отношением 25:50 всё нормально!'''
time_rule = r'([01]\d|2[0-3])(:[0-5][0-9]){1,2}'
time_swap = re.sub(time_rule, '(TBD)', letter)
print(f'Письмо Вовочки теперь выглядит так: {time_swap}')
Проблемы регулярных выражений
Среди программистов ходит такая шутка:
“У вас есть проблема. Вы решили использовать регулярные выражения чтобы её решить. Теперь у вас две проблемы.”
Связано это со сложностью регулярок. Можно написать шаблон и думать что он рабочий, но... это не так.
К примеру одной из распространенных задачек, при изучении регулярных выражений, является валидация email. И хоть сами по себе задачки интересные, делать так на реальных примерах не стоит. В отсутствии четких требований к регистрации e-mail адресов, растет и размер регулярных выражений.
Вот пример одного из таких выражений:
(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])
Подводя итог, надо признать что RegExp-ы очень мощное средство для решения задач обработки текста. Но, как любое мощное средство, требует аккуратного обращения. Дабы не выстрелить себе в ногу.