Подтвердить что ты не робот

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'] для получения значения соответствия.

4b9b3361

Ответ 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">]

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