Семинар 4: Регулярные выражения

Исторически так сложилось, что самыми первыми пользователями вычислительных машин были математики. Так как программирование не являлось их основной профессией (и в те незапамятные времена не являлось профессией в принципе), а разбираться в машинных кодах было не только лениво и неинтересно, но еще и совершенно непродуктивно, они придумали себе языки высокого уровня. Одним из самых известных таких языков является FORTRAN.

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

При этом следует отметить, что основное внимание в таких языках уделялось именно числовой информации, работа со строками была представлена слабо - это была эра обработки чисел.

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

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

На этом семинаре мы познакомимся с очень мощной концепцией - регулярными выражениями.

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

Начнём с примера.

Есть задача — валидация e-mail адресов.

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

Однако эту задачу можно решить проще, используя регулярные выражения:

emails = ['my-cool-email@mail.ru', 'плохой)address^mail@brr.dom.']

emails.grep /^[\w0-9._%+-]+@[\w0-9-]+.+.[\w]{2,}$/i do |_|
  puts "#{_} — валидный адрес электронной почты"
  _
end

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

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

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

Примеры шаблонов:

  • abcd12\w45 — шаблон, который соответствует любой подстроке, состоящей по порядку: из одной подстроки abcd12, одного любого символа английского алфавита или цифры, а затем двух цифр — 45.

  • [e-l]\s?\d+ — шаблон, который соответствует любой подстроке, состоящей по порядку: из одного символа в диапазоне английских букв от e до l, одному пробельному символу или нулю таких символов, и одному или более десятичных цифр.

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

Как писать эти шаблоны?

  • Обычно шаблоны разграничиваются слэшами: /

  • В простейшем случае, если мы ищем определённое слово, то можно обойтись регуляркой следующего вида:

/искомоеслово/
  • Если мы ищем несколько слов на выбор, то можно использовать метасимвол |, называемый «пайп« или просто вертикальная черта:
/слово-раз|слово-два/

Этот метасимвол экваивалентен логической функции ИЛИ. Он позволяет находить или слово-раз, или слово-два.

А что, если нам нужно сделать более общий поиск?

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

Классы (или классы символов) — множества символов. Они могут быть объединены каким-то общим свойством. А могут и не быть. Но главное — это множество символов.

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

  • [abcde] — символы a, b, c, d, e.

Классы могут включать в себя диапазон символов. Диапазон определяется при помощи метасимвола дефиса -, помещаемого между крайними символами диапазона. Положение диапазонов относительно друг друга никак не влияет на результат. Так, следующие классы равнозначны:

  • [abcdefwxyz]. [wxa-dyz]. [a-dw-z] и [w-za-d] — все включают символы из первой группы.

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

  • [^a-z] — все символы, кроме символов латиницы в нижнем регистре.

В классы символов могут попадать абсолютно любые символы. Хоть #, хоть %, хоть :. Но некоторые символы являются метасимволами (имеют особое значение), поэтому их нужно экранировать, то есть заменять сам символ на последовательность из бэкслеша \ и самого символа. Экранируются следующие символы:

  • - — дефис, всегда.
  • ^ — циркумфлекс или «крышечка», только в начале группы.
  • . — точка, всегда.

Если не уверены, экранируется ли символ, экранируйте. 😃

Примеры обычных классов
  • [a-l] — класс, задающий диапазон символов латиницы в нижнем регистре, находящихся в английском алфавите между a и l, включая их.

  • [%^&*()] — класс символов, в который входят символы %, ^, &, *, (, ).

  • [\-_=] — класс символов, состоящий из -, _, =.

В языке регулярных выражений бывают встроенные классы, которые предопределены заранее и чей синтаксис поддерживается движком регулярок. Некоторые встроенные классы имеют себе в пару инверсные/обратные классы. Чаще всего на практике используют именно встроенные классы символов.

  • \w и \W

    • \w — любой символ английского алфавите в любом регистре, десятичные цифры и символ подчёркивания. Аналог [a-zA-Z0-9_].
    • \W — любой символ, кроме тех, что перечислены выше. Аналог [^a-zA-Z0-9_].
  • . — любой символ, кроме перевода новой строки \n. Да, это метасимвол. 😃

  • \d, \D, \h и \H

    • \d — любая десятичная цифра. Аналог [0-9].
    • \D — любой символ, кроме десятичных цифр. Аналог [^0-9].
    • \h — любая цифра шестнадцатиричной системы счисления. Аналог [0-9a-fA-F].
    • \H — любой символ, кроме цифр шестнадцатиричной системы счисления. Аналог [^0-9a-fA-F].
  • \s и \S

    • \s — любой пробельный непечатаемый символ. Аналог [ \t\r\n\f\v].
    • \S — любой символ, кроме [ \t\r\n\f\v]. Аналог [^ \t\r\n\f\v].

А можно как-то определять начало и конец строки?

Действительно, это вполне нормальный вопрос, который может возникнуть. До этого все наши изыскания были связаны лишь с подстроками в целой строке. Но что, если нам нужно сопоставлять с образцом целую строку? Для этого существуют два метасимвола, ^ и $.

^ — «крышечка» или циркумлфекс — метасимвол начала строки.

$ — знак доллара — метасимвол конца строки.

Отлично, мы можем сопоставлять с шаблонами, а можно ли что-то помимо этого?

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

