Regex с именованными группами захвата, получая все совпадения в Ruby - PullRequest
28 голосов
/ 14 января 2011

У меня есть строка:

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

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

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

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

Ответы [ 10 ]

30 голосов
/ 14 января 2011

Именованные захваты подходят только для одного совпадающего результата.
Аналогом Руби findall является String#scan. Вы можете использовать scan result в качестве массива или передать ему блок:

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"
20 голосов
/ 11 декабря 2012

Чипинг в супер-конце, но вот простой способ репликации сканирования String #, но вместо этого получение совпадающих данных:

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

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

9 голосов
/ 28 февраля 2012

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

Мне нужно что-то подобное недавно.Это должно работать как 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
2 голосов
/ 22 февраля 2011

Если вы используете 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

Использование (рубин> = 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">
2 голосов
/ 14 января 2011

@ Накилон правильно показывает 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"}

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

1 голос
/ 18 апреля 2012

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

1 голос
/ 22 февраля 2011

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

0 голосов
/ 10 декабря 2014

Воспользовавшись ответом Марка Хаббарта, я добавил следующий обезьян-патч:

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

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

0 голосов
/ 25 августа 2013

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

Линия:

  match_datas << md

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

Этот код дает всю строку до и включая шаблон, соответствующий / захваченный регулярным выражением. (Часть [0] MatchData) Если регулярное выражение имеет метод capture (), то этот результат, вероятно, не соответствует желанию пользователя (меня) в конечном выводе.

Я думаю, что в случае использования captures () в регулярном выражении правильный код должен быть:

  match_datas << md[1]

Возможный выход match_datas будет массивом совпадений захвата шаблона, начиная с match_datas [0]. Это не совсем то, что можно ожидать, если требуется нормальная MatchData, которая включает значение match_datas [0], которое представляет собой всю совпадающую подстроку, за которой следуют match_datas [1], match_datas [[2], .., которые являются захватами (если есть ) в регулярном выражении.

Вещи сложные - возможно, поэтому match_all не был включен в собственные MatchData.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...