Практическое введение в регулярные выражения для новичков

0
9713

Легкое и веселое введение в теорию регулярных выражений от веб-разработчика Джоша Хоукинса, охватывающее все основные моменты, которые нужно знать новичку.






Вы когда-нибудь работали со строками? Да-да, с теми самыми «массивами символов», которые мы все знаем и любим. Если вы программировали на чем-нибудь, кроме чистого С, с уверенностью можно предположить, что работали, причем не раз. Но что, если вы имеете дело со множеством строк? Или со строками, которые сгенерированы не вашей программой? Например, вы считываете электронное письмо, парсите аргументы командной строки или читаете инструкции, написанные человеком, и вам нужен более структурированный метод работы со всем этим.

Безусловно, вы можете перебирать каждое слово или символ во всех строках. И, вероятно, подобный код будет довольно прост для понимания. Но в масштабных приложениях это может быть излишне громоздким и слишком ресурсозатратным.

Введение. Регулярное выражение

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

  • Регулярные выражения — это правила, описывающие некоторый формальный язык.
  • Регулярные выражения — разновидность формального языка, которая может быть обработана конечным автоматом.

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

Введение. Regex

Сейчас, возможно, мы подошли к месту, где пора испугаться (если вы до сих пор этого не сделали). Мы рассмотрим различия между тем, что вкладывается в понятие регулярных выражений языками программирования, а что — фундаментальной информатикой.

  • Регулярные выражения с точки зрения информатики — правила, объясняющие формальный язык.
  • Регулярные выражения с точки зрения языков программирования — грамматика, выражающая, в большей степени, некоторый контекстно-зависимый язык.

Контекстно-зависимые языки ощутимо сложнее и мощнее, так что с этого момента условимся называть регулярные выражения в терминах языков программирования „regex“, дабы подчеркнуть их обособленность от формальных языков в целом.

Учимся писать regex-ы

