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

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

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

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

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

str = "а123b45с678"
if /(ad+)(bd+)(cd+)/ =~ str
 puts "Частичные соответствия: '#$1', '#$2', '#$3'"
 # Печатается: Частичные соответствия: 'а123', 'b45', 'c768'
end

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

str = "а123b45с678"
str.sub(/(ad+)(bd+)(cd+)/, "1st=#$1, 2nd=#$2, 3rd=#$3")
# "1st=, 2nd=, 3rd="

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

str = "а123b45с678"
s2 = "1st=#$1, 2nd=#$2, 3rd=#$3"
reg = /(ad+)(bd+)(cd+)/
str.sub(reg,s2)
# "1st=, 2nd=, 3rd="

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

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

str = "а123b45с678"
str.sub(/(ad+)(bd+)(cd+)/, '1st=1, 2nd=2, 3rd=3')
# "1st=a123, 2nd=b45, 3rd=c768"

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

str = "а123b45с678"
str.sub(/(ad+)(bd+)(cd+)/, "1st=1, 2nd=2, 3rd=3")
# "1st=01, 2nd=02, 3rd=03"

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

str = "а123b45с678"
str.sub(/(ad+)(bd+)(cd+)/, "1st=1, 2nd=2, 3rd=3")
# "1st=a123, 2nd=b45, 3rd=c678"

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

str = "а123b45с678"
str.sub(/(ad+)(bd+)(cd+)/) { "1st=#$1, 2nd=#$2, 3rd=#$3" }
# "1st=a123, 2nd=b45, 3rd=c678"

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

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

str = "а123b45с678"
str.sub(/(ad+)(?:bd+)(cd+)/, "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"

Оглавление книги


Генерация: 1.074. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз