RubyDev Ruby Tutorial #6 > Регулярные выражения (Regular Expressions — RegExp) в Ruby

мая 15, 2011  |  Published in Ruby, Основы  |  12 Comments

1. регулярные выражения (Regular Expressions — RegExp) — что это?
Регулярные выражения — это очень мощная концепция, суть которой состоит в создании некоторого  шаблона строки, по которому можно производить поиск, проверку, замену, редактирование, разбиение и т.д. строки или ее подстроки.

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

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

Метасимвол — это символ или несколько символов используемые в шаблоне для определения какой-либо вещи.

В Ruby регулярное выражение является объектом класса Regexp и позволяет любые операции над собой, которые позволяет любой другой объект. Пример самого простого регулярного выражения:

//.class #=> Regexp

Как вы уже догадались, регулярные выражения (шаблоны) заключаются в символы слэшей. Любой набор символов заключенных между прямыми слэшами будет расцениваться интерпретатором Ruby как регулярное выражение. Также существуют другие способы создания регулярных выражений:

%r{regexp here} и Regexp.new(«»)

2. Простые специальные символы (метасимволы)

Самым простым примером регулярного выражения являются простые специальные символы, которые соответствуют самим себе в проверяемых на соответствие шаблону строках. Пример:

/a/ соответствует любая строка содержащая в себе подстроку «а»
/abc/ соответствует любая строка содержащая в себе подстроку «abc»

Здесь все понятно.

3. Проверка соответствия
Для проверки соответствует строка шаблону в Ruby имеется метод match, вместо которого так же можно использовать оператор =~:

/abcd/.match "abcdefgh" #=> #<MatchData "abcd">

/abcd/=~'abcdefgh'

Если строка соответствует шаблону, то метод match возвращает объект MatchData, в противном же случае будет возвращено значение nil, которое интерпретируется как false.

/abcd/.match "bbbbbb" #=> nil

Что же собой представляет объект MatchData? MatchData содержит массив фрагментов строки, которые соответствуют шаблону.

a = /abcd/.match "abcdefgh" #=> #<MatchData "abcd">
puts a #=> abcd

В то время как match возвращает специальный объект, оператор =~ просто возвращает положение символа в строке с которого начинается подстрока соответствующая шаблону если выражение соответствует шаблону и nil если не соответствует.

/abcd/=~'abcdefghabcdabcd' #=> 0
/abcd/=~'hello!' #=> nil

4. Проблемы интерпретации
На самом деле никаких проблем нет, точнее не будет, если вы будете осведомлены о том, что не все метасимволы соответствуют себе же в строках (нет буквального соответствия). Таким символы интерпретируются в иной способ, однако вы може это изменить используя экранирование. Пример:

/./=~"abdrfjkg" #=> 0

/\./=~"abdrfjkg" #=> nil

Как вы догадались в первом случае символ «.» — точка является определенным специальным символом, в то время, как во втором случае благодаря «экрану» — символу обратного слеша » \» мы сообщаем парсеру регулярных выражений о том, что следующий символ следует понимать буквально и благодаря этому второй проверке соответствуют только те строки, которые содержат в себе точку. Вам следует помнить об экранировании некоторых специальных символов список которых я вам представляю: ^, $, ? , ., /, \, [, ], {, }, (, ), +, *

5. Точка

Метасимвол точка соответствует любому символу, кроме символа перевода на новою строку «\n».

/./=~"\n" #=> nil
/./=~"Hello world!" #=> 0

Работая с регулярными выражениями не забывайте о том, чем отличаются одинарные кавычки от двойных:

/./=~'\n' #=> 0

Видите, в отличие от предыдущего примера, этот возвращает 0, а не nil. Все потому, что в данном случае \n не является символом перехода на новую строку, поскольку все, что находится в одинарных кавычках воспринимается «буквально».

6. Символьные классы или символьные коллекции

Символьные классы и символьные коллекции это название одной и той же вещи. Не путайте символьные классы с классами из ООП, здесь под словом «класс» подразумевается слово «сорт».

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

/hello/=~'h' #=> nil
/[hello]/=~'h' #=> 0

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

/[0123456789]/=~ "9" #=> 0
/[0-9]/=~ "9" #=> 0

Согласитесь, второй вариант значительно красивей и не менее понятен.

Вы так же можете использовать диапазоны для всех прописных символов или строчных:

/[A-Z]/=~"ABCD" #=> 0
/[a-z]/=~"xyz" #=> 0
/[a-e]/=~ 'abc' #=> 0
/[1-4]/=~ '3' #=> 0

Вы так же можете использовать сразу несколько диапазонов внутри одного символьного класса:

/[A-Za-z0-9]/=~"abCD97" #=> 0

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

7. Негативный поиск и символ отрицания
Обычно производится проверка соответствия где проверяется содержит ли строка какой-либо символ или последовательность символов. Однако, часто бывает необходимо, проверить чтобы строка, наоборот, не содержала определенного символа или их последовательности. Для этого используется такой метасимвол как ^ (если не ошибаюсь этот символ называется символом вставки). Пример использования:

/[a-y]/=~ "z" #=> nil
/[^a-y]/=~ "z" #=> 0

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

8. Псевдонимы символьных классов (переключатели)
Разработчикам регулярных выражений даже такая форма [0-9] показалась избыточной благодаря чему были созданы псевдонимы для часто используемых символьных классов (это я их так называю, как они насамом деле называются я не помню).

Шаблон /[\d]/ равноценен шаблону /[0-9]/
Шаблон /[\w]/ равноценен шаблону /[A-Za-z0-9_]/
Шаблон /[\s]/ равноценен шаблону /[\s\t\r\n\f]/ — символы «свободного места» — пробел, табуляция, новая строка и т.д.

У каждого псевдонима также имеется полная собственная противоположность:

Шаблон /[\D]/ равноценен шаблону /[^d]/
Шаблон /[\W]/ равноценен шаблону /[^w]/
Шаблон /[\S]/ равноценен шаблону /[^s]/

Запомните, что использование прописной буквы вместо строчной создает отрицание.

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

[:alnum:] Все символы латинского алфавита и цифры ([A-Za-z0-9])
[:alpha:] Только символы латинского алфавита ([A-Za-z])
[:blank:] Пробел или табуляция
[:cntrl:] Управляющий символы
[:digit:] Десятичные цифры
[:graph:] Все печатаемые символы кроме пробела
[:lower:] Строчные символы латинского алфавита
[:print:] Все печатаемые символы
[:punct:] Символы пунктуации, т.е. все кроме символов алфавита, цифр, пробела и непечатемых символов.
[:space:] Символы «свободного места». Сюда входят:
\n - символ новой строки
\r — символ возврата каретки
\t — символ табуляции
\f — символ конца файла

[:upper:] Прописные буквы латинского алфавита
[:xdigit:] Цифры шестнадцетиричной системы исчисления.

9. Повторение и квантификаторы
Из официальной литературы известен термин «квантификатор» — это метасимвол, который используется для указания того сколько раз в проверяемой строке должен содержатся какой-нибудь символ или группа символов.

Метасимволы  квантификаторы с пояснением и примером:

? — символ или группа входит в строку либо один раз, либо вообще не входят.

/a?/=~"bbb" #=> 0
/a?/=~"aaa" #=> 0 

* — символ или группа входит в строку любое количество раз (в том числе ниразу).

/(aaa)*/=~"b" #=> 0 
/(aaa)*/=~"aaa" #=> 0 
 

+ — вхождение символа или группы минимум один раз

/a+/=~"bbb" #=> nil
/a+/=~"bba" #=> 2

{n} - Вхождение символа или группы в строку n-раз.

/Hello!{2}/=~"Hello!Hello!" #=> nil
/(Hello!){2}/=~"Hello!Hello!" #=> 0
/(Hello!){2}/=~"Hello!" #=> nil

