2011-01-14 2 views
28

У меня есть строка:Regex с именованными группами захвата получать все матчи в Рубине

s="123--abc,123--abc,123--abc" 

Я попытался с помощью Ruby 1.9 в новую функцию «именованные группы», чтобы принести всю информацию о группе под названием:

/(?<number>\d*)--(?<chars>\s*)/ 

Есть ли API, такой как Python's findall, который возвращает коллекцию matchdata? В этом случае мне нужно вернуть два матча, потому что 123 и abc повторяют дважды. Каждая информация о совпадении содержит подробную информацию о каждой именованной информации захвата, поэтому я могу использовать m['number'], чтобы получить значение соответствия.

ответ

29

Именованные захваты подходят только для одного совпадающего результата.
Ruby's analogue 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" 
+0

/(\ d *) - ([az] *)/если я использую это регулярное выражение, как я могу получить полную строку соответствия, в этом случае это ['123 - abc', '123 - abc '], то я могу построить matchdata для каждого элемента самостоятельно – mlzboy

+0

@mlzboy, есть два решения. Самый простой из них - добавить третью группу в регулярное выражение: '/ ((\ d *) - ([az] *))/do | all, number, chars |' – Nakilon

+2

спасибо, похоже, рубин не поддерживал названная функция захвата хорошо – mlzboy

2

@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"} 

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

+0

'Hash [s.split (", "). Map {| i | i.split ("-")}] ' – Nakilon

2

Год назад я хотел регулярные выражения, которые были более легко читать и назвал снимки, так что я сделал следующее дополнение к 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.

2

При использовании рубинового> = 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 

Usage (рубин> = 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

Вы можете извлечь используемые переменные из регулярного выражения, используя метод 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"}] 
0

Мне очень понравилось @ решение Умут-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 всего несколькими строками!

2

Мне было нужно что-то подобное недавно. Это должно работать как String#scan, но вместо этого возвращать массив объектов MatchData.

class String 
    # This method will return an array of MatchData's 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 
+0

Лично я бы сделал это принадлежащим классу Regexp, но это все еще очень приятное решение. Удивленный это не основной метод. –

17

звона в супер-конце, но вот простой способ репликации String # scan, но вместо этого получить сопоставление:

matches = [] 
foo.scan(regex){ matches << $~ } 

matches теперь содержит объекты MatchData, которые соответствуют сканированию строки.

+4

'$ LAST_MATCH_INFO' - аналогичная« английская »переменная для' $ ~ ', надеемся, что она сохранит некоторые поисковые запросы для людей, которым нужен более читаемый код. – Michael

+1

Все сводится к предпочтениям (или руководства по стилям!), Но некоторые сказали бы, что '$ ~' более читабельны, чем '$ LAST_MATCH_DATA'. '$ ~' имеет две вещи для этого: это более кратким (2 символа против 12), и его тильда следует шаблону оператора регулярного выражения, '= ~'. Конечно, вы всегда должны указывать на то, что ваша аудитория (коллеги, будущие вам) будут легко понимать. Я никогда не могу вспомнить, что делают многие из этих $ переменных: '$;', '$ &' и '$ @'? Кто может сказать, не глядя на них? :-) –

+0

Как это работает? Что гарантирует '' ~ 'меняется на каждую итерацию? –

0

Мне нравится match_all, данное Джоном, но я думаю, что у него есть ошибка.

Линия:

match_datas << md 

работает, если нет никаких захватов() в регулярном выражении.

Этот код дает всю строку до и включая шаблон, сопоставленный/захваченный регулярным выражением. ([0] часть MatchData) Если в 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.

0

Спекуляция от ответа Марка Hubbart, я добавил следующую обезьяну-патч:

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">]

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

Смежные вопросы