簡體   English   中英

具有命名捕獲組的正則表達式獲取Ruby中的所有匹配項

[英]Regex with named capture groups getting all matches in Ruby

我有一個字符串:

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

我嘗試使用Ruby 1.9的新功能“命名組”來獲取所有命名的組信息:

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

是否有像Python的findall這樣的API返回一個matchdata集合? 在這種情況下,我需要返回兩個匹配,因為123abc重復兩次。 每個匹配數據包含每個命名捕獲信息的詳細信息,因此我可以使用m['number']來獲取匹配值。

命名捕獲僅適用於一個匹配結果。
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"

超級遲到,但這是一種復制String#scan的簡單方法,但獲取matchdata:

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

matches現在包含與掃描字符串相對應的MatchData對象。

您可以使用names方法從regexp中提取已使用的變量。 所以我做的是,我使用常規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"}]

我最近需要類似的東西。 這應該像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

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

這將返回一個哈希值,因為元素具有相同的鍵,所以只有唯一鍵值。 當你有一堆重復的鍵但想要獨特的鍵時,這是很好的。 如果您需要與鍵相關聯的唯一值,則會出現其缺點,但這似乎是一個不同的問題。

如果使用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">

我真的很喜歡@ 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功能的例子!

一年前,我想要更容易閱讀並命名為捕獲的正則表達式,所以我對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的參數中會更優雅。

我喜歡John給出的match_all,但我認為它有錯誤。

這條線:

  match_datas << md

如果正則表達式中沒有捕獲(),則有效。

此代碼提供整個行,包括正則表達式匹配/捕獲的模式。 (MatchData的[0]部分)如果正則表達式具有capture(),則該結果可能不是用戶(我)在最終輸出中想要的結果。

我認為在regex中有capture()的情況下,正確的代碼應該是:

  match_datas << md[1]

match_datas的最終輸出將是從match_datas [0]開始的模式捕獲匹配數組。 如果需要正常的MatchData,這可能是預期的,其中包括match_datas [0]值,該值是整個匹配的子串,后跟match_datas [1],match_datas [[2],..這是捕獲(如果有的話) )在正則表達式模式中。

事情很復雜 - 這可能就是為什么match_all不包含在原生MatchData中的原因。

撇開Mark Hubbart的回答,我添加了以下猴子補丁:

class ::Regexp
  def match_all(str)
    matches = []
    str.scan(self) { matches << $~ }

    matches
  end
end

可以用作/(?< /(?<letter>\\w)/.match_all('word') . /(?<letter>\\w)/.match_all('word') ,並返回:

[#<MatchData "w" letter:"w">, #<MatchData "o" letter:"o">, #<MatchData "r" letter:"r">, #<MatchData "d" letter:"d">]

正如其他人所說,這依賴於在掃描塊中使用$~作為匹配數據。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM