#!/usr/bin/env python # coding: utf-8 # # Основы программирования в Python # # *Алла Тамбовцева, НИУ ВШЭ* # ### Введение в регулярные выражения # *Большой эпиграф* # # # # > Алиса попыталась прочесть надпись на боку шара, которая выглядела так: # # > «С ‒ гнездо ‒ В ‒ спящая птица ‒ Н ‒ стая бабочек ‒ Я УЛ ‒ гнездо ‒ А.» # # > ‒ Ничего не понимаю, ‒ сказала Алиса. # # > ‒ К сожалению, я тоже забыл, ‒ сказал робот. # # > ‒ Может, это Свиная улица? ‒ спросила Алиса. # # > ‒ Нет, ‒ обиделся робот, ‒ у нас не может быть такого названия. # # > ‒ Улица Свидания, ‒ подсказал турист с Альдебарана. # # > ‒ Нет, ‒ сказал робот, ‒ для свиданий у нас парк, а не улица. # # > ‒ Все просто, ‒ сказал турист-двадцатитрехног. ‒ Это Северная улица. # # > ‒ Нет, возразил робот, ‒ север у нас совершенно в другой стороне. # # > Каждый из туристов пытался помочь Алисе и предложить свое название. # # > Когда исчерпались все названия на космическом языке, некоторые стали предлагать слова на своих родных языках. # # > ‒ Совенкуня улица! ‒ кричал двухголовый веганец. # # > ‒ Улица Справгенупяря? # # > ‒ Улица Сдерв-ван-ни-ван-ня? # # > <...> # # > И неизвестно, сколько бы времени это продолжалось, если бы какой-то мальчишка не кинул бутербродом в шарб да так метко, что попал точно в стайку бабочек. Бабочки взлетели, и оказалось, что они скрывали за собой буквы "ИРНА". # Получилось «С..В..НИРНАЯ УЛ..». # ‒ Вспомнил! ‒ воскликнул робот. ‒ Это Cувенирная улица! # # > Кир Булычёв, Миллион приключений #

