Regex с именованными группами захвата, получающими все совпадения в Ruby
У меня есть строка:
s="123--abc,123--abc,123--abc"
Я попробовал использовать новую функцию Ruby 1.9 "named groups", чтобы получить всю именованную информацию о группе:
/(?<number>\d*)--(?<chars>\s*)/
Существует ли API, подобный Python findall
, который возвращает коллекцию matchdata
? В этом случае мне нужно вернуть два совпадения, потому что 123
и abc
повторяются дважды. Каждая информация о совпадении содержит подробную информацию о каждой именованной информации захвата, поэтому я могу использовать m['number']
для получения значения соответствия.
Ответы
Ответ 1
Именованные захваты подходят только для одного совпадающего результата.
Ruby-аналог findall
String#scan
. Вы можете использовать результат scan
в качестве массива или передать ему блок:
irb> s = "123--abc,123--abc,123--abc"
=> "123--abc,123--abc,123--abc"
irb> s.scan(/(\d*)--([a-z]*)/)
=> [["123", "abc"], ["123", "abc"], ["123", "abc"]]
irb> s.scan(/(\d*)--([a-z]*)/) do |number, chars|
irb* p [number,chars]
irb> end
["123", "abc"]
["123", "abc"]
["123", "abc"]
=> "123--abc,123--abc,123--abc"
Ответ 2
Chiming в супер-позднем, но здесь простой способ репликации String # scan, но вместо этого получить matchdata:
matches = []
foo.scan(regex){ matches << $~ }
matches
теперь содержит объекты MatchData, которые соответствуют сканированию строки.
Ответ 3
Вы можете извлечь использованные переменные из регулярного выражения с помощью метода names
. Так что я сделал это, я использовал обычный метод scan
для получения совпадений, затем заархивированные имена и каждое совпадение для создания Hash
.
class String
def scan2(regexp)
names = regexp.names
scan(regexp).collect do |match|
Hash[names.zip(match)]
end
end
end
Использование:
>> "aaa http://www.google.com.tr aaa https://www.yahoo.com.tr ddd".scan2 /(?<url>(?<protocol>https?):\/\/[\S]+)/
=> [{"url"=>"http://www.google.com.tr", "protocol"=>"http"}, {"url"=>"https://www.yahoo.com.tr", "protocol"=>"https"}]
Ответ 4
@Nakilon правильно показывает scan
с регулярным выражением, однако вам даже не нужно рисковать в землю регулярных выражений, если вы не хотите:
s = "123--abc,123--abc,123--abc"
s.split(',')
#=> ["123--abc", "123--abc", "123--abc"]
s.split(',').inject([]) { |a,s| a << s.split('--'); a }
#=> [["123", "abc"], ["123", "abc"], ["123", "abc"]]
Возвращает массив массивов, что удобно, если у вас есть несколько вхождений и нужно просмотреть/обработать их все.
s.split(',').inject({}) { |h,s| n,v = s.split('--'); h[n] = v; h }
#=> {"123"=>"abc"}
Это возвращает хеш, который, поскольку элементы имеют один и тот же ключ, имеет только уникальное значение ключа. Это хорошо, когда у вас есть куча дубликатов ключей, но вам нужны уникальные. Его недостаток возникает, если вам нужны уникальные значения, связанные с ключами, но это, по-видимому, другой вопрос.
Ответ 5
Год назад мне нужны регулярные выражения, которые легче читать и называли захватами, поэтому я сделал следующее дополнение к String (возможно, не было, но в то время это было удобно):
scan2.rb:
class String
#Works as scan but stores the result in a hash indexed by variable/constant names (regexp PLACEHOLDERS) within parantheses.
#Example: Given the (constant) strings BTF, RCVR and SNDR and the regexp /#BTF# (#RCVR#) (#SNDR#)/
#the matches will be returned in a hash like: match[:RCVR] = <the match> and match[:SNDR] = <the match>
#Note: The #STRING_VARIABLE_OR_CONST# syntax has to be used. All occurences of #STRING# will work as #{STRING}
#but is needed for the method to see the names to be used as indices.
def scan2(regexp2_str, mark='#')
regexp = regexp2_str.to_re(mark) #Evaluates the strings. Note: Must be reachable from here!
hash_indices_array = regexp2_str.scan(/\(#{mark}(.*?)#{mark}\)/).flatten #Look for string variable names within (#VAR#) or # replaced by <mark>
match_array = self.scan(regexp)
#Save matches in hash indexed by string variable names:
match_hash = Hash.new
match_array.flatten.each_with_index do |m, i|
match_hash[hash_indices_array[i].to_sym] = m
end
return match_hash
end
def to_re(mark='#')
re = /#{mark}(.*?)#{mark}/
return Regexp.new(self.gsub(re){eval $1}, Regexp::MULTILINE) #Evaluates the strings, creates RE. Note: Variables must be reachable from here!
end
end
Пример использования (irb1.9):
> load 'scan2.rb'
> AREA = '\d+'
> PHONE = '\d+'
> NAME = '\w+'
> "1234-567890 Glenn".scan2('(#AREA#)-(#PHONE#) (#NAME#)')
=> {:AREA=>"1234", :PHONE=>"567890", :NAME=>"Glenn"}
Примечания:
Конечно, было бы более элегантно поместить шаблоны (например, AREA, PHONE...) в хэш и добавить этот хэш с шаблонами в аргументы scan2.
Ответ 6
При использовании ruby >= 1.9 и названных захватов вы можете:
class String
def scan2(regexp2_str, placeholders = {})
return regexp2_str.to_re(placeholders).match(self)
end
def to_re(placeholders = {})
re2 = self.dup
separator = placeholders.delete(:SEPARATOR) || '' #Returns and removes separator if :SEPARATOR is set.
#Search for the pattern placeholders and replace them with the regex
placeholders.each do |placeholder, regex|
re2.sub!(separator + placeholder.to_s + separator, "(?<#{placeholder}>#{regex})")
end
return Regexp.new(re2, Regexp::MULTILINE) #Returns regex using named captures.
end
end
Использование (ruby >= 1.9):
> "1234:Kalle".scan2("num4:name", num4:'\d{4}', name:'\w+')
=> #<MatchData "1234:Kalle" num4:"1234" name:"Kalle">
или
> re="num4:name".to_re(num4:'\d{4}', name:'\w+')
=> /(?<num4>\d{4}):(?<name>\w+)/m
> m=re.match("1234:Kalle")
=> #<MatchData "1234:Kalle" num4:"1234" name:"Kalle">
> m[:num4]
=> "1234"
> m[:name]
=> "Kalle"
Использование опции разделителя:
> "1234:Kalle".scan2("#num4#:#name#", SEPARATOR:'#', num4:'\d{4}', name:'\w+')
=> #<MatchData "1234:Kalle" num4:"1234" name:"Kalle">
Ответ 7
Мне недавно было нужно что-то подобное. Это должно работать как String#scan
, но вместо этого возвращать массив объектов MatchData.
class String
# This method will return an array of MatchData rather than the
# array of strings returned by the vanilla `scan`.
def match_all(regex)
match_str = self
match_datas = []
while match_str.length > 0 do
md = match_str.match(regex)
break unless md
match_datas << md
match_str = md.post_match
end
return match_datas
end
end
Запуск ваших данных образца в REPL приводит к следующему:
> "123--abc,123--abc,123--abc".match_all(/(?<number>\d*)--(?<chars>[a-z]*)/)
=> [#<MatchData "123--abc" number:"123" chars:"abc">,
#<MatchData "123--abc" number:"123" chars:"abc">,
#<MatchData "123--abc" number:"123" chars:"abc">]
Вы также можете найти мой тестовый код полезным:
describe String do
describe :match_all do
it "it works like scan, but uses MatchData objects instead of arrays and strings" do
mds = "ABC-123, DEF-456, GHI-098".match_all(/(?<word>[A-Z]+)-(?<number>[0-9]+)/)
mds[0][:word].should == "ABC"
mds[0][:number].should == "123"
mds[1][:word].should == "DEF"
mds[1][:number].should == "456"
mds[2][:word].should == "GHI"
mds[2][:number].should == "098"
end
end
end
Ответ 8
Мне очень понравилось решение Umut-Utkan, но он не совсем сделал то, что я хотел, поэтому я немного переписал его (примечание: ниже может быть не красивый код, но он работает)
class String
def scan2(regexp)
names = regexp.names
captures = Hash.new
scan(regexp).collect do |match|
nzip = names.zip(match)
nzip.each do |m|
captgrp = m[0].to_sym
captures.add(captgrp, m[1])
end
end
return captures
end
end
Теперь, если вы делаете
p '12f3g4g5h5h6j7j7j'.scan2(/(?<alpha>[a-zA-Z])(?<digit>[0-9])/)
Вы получаете
{:alpha=>["f", "g", "g", "h", "h", "j", "j"], :digit=>["3", "4", "5", "5", "6", "7", "7"]}
(т.е. все альфа-символы, найденные в одном массиве, и все цифры, найденные в другом массиве). В зависимости от вашей цели для сканирования это может быть полезно. Во всяком случае, мне нравится видеть примеры того, как легко переписать или расширить функциональность ядра Ruby всего несколькими строками!
Ответ 9
Мне нравится match_all, данное Джоном, но я думаю, что у него есть ошибка.
Строка:
match_datas << md
работает, если в регулярном выражении нет захватов().
Этот код дает всю строку до и включая шаблон, сопоставленный/захваченный регулярным выражением. ([0] часть MatchData) Если в regex есть capture(), то этот результат, вероятно, не тот, который хочет пользователь (я) в конечном результате.
Я думаю, что в случае, когда в regex есть capture(), правильный код должен быть:
match_datas << md[1]
Конечным результатом match_datas будет массив совпадений с шаблоном, начиная с match_datas [0]. Это не совсем то, что можно ожидать, если требуется обычная MatchData, которая включает в себя значение match_datas [0], которое представляет собой всю согласованную подстроку, за которой следуют match_datas [1], match_datas [[2],.. которые являются захватами (если они есть ) в шаблоне регулярных выражений.
Все сложное - возможно, поэтому match_all не был включен в native MatchData.
Ответ 10
Отказываясь от ответа Марка Хаббарта, я добавил следующий патч обезьяны:
class ::Regexp
def match_all(str)
matches = []
str.scan(self) { matches << $~ }
matches
end
end
который можно использовать как /(?<letter>\w)/.match_all('word')
, и возвращает:
[#<MatchData "w" letter:"w">, #<MatchData "o" letter:"o">, #<MatchData "r" letter:"r">, #<MatchData "d" letter:"d">]
Это, как говорят другие, зависит от использования $~
в блоке сканирования для данных соответствия.