Программирование на языке Ruby

Фултон Хэл

Глава 3. Регулярные выражения

 

 

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

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

Во время работы над данной книгой язык Ruby находился в переходном состоянии. Старая библиотека регулярных выражений заменялась новой под названием Oniguruma. Этой библиотеке посвящен раздел 3.13 данной главы. Что касается интернационализации, то это тема главы 4.

 

3.1. Синтаксис регулярных выражений

Обычно регулярное выражение ограничено с двух сторон символами косой черты. Применяется также форма %r. В таблице 3.1 приведены примеры простых регулярных выражений:

Таблица 3.1. Простые регулярные выражения

Регулярное выражение Пояснение
/Ruby/ Соответствует одному слову Ruby
/[Rr]uby/ Соответствует Ruby или ruby
/^abc/ Соответствует abc в начале строки
%r(xyz$) Соответствует xyz в конце строки
%r|[0-9]*| Соответствует любой последовательности из нуля или более цифр

Сразу после регулярного выражения можно поместить однобуквенный модификатор. В таблице 3.2 приведены наиболее часто употребляемые модификаторы.

Таблица 3.2. Модификаторы регулярных выражений

Модификатор Назначение
I Игнорировать регистр
O Выполнять подстановку выражения только один раз
M Многострочный режим (точка сопоставляется с символом новой строки)
X Обобщенное регулярное выражение (допускаются пробелы и комментарии)

Дополнительные примеры будут рассмотрены в главе 4. Чтобы завершить введение в регулярные выражение, в таблице 3.3 мы приводим наиболее употребительные символы и обозначения.

Таблица 3.3. Общеупотребительные обозначения в регулярных выражениях