Посмотрите на 1 и 3, квантификатор {n} не правильно работает если метасимволы не заключены в круглые или квадратные скобки. Если вы не используете символьный класс, то всегда группируйте метасимволы при помощи круглых скобок!

{n,} — предыдущий символ или группа входит в строку n и более раз:

/(Hello!){1,}/=~"Hello!" #=> 0
/(Hello!){2,}/=~"Hello!Hello!" #=> 0
/(Hello!){2,}/=~"Hello!" #=> nil

{n,m} — предыдущий символ или группа входит в строку от n до m раз:

/(Hello!){2,3}/=~"Hello!" #=> nil
/(Hello!){2,3}/=~"Hello!Hello!" #=> 0
/(Hello!){2,3}/=~"Hello!Hello!Hello!" #=> 0

10. Объединение или группирование
Выше уже было упоминание о группах, в этом разделе рассказывается подробнее, чтоже это такое. Шаблоны регулярных выражений стараются повторить структуру проверяемых строк. Например номер телефона состоит из кода страны, кода города, номера АТС или кода провайдера мобильной связи и собственно номера телефона. некоторые группы объединяются, например номер АТС и номер телефона, а некоторые отделаются всегда: код страны. Для группирования метасимволов и соответственно разделения шаблона на группы используются круглые скобки ().

Вот, например, номер какого-нибудь украинского телефонного номера с отделением кода страны от собственно номера:

/\+([\d]){3}-([\d]){9}/=~"+380-123456789" #=> 0

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

md = /([\d]){1}([\d]){1}([\d]){1}/.match "123" #=> #<MatchData "123" 1:"1" 2:"2" 3:"3">
print md.to_a #=> ["123", "1", "2", "3"] => nil

md = /([\d]){2}([\d]){3}([\d]){4}/.match "123456789" #=> #<MatchData "123456789" 1:"2" 2:"5" 3:"9">
md.to_a #=> ["123456789", "2", "5", "9"]

Как не сложно догадаться объект MatchData хранит не только подстроки, которые соответствует шаблону, но и позиции начала следующей группы. Сложновато, да? В примере выше цифры 2, 5 означают порядок символов с которых начинается следующая граппа, цифра 9 указывает последний символ (или количество символов) в подстроке. По эти данным вы можете разбить подстроку на другие подстроки каждая из которых соответствует определенной группе в шаблоне, однако такое разбитие почти никогда не делается при помощи метода match, так как для этого существуют более удобные средства.

11. Метасимвол «|» — вариативность
Часто бывает необходимо проверить соответствие строки шаблону регулярного выражения при чем проверка необходима не строгая, но в пределах предложенных вариантов (иначе какая же это проверка?).

/(Sasha|Seroja)/=~ "Sasha" #=> 0
/(Sasha|Seroja)/=~ "Seroja" #=> 0
/(Sasha|Seroja)/=~ "Vova" #=> nil

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

//regexp/|/regexp/|/regexp//

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

12. Начало и конец строки
До сих пор мы писали глупые шаблоны регулярных выражений, которые нельзя использовать для проверки правильности вводимой пользователем информации. Я наверное погорячился назвав их глупыми, на самомделе они очень полезные при парсинге, однако для проверки вводимой пользователем информации совсем не годятся: шаблону /a/ может соответствовать любая строка содержащая подстроку «a», а шаблону /(\d){3}/ может соответствовать любая строка содержащая 3 подряд идущих цыфры.

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

Указателем начала строки является метасимвол ^, который также означает начало новой внутренней строки. Здесь под внутренней строкой с подразумеваю строку в привычном понимании этого слова, а не как объект класса String. Строки разделяются между собой символом новой строки \n, поэтому строка как экземпляр класса String может содержать множество внутренних строк. Конец строки или внутренней строки можно обозначить метасимволом $. Пример использования метасимволов — определителей начала и конца строки:

/^(H)/=~"Hello word" #=> 0

