{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Основы программирования в Python\n", "\n", "*Алла Тамбовцева, НИУ ВШЭ*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Введение в регулярные выражения" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Большой эпиграф*\n", "\n", "\n", "\n", "> Алиса попыталась прочесть надпись на боку шара, которая выглядела так: \n", " \n", "> «С ‒ гнездо ‒ В ‒ спящая птица ‒ Н ‒ стая бабочек ‒ Я УЛ ‒ гнездо ‒ А.» \n", "\n", "> ‒ Ничего не понимаю, ‒ сказала Алиса.\n", "\n", "> ‒ К сожалению, я тоже забыл, ‒ сказал робот.\n", "\n", "> ‒ Может, это Свиная улица? ‒ спросила Алиса.\n", "\n", "> ‒ Нет, ‒ обиделся робот, ‒ у нас не может быть такого названия. \n", "\n", "> ‒ Улица Свидания, ‒ подсказал турист с Альдебарана.\n", "\n", "> ‒ Нет, ‒ сказал робот, ‒ для свиданий у нас парк, а не улица.\n", "\n", "> ‒ Все просто, ‒ сказал турист-двадцатитрехног. ‒ Это Северная улица.\n", "\n", "> ‒ Нет, возразил робот, ‒ север у нас совершенно в другой стороне.\n", "\n", "> Каждый из туристов пытался помочь Алисе и предложить свое название. \n", "\n", "> Когда исчерпались все названия на космическом языке, некоторые стали предлагать слова на своих родных языках. \n", "\n", "> ‒ Совенкуня улица! ‒ кричал двухголовый веганец.\n", "\n", "> ‒ Улица Справгенупяря?\n", "\n", "> ‒ Улица Сдерв-ван-ни-ван-ня?\n", "\n", "> <...>\n", "\n", "> И неизвестно, сколько бы времени это продолжалось, если бы какой-то мальчишка не кинул бутербродом в шарб да так метко, что попал точно в стайку бабочек. Бабочки взлетели, и оказалось, что они скрывали за собой буквы \"ИРНА\".\n", "Получилось «С..В..НИРНАЯ УЛ..».\n", "‒ Вспомнил! ‒ воскликнул робот. ‒ Это Cувенирная улица!\n", "\n", "> Кир Булычёв, Миллион приключений\n", "

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