Обозначение Пояснение
^ Начало строки текста (line) или строки символов (string)
$ Конец строки текста или строки символов
. Любой символ, кроме символа новой строки (если не установлен многострочный режим)
\w Символ - часть слова (цифра, буква или знак подчеркивания)
\W Символ, не являющийся частью слова
\s Пропуск (пробел, знак табуляции, символ новой строки и т.д.)
\S Символ, не являющийся пропуском
\d Цифра (то же, что [0-9])
\D Не цифра
\A Начало строки символов (string)
\Z Конец строки символов или позиция перед конечным символом новой строки
\z Конец строки символов (string)
\b Граница слова (только вне квадратных скобок [ ])
\B Не граница слова
\b Забой (только внутри квадратных скобок [ ])
[] Произвольный набор символов
* 0 или более повторений предыдущего подвыражения
*? 0 или более повторений предыдущего подвыражения (нежадный алгоритм)
+ 1 или более повторений предыдущего подвыражения
+? 1 или более повторений предыдущего подвыражения (нежадный алгоритм)
{m, n} От m до n вхождений предыдущего подвыражения
{m, n}? От m до n вхождений предыдущего подвыражения (нежадный алгоритм)
? 0 или 1 повторений предыдущего подвыражения
| Альтернативы
(?= ) Позитивное заглядывание вперед
(?! ) Негативное заглядывание вперед
() Группировка подвыражений
(?> ) Вложенное подвыражение
(?: ) Несохраняющая группировка подвыражений
(?imx-imx) Включить/выключить режимы, начиная с этого места
(?imx-imx: expr) Включить/выключить режимы для этого выражения
(?# ) Комментарий

Умение работать с регулярными выражениями — большой плюс для современного программиста. Полное рассмотрение этой темы выходит далеко за рамки настоящей книги, но, если вам интересно, можете обратиться к книге Jeffrey Friedl, Mastering Regular Expressions.

Дополнительный материал вы также найдете в разделе 3.13.

 

3.2. Компиляция регулярных выражений

Для компиляции регулярных выражений предназначен метод Regexp.compile (синоним Regexp.new). Первый параметр обязателен, он может быть строкой или регулярным выражением. (Отметим, что если этот параметр является регулярным выражением с дополнительными флагами, то флаги не будут перенесены в новое откомпилированное выражение.)

pat1 = Regexp.compile("^foo.*") # /^foo.*/

pat2 = Regexp.compile(/bar$/i)  # /bar/ (i не переносится)

Если второй параметр задан, обычно это поразрядное объединение (ИЛИ) каких-либо из следующих констант: Regexp::EXTENDED, Regexp::IGNORECASE, Regexp::MULTILINE. При этом любое отличное от nil значение приведет к тому, что регулярное выражение не будет различать регистры; мы рекомендуем опускать второй параметр.

options = Regexp::MULTILINE || Regexp::IGNORECASE

pat3 = Regexp.compile("^foo", options)

pat4 = Regexp.compile(/bar/, Regexp::IGNORECASE)

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

"N" или "n" означает отсутствие поддержки

"Е" или "е" означает EUC

"S" или "s" означает Shift-JIS

"U" или "u" означает UTF-8

Литеральное регулярное выражение можно задавать и не вызывая метод new или compile. Достаточно заключить его в ограничители (символы косой черты).

pat1 = /^fоо.*/

pat2 = /bar$/i

Более подробная информация приводится в главе 4.

 

3.3. Экранирование специальных символов

Метод класса Regexp.escape экранирует все специальные символы, встречающиеся в регулярном выражении. К их числу относятся звездочка, вопросительный знак и квадратные скобки.

str1 = "[*?]"

str2 = Regexp.escape(str1) # "\[\*\?\]"

Синонимом является метод Regexp.quote.

 

3.4. Якоря

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

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

string = "abcXdefXghi"

/def/ =~ string  # 4

/аbс/ =~ string  # 0

/ghi/ =~ string  # 8

/^def/ =~ string # nil

/def$/ =~ string # nil

/^аbс/ =~ string # 0

/ghi$/ =~ string # 8

Впрочем, я немного уклонился от истины. Эти якоря на самом деле соответствуют началу и концу не строки символов (string), а строки текста (line). Вот что произойдет, если те же самые образцы применить к строке, внутри которой есть символы новой строки:

string = "abc\ndef\nghi"

/def/ =~ string  # 4

/abc/ =~ string  # 0

/ghi/ =~ string  # 8

/^def/ =~ string # 4

/def$/ =~ string # 4

/^abc/ =~ string # 0

/ghi$/ =~ string # 8

Однако имеются якоря \A и \Z, которые соответствуют именно началу и концу самой строки символов.

string = "abc\ndef\nghi"

/\Adef/ =~ string # nil

/def\Z/ =~ string # nil

/\Aabc/ =~ string # 0

/ghi\Z/ =~ string # 8

Якорь \z отличается от \Z тем, что последний устанавливает соответствие перед конечным символом новой строки, а первый должен соответствовать явно.

string = "abc\ndef\nghi"

str2 << "\n"

/ghi\Z/ =~ string # 8

/\Aabc/ =~ str2   # 8

/ghi\z/ =~ string # 8

/ghi\z/ =~ str2   # nil

Можно также устанавливать соответствие на границе слова с помощью якоря \b или с позицией, которая не находится на границе слова (\B). Примеры использования метода gsub показывают, как эти якоря работают:

str = "this is a test"

str.gsub(/\b/,"|") # "|this| |is| |a| |test|"

str.gsub(/\В/, "-") # "t-h-i-s i-s a t-e-s-t"

He существует способа отличить начало слова от конца.

 

3.5. Кванторы

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

pattern = /ax?b/

pat2 = /а[xy]?b/

pattern =~ "ab"  # 0

pattern =~ "acb" # nil

pattern =~ "axb" # 0

pat2 =~ "ayb"    # 0

pat2 =~ "acb"    # nil

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

pattern = /[0-9]+/

pattern =~ "1"       # 0

pattern =~ "2345678" # 0

Еще один типичный случай — образец, повторяющийся нуль или более раз. Конечно, это условие можно выразить с помощью кванторов + и ?. Вот, например, как сказать, что после строки Huzzah должно быть нуль или более восклицательных знаков:

pattern = /Huzzah(!+)?/ # Скобки здесь обязательны.

pattern =~ "Huzzah"     # 0

pattern =~ "Huzzah!!!!" # 0

Но есть и способ лучше. Требуемое поведение описывается квантором *.

pattern = /Huzzah!*/    # * применяется только к символу !

pattern =~ "Huzzah"     # 0

pattern =~ "Huzzah!!!!" # 0

Как распознать американский номер социального страхования? С помощью такого образца:

ssn = "987-65-4320"

pattern = /\d\d\d-\d\d-\d\d\d\d/

pattern =~ ssn # 0

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

pattern = /\d{3}-\d{2}-\d{4}/

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

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

elbonian_phone = /\d{3,5}-\d{3,7}/

Нижняя и верхняя границы диапазона необязательны (но хотя бы одна должна быть задана):

/x{5}/   # Соответствует 5 x.

/x{5,7}/ # Соответствует 5-7 x.

/x{,8}/  # Соответствует не более 8 x.

/x{3,}/  # Соответствует по меньшей мере 3 x.

Ясно, что кванторы ?, + и * можно переписать и так:

/x?/ # То же, что /x{0,1}/

/x*/ # То же, что /x{0,}

/x+/ # то же, что /x{1,}

Фразеология, применяемая при описании регулярных выражений, изобилует яркими терминами: жадный (greedy), неохотный (reluctant), ленивый (lazy) и собственнический (possessive). Самым важным является различие между жадными и нежадными выражениями.

Рассмотрим следующий фрагмент кода. На первый взгляд, это регулярное выражение должно сопоставляться со строкой "Where the", но на самом деле ему соответствует более длинная подстрока "Where the sea meets the":

str = "Where the sea meets the moon-blanch'd land,"

match = /.*the/.match(str)

p match[0] # Вывести полученное соответствие:

           # "Where the sea meets the"

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

str = "Where the sea meets the moon-blanch'd land,"

match = /.*?the/.match(str)

p match[0] # Вывести полученное соответствие:

           # "Where the" .

Итак, оператор * жадный, если за ним не стоит ?. То же самое относится к кванторам + и {m,n} и даже к самому квантору ?.

Я не сумел найти разумных примеров применения конструкций {m,n}? и ??. Если вам о них известно, пожалуйста, поделитесь со мной своим опытом.

Дополнительная информация о кванторах содержится в разделе 3.13.

 

3.6. Позитивное и негативное заглядывание вперед

Понятно, что регулярное выражение сопоставляется со строкой линейно (осуществляя при необходимости возвраты). Поэтому существует понятие «текущего положения» в строке, это аналог указателя файла или курсора.

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

В следующем примере строка "New world" будет сопоставлена, если за ней следует одна из строк "Symphony" или "Dictionary". Однако третье слово не будет частью соответствия.

s1 = "New World Dictionary"

s2 = "New World Symphony"

s3 = "New World Order"

reg = /New World(?= Dictionary | Symphony)/

m1 = reg.match(s1)

m.to_a[0]          # "New World"

m2 = reg.match(s2)

m.to_a[0]          # "New World"

m3 = reg.match(s3) # nil

Вот пример негативного заглядывания:

reg2 = /New World(?! Symphony)/

m1 = reg.match(s1)

m.to_a[0]          # "New World"

m2 = reg.match(s2)

m.to_a[0]          # nil

m3 = reg.match(s3) # "New World"

В данном случае строка "New world" подходит, только если за ней не следует строка "Symphony".

 

3.7. Обратные ссылки

Каждая заключенная в круглые скобки часть регулярного выражения является отдельным соответствием. Они нумеруются, и есть несколько способов сослаться на такие части по номерам. Сначала рассмотрим традиционный «некрасивый» способ.

Сослаться на группы можно с помощью глобальных переменных $1, $2 и т.д:

str = "а123b45с678"

if /(a\d+)(b\d+)(c\d+)/ =~ str

 puts "Частичные соответствия: '#$1', '#$2', '#$3'"

 # Печатается: Частичные соответствия: 'а123', 'b45', 'c768'

end

Эти переменные нельзя использовать в подставляемой строке в методах sub и gsub:

str = "а123b45с678"

str.sub(/(a\d+)(b\d+)(c\d+)/, "1st=#$1, 2nd=#$2, 3rd=#$3")

# "1st=, 2nd=, 3rd="

Почему такая конструкция не работает? Потому что аргументы sub вычисляются перед вызовом sub. Вот эквивалентный код:

str = "а123b45с678"

s2 = "1st=#$1, 2nd=#$2, 3rd=#$3"

reg = /(a\d+)(b\d+)(c\d+)/

str.sub(reg,s2)

# "1st=, 2nd=, 3rd="

Отсюда совершенно понятно, что значения $1, $2, $3 никак не связаны с сопоставлением, которое делается внутри вызова sub.

В такой ситуации на помощь приходят специальные коды \1, \2 и т.д.:

str = "а123b45с678"

str.sub(/(a\d+)(b\d+)(c\d+)/, '1st=\1, 2nd=\2, 3rd=\3')

# "1st=a123, 2nd=b45, 3rd=c768"

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

str = "а123b45с678"

str.sub(/(a\d+)(b\d+)(c\d+)/, "1st=\1, 2nd=\2, 3rd=\3")

# "1st=\001, 2nd=\002, 3rd=\003"

Обойти эту неприятность можно за счет двойного экранирования:

str = "а123b45с678"

str.sub(/(a\d+)(b\d+)(c\d+)/, "1st=\\1, 2nd=\\2, 3rd=\\3")

# "1st=a123, 2nd=b45, 3rd=c678"

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

str = "а123b45с678"

str.sub(/(a\d+)(b\d+)(c\d+)/) { "1st=#$1, 2nd=#$2, 3rd=#$3" }

# "1st=a123, 2nd=b45, 3rd=c678"

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

Упомяну попутно о том, что существуют незапоминаемые группы (noncapturing groups). Иногда при составлении регулярного выражения нужно сгруппировать символы, но чему будет соответствовать в конечном счете такая группа, несущественно. На этот случай и предусмотрены незапоминаемые группы, описываемые синтаксической конструкцией (?:...):

str = "а123b45с678"

str.sub(/(a\d+)(?:b\d+)(c\d+)/, "1st=\\1, 2nd=\\2, 3rd=\\3")

# "1st=a123, 2nd=c678, 3rd="

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

Лично мне не нравится ни одна из двух нотаций (\1 и $1). Иногда они удобны, но никогда не бывают необходимы. Все можно сделать «красивее», в объектно-ориентированной манере.

Метод класса Regexp.last_match возвращает объект класса MatchData (как и метод экземпляра match). У этого объекта есть методы экземпляра, с помощью которых программист может получить обратные ссылки.

Обращаться к объекту MatchData можно с помощью квадратных скобок, как если бы это был массив соответствий. Специальный элемент с индексом 0 содержит текст всей сопоставляемой строки, а элемент с индексом n ссылается на n-ую запомненную группу:

pat = /(. + [aiu])(.+[aiu])(.+[aiu])(.+[aiu])/i

# В этом образце есть четыре одинаковых группы.

refs = pat.match("Fujiyama")

# refs is now: ["Fujiyama","Fu","ji","ya","ma"]

x = refs[1]

y = refs[2..3]

refs.to_a.each {|x| print "#{x}\n"}

Отметим, что объект refs — не настоящий массив. Поэтому, если мы хотим обращаться с ним как с таковым, применяя итератор each, следует сначала преобразовать его в массив с помощью метода to_a (как показано в примере).

Есть и другие способы нахождения сопоставленной подстроки внутри исходной строки. Методы begin и end возвращают смещения начала и конца соответствия. (Важно понимать, что смещение конца — это индекс символа, следующего за найденным соответствием.)

str = "alpha beta gamma delta epsilon"

#      0....5....0....5....0....5....

#      (для удобства подсчета)

pat = /(b[^ ]+ )(g[^ ]+ )(d[^ ]+ )/

# Три слова, каждое из которых представляет собой отдельное соответствие.

refs = pat.match(str)

# "beta "

p1 = refs.begin(1) # 6

p2 = refs.end(1)   # 11

# "gamma "

p3 = refs.begin(2) # 11

p4 = refs.end(2)   # 17

# "delta "

p5 = refs.begin(3) # 17

p6 = refs.end(3)   # 23

# "beta gamma delta"

p7 = refs.begin(0) # 6

p8 = refs.end(0)   # 23

Аналогично метод offset возвращает массив из двух чисел: смещение начала и смещение конца соответствия. Продолжим предыдущий пример:

range0 = refs.offset(0) # [6,23]

range1 = refs.offset(1) # [6,11]

range2 = refs.offset(2) # [11,17]

range3 = refs.offset(3) # [17,23]

Части строки, которые находятся перед сопоставленной подстроки и после нее, можно получить методами pre_match и post_match соответственно. В том же коде:

before = refs.pre_match # "alpha "

after = refs.post_match # "epsilon"

 

3.8. Классы символов

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

/[aeiou]/ # Соответствует любой из букв а, е, i, о, и; эквивалентно

          # /(a|e|i|o|u)/, только группа не запоминается.

Внутри класса символов управляющие последовательности типа \n по-прежнему распознаются, но такие метасимволы, как . и ?, не имеют специального смысла:

/[.\n?]/ # Сопоставляется с точкой, символом новой строки,

         # вопросительным знаком.

Символ каре (^) внутри класса символов имеет специальный смысл, если находится в начале; в этом случае он формирует дополнение к списку символов:

[^aeiou] # Любой символ, КРОМЕ а, е, i, о, и.

Дефис внутри класса символов обозначает диапазон (в лексикографическом порядке):

/[а-mA-М]/  # Любой символ из первой половины алфавита.

/[^а-mA-М]/ # Любой ДРУГОЙ символ, а также цифры и символы. отличные

            # от букв и цифр.

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

/[-^[\]]/ # Сопоставляется с дефисом, каре и правой квадратной скобкой.

Регулярные выражения в Ruby могут содержать ссылки на именованные классы символов вида [[:name:]]. Так, [[:digit:]] означает то же самое, что образец [0-9]. Во многих случаях такая запись оказывается короче или, по крайней мере, понятнее.

Есть еще такие именованные классы: [[:print:]] (символы, имеющие графическое начертание) и [[:alpha:]] (буквы):

s1 = "abc\007def"

/[[:print:]]*/.match(s1)

m1 = Regexp::last_match[0] # "abc"

s2 = "1234def"

/[[:digit:]]*/.match(s2)

m2 = Regexp::last_match[0] # "1234"

/[[:digit:]] + [[:alpha:]]/.match(s2)

m3 = Regexp::last_match[0] # "1234d"

Каре перед именем класса символов формирует его дополнение:

/[[:^alpha:]]/ # Все символы, кроме букв.

Для многих классов имеется также сокращенная нотация. Наиболее распространены сокращения \d (любая цифра), \w (любой символ, входящий в состав «слова») и \s (пропуски — пробел, знак табуляции или новой строки):

str1 = "Wolf 359"

/\w+/.match(str1)     # Соответствует "Wolf" (то же, что /[a-zA-Z_0-9]+/)

/\w+ \d+/.match(str1) # Соответствует "Wolf 359"

/\w+ \w+/.match(str1) # Соответствует "Wolf 359"

/\s+/.match(str1)     # Соответствует " "

«Дополнительные» формы обычно записываются в виде прописной буквы:

/\W/ # Любой символ, не входящий в состав слова.

/\D/ # Все кроме цифр.

/\S/ # Все кроме пропусков.

Дополнительная информация, относящаяся только к Oniguruma, приводится в разделе 3.13.

 

3.9. Обобщенные регулярные выражения

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

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

addresses =

[ "409 W Jackson Ave",           "No. 27 Grande Place",

  "16000 Pennsylvania Avenue",   "2367 St. George St.",

  "22 Rue Morgue",               "33 Rue St. Denis",

  "44 Rue Zeeday",               "55 Santa Monica Blvd.",

  "123 Main St., Apt. 234",      "123 Main St., #234",

  "345 Euneva Avenue, Suite 23", "678 Euneva Ave, Suite A"]

Здесь каждый адрес состоит из трех частей: номер дома, название улицы и необязательный номер квартиры. Я предполагаю, что перед числом может быть необязательная строка No., а точку в ней можно опускать. Еще предположим, что название улицы может включать символы, обычно входящие в состав слова, а также апостроф, дефис и точку. Наконец, если адрес содержит необязательный номер квартиры, то ему должны предшествовать запятая и одна из строк Apt., Suite или # (знак номера).

Вот какое регулярное выражение я составил для разбора адреса. Обратите внимание, насколько подробно оно прокомментировано (может быть, даже излишне подробно):

regex = / ^                 # Начало строки.

         ((No\.?)\s+)?      # Необязательно: No[.]

         \d+ \s+            # Цифры и пробелы.

         ((\w|[.'-])+       # Название улицы... может

          \s*               # состоять из нескольких слов.

         )+

         (,\s*              # Необязательно: запятая и т.д.

          (Apt\.?|Suite|\#) # Apt[.], Suite, #

          \s+               # Пробелы.

          (\d+|[A-Z])       # Цифры или одна буква.

         )?

         $                  # Конец строки.

        /x

Идея понятна. Когда сложность регулярного выражения достигает некоего порога (какого именно — дело вкуса), делайте его обобщенным, чтобы можно было добавить форматирование и комментарии.

Возможно, вы заметили, что я пользовался обычными комментариями Ruby (# ...), а не специальными, применяемыми в регулярных выражениях ((?#...)). Почему? Просто потому, что это разрешено! Специальный комментарий необходим только тогда, когда его следует закончить раньше конца строки (например, если в той же строке за комментарием продолжается регулярное выражение).

 

3.10. Сопоставление точки символу конца строки

Обычно точка соответствует любому символу, кроме конца строки. Если задан модификатор многострочности m, точка будет сопоставляться и с этим символом. Другой способ — задать флаг Regexp::MULTILINE при создании регулярного выражения:

str = "Rubies are red\nAnd violets are blue.\n"

pat1 = /red./

pat2 = /red./m

str =~ pat1 # nil

str =~ pat2 # 11

Этот режим не оказывает влияния на то, где устанавливается соответствие якорям (^, $, \A, \Z). Изменяется только способ сопоставления с точкой.

 

3.11. Внутренние модификаторы

Обычно модификаторы (например, i или m) задаются после регулярного выражения. Но что если мы хотим применить модификатор только к части выражения?

Существует специальная нотация для включения и выключения модификаторов. Заключенный в круглые скобки вопросительный знак, за которым следует один или несколько модификаторов, «включает» их до конца регулярного выражения. А если некоторым модификаторам предшествует минус, то соответствующие режимы «выключаются»:

/abc(?i)def/     # Соответствует abcdef, abcDEF, abcDef,

                 # но не ABCdef.

/ab(?i)cd(?-i)ef/# Соответствует abcdef, abCDef, abcDef, ...,

                 # но не ABcdef или abcdEF.

/(?imx).*/       # To же, что /.*/imx

/abc(?i-m).*/m   # Для последней части регулярного выражения включить

                 # распознавание регистра, выключить многострочный

                 # режим.

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

/ab(?i:cd)ef/ # То же, что /ab(?i)cd(?-i)ef/

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

 

3.12. Внутренние подвыражения

Для указания подвыражений применяется нотация ?>:

re = /(?>abc)(?>def)/   # То же, что /abcdef/

re.match("abcdef").to_a # ["abcdef"]

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

Еще обратим внимание на то, что эта конструкция собственническая, то есть жадная и при этом не допускает возврата в подвыражение.

str = "abccccdef"

re1 = /(abc*)cdef/

re2 = /(?>abc*)cdef/

re1 =~ str          # 0

re2 =~ str          # nil

re1.match(str).to_a # ["abccccdef", "abccc"]

re2.match(str).to_a # []

В предыдущем примере подвыражение abc* выражения re2 поглощает все вхождения буквы с и (в соответствии с собственническим инстинктом) не отдает их назад, препятствуя возврату.

 

3.13. Ruby и Oniguruma

 

Новая библиотека регулярных выражений в Ruby называется Oniguruma. Это японское слово означает что-то вроде «колесо духов». (Те, кто не владеет японским, часто пишут его неправильно; имейте в виду, что тут не обойтись без «guru»!)

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

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

 

3.13.1. Проверка наличия Oniguruma

Если вас интересует библиотека Oniguruma, то первым делом нужно выяснить, есть ли она в вашем экземпляре Ruby. В версиях 1.8.4 и младше ее, скорее всего, нет. Стандартно она включается в дистрибутив версии 1.9.

Вот как можно без труда выяснить, присутствует ли Oniguruma, проверив три условия. Во-первых, как я сказал, она стандартно поставляется в версии 1.9 и старше. В последних версиях обеих библиотек для работы с регулярными выражениями определена строковая константа Regexp::ENGINE. Если она содержит подстроку Oniguruma, то у вас новая библиотека. И последний шаг: если вы все еще не знаете, с какой библиотекой работаете, можно попытаться вычислить регулярное выражение, записанное в «новом» синтаксисе. Если при этом возникнет исключение SyntaxError, значит, у вас старая библиотека; в противном случае — новая.

def oniguruma?

 return true if RUBY_VERSION >= "1.9.0"

 if defined?(Regexp::ENGINE) # Константа ENGINE определена?

  if Regexp::ENGINE.include?('Oniguruma')

   return true               # Какая-то версия Oniguruma.

  else

   return false              # Старая библиотека,

  end

 end

 eval("/(?

  return true                # Сработало: новая библиотека.

 rescue SyntaxError          # Не сработало: старая библиотека.

  return false

 end

puts oniguruma?

 

3.13.2. Сборка Oniguruma

Если в вашу версию библиотека Oniguruma не включена, можете самостоятельно откомпилировать Ruby и скомпоновать с недостающей библиотекой. Ниже приведены соответствующие инструкции. Эта процедура должна работать начиная с версии 1.6.8 (хотя она уже совсем старенькая).

Получить исходный текст Oniguruma можно из архива приложений Ruby RAA (http://raa.ruby-lang.org/) или найти в другом месте. Исходные тексты Ruby, естественно, находятся на официальном сайте.

Если вы работаете на платформе UNIX (в том числе в среде Cygwin в Windows или Mac OS/X), выполните следующие действия:

1. gunzip oniguruma.tar.gz

2. tar xvf oniguruma.tar

3. cd oniguruma

4. ./configure with-rubydir=

5. Одно из следующих:

make 16 # Для Ruby 1.6.8

make 18 # Для Ruby 1.8.0/1.8.1

6. cd ruby-source-dir

7. ./configure

8. make clean

9. make

10. make test # Простой тест интерпретатора Ruby.

11. cd ../oniguruma # Укажите путь к библиотеке.

12. make rtest

Или:

make rtest RUBYDIR=ruby-install-dir

Если же вы работаете на платформе Win32, скажем в Windows XP, то потребуются Visual C++ и исполняемый файл patch.exe. Выполните следующие действия:

1. Распакуйте архив любой имеющейся у вас программой.

2. copy win32\Makefile Makefile

3. Одно из следующих:

nmake 16 RUBYDIR=ruby-source-dir # для Ruby 1.6.8

nmake 18 RUBYDIR=ruby-source-dir # для Ruby 1.8.0/1.8.1

4. Следуйте инструкции в файле ruby-source-dir\win32\README.win32.

При возникновении ошибок обратитесь в список рассылки или конференцию.

 

3.13.3. Некоторые новые возможности Oniguruma

Oniguruma добавляет много новых возможностей к механизму работы с регулярными выражениями в Ruby. Из самых простых отметим дополнительную управляющую последовательность для указания класса символов. Если \d и \D соответствуют десятичным цифрам и не цифрам, то \h и \H являются аналогами для шестнадцатеричных цифр:

"abc" =~ /\h+/ #0

"DEF" =~ /\h+/ # 0

"abc" =~ /\Н+/ # nil

Добавилось возможностей у классов символов в квадратных скобках. Для организации вложенных классов можно применять оператор &&. Вот как можно записать регулярное выражение, соответствующее любой букве, кроме гласных а, е, i, о, u:

reg1 = /[a-z&&[^aeiou]]/ # Задает пересечение.

А следующее выражение соответствует всему алфавиту, кроме букв от m до p:

reg2 = /[a-z&&[^m-р]]/

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

Другие возможности Oniguruma, например оглядывание назад и именованные соответствия, будут рассмотрены ниже. Все связанное с интернационализацией отложим до главы 4.

 

3.13.4 Позитивное и негативное оглядывание назад

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

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

Предположим, что вам нужно проанализировать некоторую генетическую последовательность (молекула ДНК состоит из четырех основных белков, которые обозначаются А, С, G и T.) Допустим, что мы ищем все неперекрывающиеся цепочки нуклеотидов (длины 4), следующие за T. Нельзя просто попытаться найти T и взять следующие четыре символа, поскольку T может быть последним символом в предыдущем соответствии.

gene = 'GATTACAAACTGCCTGACATACGAA'

seqs = gene.scan(/T(\w{4})/)

# seqs равно: [["TACA"], ["GCCT"], ["ACGA"]]

Ho в этом коде мы пропустили цепочку GACA, которая следует за GCCT. Позитивное оглядывание назад позволит найти все нужные цепочки:

gene = 'GATTACAAACTGCCTGACATACGAA'

seqs = gene.scan(/(?<=T)(\w{4})/)

# seqs равно: [["TACA"], ["GCCT"], ["GACA"], ["ACGA"]]

Следующий пример - небольшая модификация примера, предложенного К. Косако (К. Kosako). Предположим, что есть текст в формате XML (или HTML), и мы хотим перевести в верхний регистр весь текст вне тегов (то есть cdata) Вот как можно сделать это с помощью оглядывания назад:

text =<<-EOF

This is a heading

This is a paragraph with some

italics and some boldface

in it...

EOF

pattern = /(?:^| # Начало или...

   (?<=>)        # текст после '>'

  )

  ([^<]*)        # И все символы, кроме '<' (запомнены).

 /x

puts text.gsub(pattern) {|s| s.upcase }

# Вывод:

#

THIS IS A HEADING

#

THIS IS A PARAGRAPH WITH SOME

# ITALICS AND SOME BOLDFACE

# IN IT...

#

 

3.13.5. Еще о кванторах

Мы уже встречались с атомарными подвыражениями в «классической» библиотеке регулярных выражений в Ruby. Они выделяются с помощью нотации (?>...) и являются «собственническими» в том смысле, что жадные и не допускают возврата внутрь подвыражения.

Oniguruma предлагает еще один способ выразить собственническую природу — с помощью квантора +. Он отличается от метасимвола + в смысле «один или более» и даже может использоваться с ним совместно. (На самом деле это «вторичный» квантор, как и ?, который можно употреблять в таких контекстах, как ??, +? и *?.)

Применение + к повторяющемуся образцу эквивалентно заключению его в скобки как независимого подвыражения, например:

r1 = /x*+/ # То же, что /(?>x*)/

r2 = /x++/ # То же, что /(?>x+)/

r3 = /x?+/ # То же, что /(?>x?)/

По техническим причинам Ruby не считает конструкцию {n,m}+ собственнической.

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

 

3.13.6. Именованные соответствия

Специальной формой подвыражения является именованное выражение, которое позволяет присвоить образцу имя (а не просто порядковый номер).

Синтаксически это выглядит так: (?expr), где name — имя, начинающееся с буквы (как идентификаторы в Ruby). Обратите внимание на сходство этой конструкции с неименованным атомарным подвыражением.

Для чего может понадобиться именованное выражение? Например, для того, чтобы сослаться на него внутри обратной ссылки. Ниже приведен пример простого регулярного выражения для сопоставления с повторяющимся словом (см. также раздел 3.14.6):

re1 = /\s+(\w+)\s+\1\s+/

str = "Now is the the time for all..."

re1.match(str).to_a # ["the the","the"]

Здесь мы запомнили слово, а затем сослались на него по номеру \1. Примерно так же можно пользоваться ссылками на именованные выражения. При первом обнаружении подвыражения ему присваивается имя, а в обратной ссылке употребляется символ \k, за которым следует это имя (всегда в угловых скобках):

re2 = /\s+(?\w+)\s+\k\s+/

Второй вариант длиннее, зато понятнее. (Имейте в виду, что в одном и том же регулярном выражении нельзя использовать и именованные, и нумерованные обратные ссылки.) Если нравится, пользуйтесь!

В Ruby уже давно можно включать обратные ссылки в строки, передаваемые методам sub и gsub. Раньше с этой целью допускалось лишь использование нумерованных ссылок, но в самых последних версиях именованные тоже разрешены:

str = "I breathe when I sleep"

# Нумерованные соответствия...

r1 = /I (\w+) when I (\w+)/

s1 = str.sub(r1,' I \2 when I \1')

# Именованные соответствия...

r1 = /I (?\w+) when I (?\w+)/

s2 = str.sub(r2,'I \k when I \k')

Puts s1 # I sleep when I breathe

Puts s2 # I sleep when I breathe

Еще одно возможное применение именованных выражений — повторное употребление выражения. В таком случае перед именем ставится символ \g (а не \k). Определим, например, образец spaces так, чтобы можно было использовать его многократно. Тогда последнее выражение примет вид:

re3 = /(?\s+)(?\w+)\g\k\g/

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

Нотацией \g<1> можно пользоваться и тогда, когда именованных подвыражений нет. Тогда запомненное ранее подвыражение вызывается по номеру, а не по имени.

И последнее замечание об именованных соответствиях. В самых последних версиях Ruby имя (в виде строки или символа) может передаваться методу MatchData в качестве индекса, например:

str = "My hovercraft is full of eels"

reg = /My (?\w+) is (?.*)/

m = reg.match(str)

puts m[:noun] # hovercraft

puts m["predicate"] # full of eels

puts m[1] # то же, что m[:noun] или m["noun"]

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

puts m.noun

puts m.predicate

Но во время работы над книгой это еще не было реализовано.

 

3.13.7. Рекурсия в регулярных выражениях

Возможность повторно обращаться к подвыражению позволяет создавать рекурсивные регулярные выражения. Например, данный код находит любое вложенное выражение с правильно расставленными скобками (спасибо Эндрю Джексону):

str = "а * ((b-c)/(d-e) - f) * g"

reg = /(?         # Начало именованного выражения.

       \(         # Открывающая круглая скобка.

        (?:       # Незапоминаемая группа.

         (?>      # Сопоставление с собственническим выражением:

           \\[()] # экранированная скобка

          |       # ЛИБО

           [^()]  # вообще не скобка. )

          )       # Конец собственнического выражения.

          |       # ЛИБО

          \g      # Вложенная группа в скобках (рекурсивный вызов).

         )*       # Незапоминаемая группа повторяется нуль или

                  # более раз.

        \)        # Закрывающая круглая скобка.

       )          # Конец именованного выражения.

      /x

m = reg.match(str).to_a # ["((b-c)/(d-e) - f)", "((b-c)/(d-e) - f)"]

Отметим, что левосторонняя рекурсия запрещена. Следующий пример допустим:

str = "bbbaccc"

re1 = /(?a|b\gc)/

re1.match(str).to_a # ["bbbaccc","bbbaccc"]

А такой — нет:

re2 = /(?a|\gc)/ # Синтаксическая ошибка!

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

 

3.14. Примеры регулярных выражений

 

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

 

3.14.1. Сопоставление с IP-адресом

Пусть мы хотим понять, содержит ли строка допустимый IPv4-адрес. Стандартно он записывается в точечно-десятичной нотации, то есть в виде четырех десятичных чисел, разделенных точками, причем каждое число должно находиться в диапазоне от 0 до 255.

Приведенный ниже образец решает эту задачу (за немногими исключениями типа «127.1»). Для удобства восприятия мы разобьем его на части. Отметим, что символ \d дважды экранирован, чтобы косая черта не передавалась из строки в регулярное выражение (чуть ниже мы решим и эту проблему).

num = "(\\d|[01]?\\d\\d|2[0-4]\\d\25[0-5])"

pat = ^(#{num}\.){3}#{num}$"

ip_pat = Regexp.new(pat)

ip1 = "9.53.97.102"

if ip1 =~ ip_pat # Печатается: "да"

 puts "да"

else

 puts "нет"

end

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

num = /(\d1[01]?\d\d|2[0-4]\d|25[0-5])/

Когда одно регулярное выражение интерполируется в другое, вызывается метод to_s, который сохраняет всю информацию из исходного регулярного выражения.

num.to_s # "(?-mix:(\\d|[01]?\\d\\d|2[0-4]\\d|25[0-5]))"

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

IPv6-адреса пока не очень широко распространены, но для полноты рассмотрим и их. Они записываются в виде восьми шестнадцатеричных чисел, разделенных двоеточиями, с подавлением начальных нулей.

num = /[0-9A-Fa-f]{0,4}/

pat = /^(#{num}:){7}#{num}$/

ipv6_pat = Regexp.new(pat)

v6ip = "abcd::1324:ea54::dead::beef"

if v6ip =~ ipv6_pat # Печатается: "да"

 puts "да"

else

 puts "нет"

end

 

3.14.2. Сопоставление с парой «ключ-значение»

Иногда приходится работать со строками вида «ключ=значение» (например, при разборе конфигурационного файла приложения).

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

pat = /(\w+)\s*=\s*(.*?)$/

str = "color = blue"

matches = pat.match(str)

puts matches[1] # "color"

puts matches[2] # "blue"

 

3.14.3. Сопоставление с числами, записанными римскими цифрами

Следующее довольно сложное регулярное выражение сопоставляется с любым правильно записанным римскими цифрами числом (до 3999 включительно). Как и раньше, для удобства восприятия образец разбит на части:

rom1 = /m{0,3}/i

rom2 = /(d?c{0,3}|с[dm])/i

rom3 = /(l?x{0,3}|x[lс])/i

rom4 = /(v?i{0,3}|i[vx])/i

roman = /^#{rom1}#{rom2}#{rom3}#{rom4}$/

year1985 = "MCMLXXXV"

if year1985 =~ roman # Печатается: "да"

 puts "да"

else

 puts "нет"

end

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

# Это не работает!

rom1 = /m{0,3}/

rom2 = /(d?c{0,3}|с[dm])/

rom3 = /(l?x{0,3}|x[lс])/

rom4 = /(v?i{0,3}|i[vx])/

roman = /^#{rom1}#{rom2}#{rom3}#{rom4}$/i

Почему такое выражение не годится? Взгляните на этот пример и поймете:

rom1.to_s # "(?-mix:m{0,3})"

Обратите внимание, что метод to_s запоминает флаги для каждого выражения; тем самым флаг всего выражения перекрывается.

 

3.14.4 Сопоставление с числовыми константами

Сопоставление с простым целым десятичным числом — самое простое. Число состоит из необязательного знака и последовательности цифр (правда, Ruby позволяет использовать знак подчеркивания в качестве разделителя цифр). Отметим, что первая цифра не должна быть нулем, иначе число будет интерпретироваться как восьмеричное.

int_pat = /^[+-]?[1-9][\d_]*$/

Целые константы в других системах счисления обрабатываются аналогично. Образцы для шестнадцатеричных и двоичных чисел сделаны не чувствительными к регистру, так как они содержат букву:

hex_pat = /^[+-]?0x[\da-f_]+$/i

oct_pat = /^[+-]?0[0-7_]+$/

bin_pat = /^[+-]?0b[01_]+$/i

Сопоставить число с плавающей точкой в обычной нотации несколько сложнее. Последовательности цифр по обе стороны десятичной точки необязательны, но хотя бы одна цифра должна быть:

float_pat = /^(\d[\d_]*)*\.[\d_]*$/

Образец для чисел, записанных в научной нотации, основан на предыдущем:

sci_pat = /^(\d[\d_]*)?\.[\d_]*(e[+-]?)?(_*\d[\d_]*)$/i

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

 

3.14.5 Сопоставление с датой и временем

Пусть надо выделить дату и время, записанные в формате mm/dd/yy hh:mm:ss. Вот первая попытка: datetime = /(\d\d)\/(\d\d)\/(\d\d) (\d\d): (\d\d): (\d\d)/.

Но такой образец распознает некоторые некорректные даты и отвергает правильные. Следующий вариант более избирателен. Обратите внимание, как мы строим его путем интерполяции мелких регулярных выражений в более крупное:

mo = /(0?[1-9]|1[0-2])/         # От 01 до 09 или от 1 до 9 или 10-12.

dd = /([0-2]?[1-9]| [1-3][01])/ # 1-9 или 01-09 или 11-19 и т.д.

yy = /(\d\d)/ # 00-99

hh = /([01]?[1-9]|[12][0-4])/   # 1-9 или 00-09 или...

mi = /([0-5]\d)/                # 00-59, обе цифры должны присутствовать.

ss = /([0-6]\d)?/               # разрешены еще и доли секунды ;-)

date = /(#{mo}\/#{dd}\/#{yy})/

time = /{#{hh}:#{mi}:#{ss})/

datetime = /(#{date} #{time})/

Вот как можно вызвать это регулярное выражение из метода String#scan, чтобы получить массив соответствий:

str="Recorded on 11/18/07 20:31:00"

str.scan(datetime)

# [["11/18/07 20:31:00", "11/18/07", "11", "18", "00",

# "20:31:00", "20", "31", ":00"]]

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

datetime = %r{(

 (0?[1-9]|1[0-2])/        # mo: от 01 до 09 или от 1 до 9 или 10-12.

 ([0-2]?[1-9]|[1-3][01])/ # dd: 1-9 или 01-09 или 11-19 и т. д.

 (\d\d) [ ]               # yy: 00-99

 ([01]?[1-9]|[12][0-4]):  # hh: 1-9 или 00-09 или...

 ([0-5]\d):               # mm: 00-59, обе цифры должны присутствовать.

 (([0-6]\d))?             # ss: разрешены еще и доли секунды ;-)

)}x

Обратите внимание на конструкцию %r{}, позволяющую не экранировать символы обратной косой черты.

 

3.14.6. Обнаружение повторяющихся слов в тексте

В этом разделе мы реализуем детектор повторяющихся слов. Повторение одного и того же слова два раза подряд — типичная опечатка. Следующий код распознает такие ситуации:

double_re = /\b(['A-Z]+) +\1\b/i

str="There's there's the the pattern."

str.scan(double_re) # [["There's"],["the"]]

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

 

3.14.7. Поиск слов, целиком набранных прописными буквами

Мы упростили пример, предположив, что в тексте нет чисел, подчерков и т.д.

allcaps = /\b[A-Z]+\b/

string = "This is ALL CAPS"

string[allcaps]      # "ALL"

Suppose you want to extract every word in all-caps:

string.scan(allcaps) # ["ALL", "CAPS"]

При желании можно было бы обобщить эту идею на идентификаторы Ruby и аналогичные вещи.

 

3.14.8. Сопоставление с номером версии

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

package = "mylib-1.8.12"

matches = package.match(/(.*)-(\d+)\.(\d+)\.(\d+)/)

name, major, minor, tiny = matches[1..-1]

 

3.14.9. Еще несколько образцов

Завершим наш список несколькими выражениями из категории «разное». Как обычно, почти все эти задачи можно решить несколькими способами.

Пусть нужно распознать двузначный почтовый код американского штата. Проще всего, конечно, взять выражение /[A-Z]{2}/. Но оно сопоставляется с такими строками, как XX или ZZ, которые допустимы, но бессмысленны. Следующий образец распознает все стандартные аббревиатуры, общим числом 51 (50 штатов и DC — округ Колумбия):

state = /^A[LKZR] ! C[AOT] | D[EC] | FL | GA | HI | I[DLNA] |

          K[SY] | LA | M[EDAINSOT] | N[EVHJMYCD] | O[HKR] |

          PA | RI | S[CD] | T[NX] | UT | V[TA] | W[AVIY]$/x

Для ясности я воспользовался обобщенным регулярным выражением (модификатор x). Пробелы и символы новой строки в нем игнорируются.

Продолжая эту тему, приведем регулярное выражение для распознавания почтового индекса США (он может состоять из пяти или девяти цифр):

zip = /^\d{5}(-\d{4))?$/

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

Следующее регулярное выражение распознает номер телефона в формате NANP (североамериканский план нумерации). Есть три способа записи такого номера:

phone = /^((\(\d{3}\) |\d{3}-)\d{3}-\d{4}|\d{3}\.\d{3}\.\d{4})$/

"(512) 555-1234" =~ phone # true

"512.555.1234" =~ phone   # true

"512-555-1234" =~ phone   # true

"(512)-555-1234" =~ phone # false

"512-555.1234" =~ phone   # false

Распознавание денежной суммы в долларах также не составит труда:

dollar = /^\$\d+{\.\d\d)?$/

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

 

3.15. Заключение

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