/(H)$/=~"Hello word" #=> nil

/(H)$/=~"Hello word".reverse #=> 9

/^(H)$/=~"H" #=> 0

Вы так же можете использовать для определения начала и конца строки и внутренней строки метасимволы: \A и \Z, пример:

/\A(H)\Z/=~"H" #=> 0
/\A(H)\Z/=~"Haaa!" #=> nil

Стоит отметить, что \A,\Z и ^,$ — не одно и то же. \A и \Z обозначают конец вообще всего текста, который проверяется регуляным выражением, а ^ и $ только внутренних подстрок, которые разделены символом новой строки: \n (спасибо комментатору Roger).

«asd\na» =~ /^asd$/ #=> 0
«asd\na» =~ /\Aasd\Z/ #=> nil
«asd\na» =~ /^a$/ #=> 4
«asd\na» =~ /\Aasd\na\Z/ #=> 0

Важно: не забывайте, что символ вставки «^» означает отрицание только внутри квадратных скобок.

Вы можете не верить, но вы уже способны написать более-менее полезное регулярное выражение! Осталось еще совсем чуть-чуть, чтобы вы стали настоящим RegExp — гуру =)

13. «Жадный» и «не жадный» поиск
Мы уже познакомились с метасимволами * и +. Однако вам еще не известно об их так называемой «не жадной» форме. * и + ищут в строке строку максимального размера, которая соответствует шаблону, однако «не жадная» форма позволяет искать подстроку минимальной длины. Чтобы все это стало понятнее я привожу сравнительный пример «жадной» и «не жадной» форм. Для сосдания «не жадной» формы мосле метасимволов * и + следует добавить ?, таким образом мы получаем новые — «не жадные» метасимволы *? и +?:

/\!([A-Z\!])*\!/.match "!HELLO!HELLO!" #=> #<MatchData "!HELLO!HELLO!" 1:"O">

/\!([A-Z\!])*?\!/.match "!HELLO!HELLO!" #=> #<MatchData "!HELLO!" 1:"O">

14. Ссылки в регулярных выражениях
Регулярные выражения имеют что-то похожее на переменные, что называется ссылками (обратными ссылками). Просто так на словах сложно объяснить что это, поэтому приведу пример:

/(sweet) girl likes \1s/.match "sweet girl likes sweets" #=> #<MatchData "sweet girl likes sweets" 1:"sweet">

Здесь «\1″ является ссылкой на содержимое первых круглых скобок, то есть содержимое первой группы. Честно говоря, я очень редко встречал когда этим можно воспользоваться, еще реже, когда это применялось.

15. Переменные Ruby в регулярных выражениях
Очень полезной возможностью является возможность вставки в регуляное выражение переменных языка Ruby. Вставка переменных осуществляется в тот же способ, что для строк:

string = "My phone number is +380 962870430."

country_code = /(\+[\d]{3})/
number = /( [0-9]{1,})/
phone_number = /#{country_code}#{number}/

p string.scan(country_code) #=> [["+380"]]
p string.scan(number) #=> [[" 962870430"]]
p string.scan(phone_number) #=> [["+380", " 962870430"]]

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

articles = [
"PPPPPPPPPPDDDDDDDDDD",
"PPPPPPPPPPDDDDDDDDDDRRRRRRRRRR",
"DDDDDDDDDDPPPPP",
"DDDDDDDDDDPPPPPSSSSS",
"DDDDDDDDDDSSSSSPPP"]

keyword = "P"
relevant = []