#
# Регулярные выражения ‒ выражения, последовательности символов, которые позволяют искать совпадения в тексте. Выражаясь более формально, они помогают найти подстроки определенного вида в строке. Еще о регулярных выражениях можно думать как о шаблонах, в которые мы можем подставлять текст, и этот текст либо соответствует шаблону, либо нет. В самом простом случае в качестве регулярного выражения может использоваться обычная строка. Например, чтобы найти в предложении *Кошка сидит под столом.* слово *Кошка*, ничего специального применять не нужно, достаточно воспользоваться оператором `in`: # In[32]: sent = 'Кошка сидит под столом.' 'Кошка' in sent # Если нас интересует слово *кошка* в любом регистре, то это уже более интересная задача. Правда, ее все еще можно решить без регулярных выражений, приведя все слова в `sent` к нижнему регистру. А что, если у нас будет текст подлиннее, и в нем необходимо "обнаружить" *кошку* в разных падежах? И еще производные слова вроде *кошечка*? Тут уже удобнее написать некоторый шаблон, чтобы не создавать длинный список слов с разными формами слова *кошка*. И на помощь придут регулярные выражения. Прежде, чем знакомиться с ними в Python, посмотрим на общие правила построения регулярных выражений, которые верны всегда, не только в Python и не только в программировании вообще. # * Промежутки, заключенные в квадратные скобки, позволяют найти цифры или буквы разных алфавитов и разных регистров # # # [0-9] соответствует любой цифре # # [A-Z] соответствует любой заглавной букве английского алфавита # # [a-z] соответствует любой строчной букве английского алфавита # # [А-Я] и [а-я] ‒ аналогично для букв русского алфавита # * Для цифр есть специальный символ `\d` (от *digit*). Добавление обратного слэша называется экранированием: так мы отмечаем, что ищем именно цифру, а не просто букву d. # # * Для пробела тоже существует свой символ ‒ `\s` (от *space*). Этот символ соответсвуют ровно одному пробелу в тексте. # # * Любой знак, отличный от пробела, обозначается как `\S` (заглавная буква здесь отвечает за отрицание). # Для разбора дальнейших символов в регулярных выражениях, создадим небольшой набор слов (не очень осмысленный, но удобный): # # ха, хаха, ха-ха, хах, хех # * Знак `.` соответствует одному любому символу в строке. Так, регулярное выражение `x.x` "поймает" слова *хах* и *хех*. # * Знак `+` соответствует одному или более вхождению символа(ов), который стоит слева от `+`. Выражение `xa+` "поймает" слова *xa* и *хаха*. # * Знак `*` соответствует нулю или более вхождениям символа, который стоит слева от `*`. Выражение `xaх*` "поймает" слова *xa* и *хах*. # * Знак `?` соответствует нулю или одному вхождению символа, который стоит слева от `?`. Выражение `xa?` "поймает" слово *xa*. # Как быть, если с помощью регулярного выражения нужно найти подстроку, содержащую знаки препинания? Те же точки, вопросительные знаки, скобки? Нужно их экранировать ‒ ставить перед ними `\`, например, `\.`, `\,`, `\?`. Это символ будет сообщать Python, что нам нужен именно конкретный символ (точка, запятая, знак вопроса и др.). # В регулярных выражениях можно явно задавать число повторений символов. Если мы знаем точное число символов, то его можно указать в фигурных скобках. Так, выражение `а{4}` будет соответствовать четырем буквам `a`подряд. Если точное число повторений нам неизвестно, можно задать диапазон, указав начало и конец отрезка через запятую. Например, такое выражение позволит найти от двух до четырех букв `a` подряд: `a{2,4}`. Если известен только левый или правый конец отрезка, то второй конец можно опустить: `a{2,}` (не менее двух) или `a{,4}` (не более 4). # В регулярных выражениях также можно использовать условие *или*. Например, возвращаясь к нашей "смеющейся"строке, если мы напишем выражение `x[о|е]х`, оно поймает слова *хах* и *хех*, а вот вдруг появившийся *хох* не поймает. # # Этими примерами, конечно, синтаксис регулярных выражений не ограничивается, но давайте для начала на этом остановимся. Какие-то примеры будут всплывать по ходу занятий, с какими-то более специфическими случаями вы сможете познакомиться самостоятельно. # Теперь перейдем к Python. Импортируем модуль `re` для работы с регулярными выражениями: # In[1]: import re # Создадим какой-нибудь незамысловатый текст с разными датами: # In[2]: text = "12 ноября 2011 года произошло удивительное событие. А 13 ноября 2012 - еще удивительнее. Даже не будем \ говорить, что произошло 2 декабря 2011 года и 25 декабря 2012 года." # Напишем регулярное выражение, которое будет соответствовать всем цифрам в тексте (не числам), и найдем их все в `text` с помощью функции `findall()`: # In[3]: re.findall("\d", text) # отдельно цифры # Если забыли, что числа можно искать с помощью `\d`, можно задействовать промежуток (только не забудьте квадратные скобки): # In[5]: re.findall("[0-9]", text) # А что, если мы хотим "ловить" не цифры, а числа, то есть последовательности из одной или более цифры. Условию "один и более" соответствует символ `+`. Попробуем. # In[14]: re.findall("\d+", text) # отдельно числа # Получилось! А если сочетания по 1-2 цифры (иногда с пробелом после)? Тут нужен знак `.`, который отвечает ровно за один символ. # In[10]: re.findall("\d.", text) # отдельно числа по 1-2 цифры # Что будет, если мы воспользуемся знаком `?`? Он отвечает за наличие 0 или 1 символа, стоящего слева от регулярного выражения. # In[11]: re.findall("\d?", text) # по 1 символу # Получили какое-то безобразие. Но это безобразие оправдано: добавив `?` мы поставили условие, что в подстроке либо есть ровно одна цифра, либо ее нет. Поэтому мы и получили такой странный список. # **Задание 1:** написать регулярное выражение, которое будет "ловить" все годы в тексте. # # *Решение:* # In[13]: re.findall("\d{4}", text) # 4 цифры подряд # **Задание 2:** написать регулярное выражение, которое будет "ловить" все слова с основой *удивительн* в тексте. # # *Решение:* # In[18]: re.findall("удивительн..", text) # из текста знаем, что больше двух букв после не будет # Теперь давайте вместе напишем регулярное выражение, которое будет соответствовать датам с годами. Как выглядят даты в нашем тексте? Сначала идет одна цифра или более, затем пробел, далее буквенное название месяца, пробел и снова цифры, но теперь уже ровно 4, так как они складываются в год. Как обозначаются цифры мы знаем, русские буквы тоже. пробелу соответствует символ `\s` (обратный слэш обязателен, так как без него это будет обычная буква *s*). # In[19]: re.findall("\d+\s+[а-я]+\s\d{4}", text) # осталось прочитать регулярку по слогам :) # Теперь давайте рассмотрим еще один пример. Пусть у нас есть список твитов, только список учебный, вместо полного текста одни хэштеги. # In[20]: twits = ["#я не могу молчать", "#я не могу кричать", "#я не могу", "#я справлюсь", "я не могу молчать", "#я не могу жить", "#я все могу", "#с кем не бывает"] # Задача: создать новый список, содержащий только твиты, начинающиеся с `#я не могу`. Сначала напишем регулярное выражение и посмотрим, как оно работает. # In[22]: for t in twits: print(re.findall("#я не могу", t)) # Написать такое выражение совсем несложно, осталось теперь правильно использовать его в цикле. # In[24]: chosen = [] for t in twits: res = re.findall("#я не могу", t) if len(res) != 0: chosen.append(t) # именно t, не res, так как добавляем твит полностью chosen # Напоследок рассмотрим какую-нибудь задачу, где необходимо применить экранирование. Пусть у нас есть некоторая строка с данными: # In[26]: data = '20.05.1963, 55, 12.12.2000, 17, 15/15/1111' # И нам нужно выбрать из нее даты, записанные через точку. Напишем регулярное выражение, которое позволит это сделать, но перед этим вспомним, что точку нужно экранировать ‒ ставить перед ней `\`, чтобы Python понимал, что мы ищем не один любой символ (`.`), а именно точку как знак препинания. # In[27]: re.findall("\d+\.\d+.\d{4}", data) # готово # Сегодня мы познакомились только с одной функцией из модуля `re`, а именно `findall`. На самом деле, функций больше, есть функция `sub()`, которая позволяет не только находить подстроку по регулярному выражению, но и заменять ее на другую подстроку, есть функции, которые позволяют определить индекс символа начала и конца совпадения, функция `split`, которая позволяет разбить строку по найденной подстроке... С некоторыми функциями мы познакомимся позже, когда будем разбирать парсинг, некоторые функции мы опустим, но о них можно почитать в дополнительных материалах и официальной [документации](https://docs.python.org/3/library/re.html). Кроме того, есть очень хороший ресурс [regexr.com](https://regexr.com/), который позволяет скопировать нужный текст и в интерактивном режиме следить, какие совпадения находятся при изменении регулярного выражения, введенного в отдельном окне.