Группы — это последовательности символов (шаблоны) в круглых скобках. Группы используются для определения области действия шаблона, определения приоритета шаблонов и выделения подшаблонов. Таким образом можно выделять подшаблоны в шаблонах.

Синтаксис групп:

(%your_pattern%)

Где %your_pattern% — любой шаблон.

Примеры групп
  • hello(\w\d\w)world — при помощи группировки мы выделили подшаблон, состоящий из одного цифробуквенного символа, одной десятичной цифры и ещё одного цифробуквенного символа.

  • hello, ([Ww]orld|our favourite tutor) — при помощи группировки мы выделили подшаблон, в который вынесли неповторяющиеся символы двух (или трёх) разных шаблонов (эквивалентно hellow, [Ww]orld|hello, our favourite tutor).

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

Синтаксис именованых групп:

(?<%group_name%>%your_pattern%)

Где %group_name% — имя группы, а %your_pattern% — любой шаблон.

Помимо этого группы бывают захватывающие (так называемые захваты) и незахватывающие. Всё, что выше мы определяли, относилось к захватывающим группам. На них можно ссылаться. На незахватывающие группы ссылаться нельзя.

Синтаксис незахватывающих групп:

  • (?:non-capturing)(group)

Это всё замечательно, но нам всегда нужно повторять эти шаблоны? Или как-то можно определить их количество?

Можно, и для этого существуют квантификаторы.

Квантификаторы — это метасимволы, указывающие, сколько раз будет повторяться у нас символ из класса символов, символ или группа. Существуют следующие квантификаторы:

  • a? — повтор символа a ноль раз или один раз.

  • a* — повтор символа a ноль или более раз.

  • a+ — повтор символа a один или более раз.

  • a{m} — повтор символа a ровно m раз.

  • a{n,} — повтор символа a n или более раз.

  • a{m,n} — повтор символа a в диапазоне между m и n раз.

Квантификаторы могут быть жадные и нежадные.

Это всё круто, но я ничего не понял, поэтому могу допускать ошибки и вообще писать неправильно. Где-то можно попрактиковаться с этими вашими регулярными выражениями? Есть ли шпаргалка по этому всему?

Есть хороший веб-ресурс — Rubular. Там можно протестировать свои регулярки и посмотреть на маленькую шпаргалку по их составлению.

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

Класс Regexp

Явный вызов конструктора (используется редко).

a = Regexp.new 'a'
a.class

Краткая форма создания - две косые черты в начале и конце.

w = /\w/
w.class

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

/a/ == /a/
/a+/ == /a/

Матчи

Матчи — это совпадение при сопоставлении с образцом. Для проверки матча используется метод match. В качестве аргумента этот метод принимает строку, в которой и нужно искать матчи. Если нет матчей, вернется nil. Если матчи существуют, вернётся объект типа MatchData, в который попадают все матчи.

rxp = /^\w+$/

p rxp.match 'correct'
p rxp.match 'w rong'

Если писать match не хочется, можно использовать оператор =~. Оператор =~, в отличие от метода match, вернёт индекс первого матча.

p rxp =~ 'correct'
p rxp =~ 'w rong'

Различия между этими двумя способами видно на примере ниже.

word_rxp = /\w+/

string = 'word another'

method_match = word_rxp.match string
operator_match = word_rxp =~ string

p method_match
p operator_match

Именованные группы и немного магии

Мы создаем регулярное выражение, в котором есть две именованные группы — lhs и rhs (left-hand side и right-hand side — левое и правое слово).

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

rexp = /(?<lhs>\w+)(?<rhs>\w+)/

p rexp =~ 'word another'

p lhs
p rhs

Вы увидите что-то вроде NameError: undefined local variable or methodlhs' for main:Object`. Ничего не работает и это логично, ведь lhs и rhs никак нами не объявлены.

НО

p /(?<lhs>\w+) (?<rhs>\w+)/ =~ 'word another'

p lhs
p rhs

Если не создавать специального объекта с регуляркой, то такая неявная магия будет работать, потому что интерпретатор объявит переменные lhs и rhs за нас.

MAGIC!

Нужно ли этим пользоваться? НЕТ.

Как обращаться к именованным группам в таком случае? Используя встроенную магическую переменную $~.

/(?<lhs>\w+) (?<rhs>\w+)/ =~ 'word another'

p $~[:lhs]
p $~[:rhs]

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

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

String#scan

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

'aaa 111 bbb'.scan(/[A-Za-z]+/)
'aaa bbb'.scan(/\d+/)

String#sub

Заменяет первое вхождение регулярного выражения на строку.

'aaa 111 bbb'.sub /[A-Za-z]+/, 'ЗАМЕНА'

String#gsub

Заменяет все вхождения регулярного выражения на строку.

'aaa 111 bbb'.gsub /[A-Za-z]+/, 'ЗАМЕНА'

Методы sub и gsub могут также принимать в качестве второго агрумента словарь

'ae'.gsub /[ae]/, 'a' => '14', 'e' => '88' 

И, самое главное, они могут принимать на вход блок кода.

Этот блок кода принимает на вход построку - матч. Причем в этом блоке можно также вызывать sub, gsub или другой метод для строки.

Заключение

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

Полезные ссылки

Стоит продублировать - Rubular. Эта штука сэкономит вам огромное количество сил, нервов и времени.