articles.each do |a|
  including = a.scan(/(#{keyword}){1}/).size
  total_size = a.size
  relevant << {article: a, relevance: including/total_size.to_f*100}
end

Как видите, применение достаточно широкое и очень полезное!

Полезные ссылки:
— онлайн редактор для проверки регулярных выражений. (Спасибо за ссылку комментатору ).

Лучшая благодарность автору — ваши комментарии!

Благодарю комментаторов за указания на ошибки и неточности!

Tags: , ,

Responses

  1. Гонзих says:

    мая 15, 2011 at 15:35 (#)

    никогда не пользовался MatchData для выдергивания результатов группировки, обычно пользуюсь глобальными переменными $n где n — номер группы начиная с 1

  2. Гонзих says:

    мая 15, 2011 at 15:43 (#)

    ах да, ещё в 1.9 вроде как новый движок регулярок, который умеет делать заглядывания вперед и назад
    /some(?= other)/ позитивное заглядывание вперед
    /some(?! other)/ негативное заглядывание вперед
    результат заглядывание не попадает в MatchData

    ещё вариант получения результата группировки
    match = /(one)/.match «one two three»
    match.to_a[0] # => ‘one’

  3. says:

    мая 15, 2011 at 15:47 (#)

    Неплохо, в 13м пункте можно добавить описания глобальных переменных $1 .. $n и $&

  4. exromany says:

    мая 15, 2011 at 16:38 (#)

    Символьные классы и символьные коллекции – это [b]то[/b] название одной и той же вещи.
    Исправьте.

  5. Meredian says:

    мая 15, 2011 at 17:27 (#)

    Переменные $1, $2 .. $n содержат подстроки, сматченные при последнем использовании регэкспа, в соответствии с номером группы.

    К слову, класс слов, разпознаваемым регулярным выражением, совпадает с классом слов, распознаваемых конечныхм автоматом (если где-то возникнет непонимание, можно обратиться к теории, почитать)

  6. says:

    мая 15, 2011 at 18:09 (#)

    Вот еще полезная тулза — онлайн редактор регулярных выражений

  7. Rodger says:

    мая 15, 2011 at 19:32 (#)

    ^$ и \A\Z — не одно и тоже. ^$ — начало и конец строки, \A\Z — начало и конец переданого текста. с этим кстати связан набор уязвимостей — обход валидации.

    > «asd\na» =~ /^asd$/ #=> 0
    > «asd\na» =~ /\Aasd\Z/ #=> nil
    > «asd\na» =~ /^a$/ #=> 4
    > «asd\na» =~ /\Aasd\na\Z/ #=> 0

  8. Михаил says:

    мая 16, 2011 at 09:29 (#)

    Исправь, пожалуйста.
    * – символ или группа входит в строку 0 или более количество раз. Во всех версиях и даже в 1.9.2
    Просто пример, который ты привел — не правильный: /aaa*/=~»a» — это означает, что ищем минимум 2 буквы «а», поэтому nil и возвращается.
    А пример: /aaa*/=~»aa» — более правильный, вернет 0

  9. rezwyi says:

    мая 16, 2011 at 19:34 (#)

    В целом не ново для меня, однако про «жадный» и «нежадный» поиски слышу впервые. Спасибо.

    Все очень доступно и познавательно.

  10. nef says:

    мая 16, 2011 at 22:51 (#)

    Спасибо автору, все изложено очень доступно :-) По теме рельс — очень бы хотелось пару статей про тестирование, которые не остановятся на rspec:install/пишем первый тест :-) RSpec, Capybara, Selenium, Jasmine…

  11. admin says:

    мая 17, 2011 at 17:32 (#)

    Всем спасибо за «спасибо» и пожалуйста!

    Алексей, спасибо зассылку, добавлю ее в конец поста.

    Михаил, спасибо за найденную ошибку, поправил, сам с регулярками запутался=)

    Rodger, спасибо за существенную подсказку. Только что проверил код одного своего проекта и поправил ошибки.

    nef, к сожалениютема тестирования мне не очень интересна, но об RSpec обязательно будет большая статья. Capybara, Selenium и Jasmine пока совсем не интересны, хотя, в будущем не исключаю возможности их изучения и написания статей.

  12. v_m_smith says:

    января 11, 2012 at 19:08 (#)

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

    puts 'boldnew div'.scan( /(.*)/ )

    Но правда для встроенных тегов это уже не работает :/

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.