(?<=>) # текст после '>'
)
([^<]*) # И все символы, кроме '<' (запомнены).
/x
puts text.gsub(pattern) {|s| s.upcase }
# Вывод:
# <body> <h1>THIS IS A HEADING</h1>
# <p>THIS IS A PARAGRAPH WITH SOME
# <i>ITALICS</i> AND SOME <b>BOLDFACE</b>
# IN IT...</p>
# </body>
3.13.5. Еще о кванторах
Мы уже встречались с атомарными подвыражениями в "классической" библиотеке регулярных выражений в Ruby. Они выделяются с помощью нотации (?>...) и являются "собственническими" в том смысле, что жадные и не допускают возврата внутрь подвыражения.
Oniguruma предлагает еще один способ выразить собственническую природу - с помощью квантора +. Он отличается от метасимвола + в смысле "один или более" и даже может использоваться с ним совместно. (На самом деле это "вторичный" квантор, как и ?, который можно употреблять в таких контекстах, как ??, +? и *?.)
Применение + к повторяющемуся образцу эквивалентно заключению его в скобки как независимого подвыражения, например:
r1 = /x*+/ # То же, что /(?>x*)/
r2 = /x++/ # То же, что /(?>x+)/
r3 = /x?+/ # То же, что /(?>x?)/
По техническим причинам Ruby не считает конструкцию {n,m}+ собственнической.
Понятно, что новый квантор - не более чем удобное обозначение, никакой новой функциональности он не несет.
3.13.6. Именованные соответствия
Специальной формой подвыражения является именованное выражение, которое позволяет присвоить образцу имя (а не просто порядковый номер).
Синтаксически это выглядит так: (?<name>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+(?<anyword>\w+)\s+\k<anyword>\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 (?<verb1>\w+) when I (?<verb2>\w+)/
s2 = str.sub(r2,'I \k<verb2> when I \k<verb1>')
Puts s1 # I sleep when I breathe
Puts s2 # I sleep when I breathe
Еще одно возможное применение именованных выражений - повторное употребление выражения. В таком случае перед именем ставится символ \g (а не \k). Определим, например, образец spaces так, чтобы можно было использовать его многократно. Тогда последнее выражение примет вид:
re3 = /(?<spaces>\s+)(?<anyword>\w+)\g<spaces>\k<anyword>\g<spaces>/
Обратите внимание, что этот образец многократно употребляется с помощью маркера \g. Особенно удобна такая возможность в рекурсивных регулярных выражениях, но это тема следующего раздела.
Нотацией \g<1> можно пользоваться и тогда, когда именованных подвыражений нет. Тогда запомненное ранее подвыражение вызывается по номеру, а не по имени.
И последнее замечание об именованных соответствиях. В самых последних версиях Ruby имя (в виде строки или символа) может передаваться методу MatchData в качестве индекса, например:
str = "My hovercraft is full of eels"
reg = /My (?<noun>\w+) is (?<predicate>.*)/
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 = /(?<foo>a|b\g<foo>c)/
re1.match(str).to_a # ["bbbaccc","bbbaccc"]
А такой - нет:
re2 = /(?<foo>a|\g<foo>c)/ # Синтаксическая ошибка!
Ошибка объясняется наличием рекурсивного обращения в начале каждой альтернативы. Немного подумав, вы поймете, что это приведет к бесконечному возврату.
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]