Регулярные выражения описываются с помощью двух слэшей (//) и соответствуют строкам, подходящим под шаблон, заключенный между ними. Например, /Hi/ соответствует „Hi“, так что мы можем проверить соответствие некоторой строки этому шаблону.

Символы в регулярных выражениях сопоставляются в том порядке, в котором вводятся. Так /Hello world/ отвечает строке „Hello world“.

Можно упростить поиск произвольных слов, добавив немного regex-магии: \w соответствует любому «слову», составленному только из букв. По такому же принципу идентифицируются числа: \d.

Пример 1

Превосходно, теперь мы можем сравнивать строки или проверять их соответствие некоторому паттерну. Что дальше? Могут ли регулярные выражения выполнять еще какие-нибудь функции?

Будьте уверены! Скажем, мы написали IRC чат бота, который реагирует, если кто-то напишет „Josh“. Наш бот сканирует каждое сообщение, пока не дождется совпадения. Тогда бот отвечает: „Woah, I hope you aren’t talking bad about my pal Josh!“ («О, надеюсь, вы не будете говорить плохо о моем приятеле Джоше!»). Потому что с Джошами дружат только роботы.

Для сравнения строк наш бот использует шаблон /Josh/. В один прекрасный момент некто по имени Eli обронит: „Eli: Josh, do you really need that much caffeine?“ («Эли: Джош, тебе действительно необходимо такое количество кофеина?»). Наш бот навострит ушки, обнаружит совпадение, выдаст свой неожиданный ответ, чем достаточно напугает Эли. Миссия выполнена! Или нет?

Что, если бы наш бот был более умным? Что, если бы он, например, обращался к говорящему по имени? Что-нибудь вроде „Woah, I hope you aren’t bad-mouthing my buddy Josh, Eli.“ («О, надеюсь, ты не будешь злословить о моем приятеле Джоше, Эли?»).

Квантификаторы (повторяющиеся символы)

0 и более

Мы можем сделать это… Но для начала нужно уяснить пару моментов. Первый — квантификаторы (для повторяющихся символов). Можно использовать *, чтобы обозначить 0 или несколько символов после. Например, /a*/ может соответствовать „aaaaaaa“, а также „“. Да, вы не ослышались: оно будет отвечать пустой строке.

* cлужит для обозначения чего-то необязательного, так как символ, которому она соответствует, существовать вовсе не обязан. Но он может. И не раз (теоретически, бесчисленное множество раз).
Можно обозначить „Josh“ с помощью /Josh/, но мы можем также задать „Jjjjjjjjjosh“ или „osh“ паттерном /J*osh/.

1 и более

Для обозначения одного и более символов используется +. Он эффективно работает по тому же принципу, что и *, за исключением того, что существование хотя бы одного символа более не является опциональным: должен присутствовать по крайней мере один.

Таким образом, мы можем задать шаблоном /J+osh/ строки „Josh“ или „Jjjjjjjjjosh“, но не „osh“.

Метасимволы

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

Но что, если он злится настолько сильно, что даже пару раз ударил лицом по клавиатуре? Как нам обозначить «ааавыопшадлорвпт», не зная заранее, насколько меток его нос?
С помощью метасимволов!

Метасимволы позволяют задавать абсолютно ЧТО УГОДНО. Их синтаксис — . . (Да, точка. Просто точка.). Бьемся об заклад, вы часто пользуетесь ею, так что не стесняйтесь обозначать ей конец предложения.

Можно задать „Joooafhuaisggsh“ выражением /Jo+.*sh/, комбинируя полученные ранее знания о повторяющихся символах и метасимволах. Если быть точными, данное выражение соответствует одной „J“, одному или более „o“, нулю или нескольким метасимволам, а также одной „s“ и одной „h“. Эти пять блоков подводят нас к тому, что мы называем…

…группами символов

Группы символов — это символьные блоки, в которых важна последовательность составляющих. Они рассматриваются как единое целое. Используя * или +, вы фактически задаете последовательность повторяющейся группы символов, а не только лишь последнего символа.

Это полезно понять и как отдельную технику, но бОльшую функциональность она обретает в сочетании с повторяющимися символами. Группы символов задаются с помощью круглых скобок (да-да, этих ребят).
Допустим, мы хотим повторять „Jos“, но не „h“. что-то вроде „JosJosJosJosJosh“. Это можно сделать выражением /(Jos)+h/. Просто, не правда ли?

Но наконец… Возвращаясь к нашему первому примеру, как нам получить имя Эли в нашем IRC чате из отправленного ею сообщения?

Группы символов могут также служить для запоминания подстрок. Для этого обычно делают что-то вроде \1, чтобы определить первую заданную группу.

Например, /(.+) \1/ — особый случай. Здесь мы видим набор случайных символов, повторяющийся один или более раз, пробел после него, а затем повторение точно такого же набора еще раз. Так что такое выражение будет соответствовать „abc abc“, но не „abc def“, даже если „def“ сам по себе отвечает (.*).

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

Пример 2

Фух… Наконец-то можно вернуться к примеру с IRC чат ботом. Давайте применим наши знания на практике.

Если мы хотим выцепить имя отправителя сообщения, когда он пишет „Josh“, наше выражение будет выглядеть примерно так: /(\w+): .*Josh.*/, и мы сможем сохранить результат в переменной в нашем языке программирования для ответа.

Давайте рассмотрим наше регулярное выражение. Здесь одна или более букв, следующих за «:», 0 или более символов, „Josh“ и снова 0 или более символов.

Заметка: /.*word.*/ — простой способ задать строку, содержащую „word“, причем другие символы в ней могут присутствовать, а могут и нет.

На Python это будет выглядеть следующим образом:
import re
pattern = re.compile(ur'(\w+): .*Josh.*') # Our regex
string = u"Eli: Josh go move your laundry" # Our string
matches = re.match(pattern, string) # Test the string
who = matches.group(1) # Get who said the message
print(who) # "Eli"

Заметьте, что мы использовали .group(1) точно так же, как \1. В этом нет ничего нового, за исключением использования регулярных выражений в Питоне.

Начало и конец

До этого момента мы предполагали, что искомые подстроки могут находиться в любом месте строки. К примеру, /(Jos)+h/ соответствует любой строке, которая содержит „Jos-повторяющееся-h“ в произвольном месте.

А что, если нам необходимо, чтобы строка начиналась с этого шаблона? Это можно обозначить как /^(Jos)+h/, где ^ соответствует началу строки. Аналогично, $ обозначает конец строки.

Теперь, если мы хотим задать строку, содержащую только „Jos-повторяющееся-h“, то напишем /^(Jos)+h$/.

Перечисление выражений

Представьте, что вы пишете регулярное выражение для рецепта бутерброда. Вы не знаете, предпочитает заказчик белый хлеб или черный, но выбрать все равно придется только один. Как добавить возможность выбора в regex? С помощью перечислений!

Они позволяют задавать наборы возможных значений для группы символов. Это выглядит следующим образом: (white|wheat). В контексте нашего примера с бутербродом, будет принят один из вариантов — либо „white“, либо „wheat“.

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

Модификаторы

Мы говорили о regex с /двумя слэшами/, верно? Мы знаем, что находится между ними, но что должно быть снаружи?

Неожиданный поворот: ничего!

…слева. Правая сторона, напротив, может содержать множество, множество всего полезного. Даже стыдно, что мы так долго не сказали об этом ни слова!
Модификаторы задают правила, по которым применяются регулярные выражения.

Вот список основных модификаторов (с Regex101.com):

МодификаторНазваниеОписание
gglobalВсе совпадения
mmulti-line^ и $ соответствуют началу и концу каждой строки
iinsensitiveРегистронезависимое сравнение
xextendedПробелы и текст после # игнорируются
Xextra\ с произвольной буквой, не имеющей особого значения, возвращает ошибку
ssingle lineИгнорирует символы новой строки
uunicodeСтроки-шаблоны обрабатываются как UTF-16
UungreedyПо умолчанию в regex используется «ленивая квантификация». Модификатор U делает квантификацию «жадной»
AanchoredШаблон форсируется к ^
JduplicateРазрешает дублирующиеся имена субпаттерннов

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

Предположим, Эли взбесилась настолько, что начала бомбить чат сообщениями с БуквАМи рАЗных РегИсТРОв. Это нас не страшит, потому что i уже здесь! Мы можем легко задать гневливое выражение „I hAate LiVing witH JOSH!!!“ паттерном /i ha+te living with josh!+/i. Теперь наши regex стали легче читаемы, а также намного более мощны и полезны. Замечательно!

Вы можете самостоятельно поиграть с разными модификаторами. На мой взгляд, в целом вам больше всего пригодится igm.

Что дальше?

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

Существует множество символов и их сочетаний, используемых в регулярных выражениях. Обычно вы будете натыкаться на них во время изучения Stack Overflow, но о значении некоторых можно догадаться и из предыдущих примеров (например, \n — символ перехода на новую строку). База заложена, но выучить предстоит еще очень многое.

Найти полный список сочетаний символов, а также проверить свои знания можно здесь.
Если это показалось для вас проще простого, попробуйте regex-кроссворды. Они действительно заставят вас попотеть.

После точки

Эта статья — перевод гайда Джоша Хоукинса. Джош — страстный веб-разработчик из Алабамы. Он начал программировать в возрасте девяти лет, сосредоточившись на видеоиграх, десктопных и некоторых мобильных приложениях. Однако, во время стажировки в 2015, Джош открыл для себя веб-разработку и ворвался в мир опенсорса, связанного с этой областью.