使用变量分组解析带有ruby正则表达式的字符串的更优雅的方法?

时间:2011-11-30 16:57:53

标签: ruby regex

目前我的正则表达式如下所示:

^(cat|dog|bird){1}(cat|dog|bird)?(cat|dog|bird)?$

它匹配一个长词列表中至少1个,最多3个实例,并通过相应的变量使每个组的匹配词可用。

有没有办法修改它,以便我可以返回字符串中每个单词的结果而不事先指定组的数量?

^(cat|dog|bird)+$

有效但仅分别返回最后一个匹配,因为只有一个组。

4 个答案:

答案 0 :(得分:3)

好的,所以我找到了解决方法。

看起来不可能创建一个未知数量的组,所以我开始寻找另一种方法来实现预期的结果:能够判断一个字符串是否由一个字组成给出清单;并匹配每个位置可能的最长单词。

我一直在阅读JeffreyE. F. Friedl的Mastering Regular Expressions,它为我揭示了一些事情。事实证明,基于NFA的Regexp引擎(如Ruby中使用的引擎)是顺序的,也是懒惰/贪婪的。这意味着您可以使用您为其选择的顺序来指定模式的匹配方式。这解释了为什么扫描返回变量结果,它正在查找列表中符合条件的第一个单词,然后转到下一个匹配。根据设计,它不是寻找最长的匹配,而是第一个。因此,为了纠正这一点,我需要做的就是将用于生成正则表达式的单词数组从字母顺序重新排序到长度顺序(从最长到最短)。

array = %w[ as ascarid car id ]
list = array.sort_by {|word| -word.length } 
regexp = Regexp.union(list)

现在扫描找到的第一个匹配将是可用的最长字。使用scan:

判断一个字符串是否只包含列表中的单词也很简单
if "ascarid".scan(regexp).join.length == word.length
  return true
else
  return false
end

感谢所有回复此问题的人,我希望这将有助于其他人。

答案 1 :(得分:2)

你可以分两步完成:

  1. 使用/^(cat|dog|bird)+$/(或更好/\A(cat|dog|bird)+\z/)确保匹配。
  2. 然后string.scan(/cat|dog|bird/)得到碎片。
  3. 您也可以使用split和Set来同时执行这两项操作。假设你在a数组中有你的单词,在s中有你的字符串,那么:

    words = Set.new(a)
    re    = /(#{a.map{|w| Regexp.quote(w)}.join('|')})/
    parts = s.split(re).reject(&:empty?)
    if(parts.any? {|w| !words.include?(w) })
      # 's' didn't match what you expected so throw a
      # hissy fit, format the hard drive, set fire to
      # the backups, or whatever is appropriate.
    else
      # Everything you were looking for is in 'parts'
      # so you can check the length (if you care about
      # how many matches there were) or something useful
      # and productive.
    end
    

    当您使用包含组的模式的split

      

    相应的匹配也将在数组中返回。

    在这种情况下,split会向我们提供类似["", "cat", "", "dog"]的内容,空字符串只会出现在我们正在查找的分隔符之间,因此我们可以reject假装他们不存在。这可能是对split的意外使用,因为我们对分隔符的兴趣超过分隔符(除了确保没有分隔任何内容),但它完成了工作。


    根据您的评论,您似乎想要一个有序的替换,以便(ascarid|car|as|id)尝试从左到右匹配。我在Ruby Oniguruma(Ruby 1.9正则表达式引擎)文档中找不到|有序或无序的内容; Perl's alternation似乎被指定(或至少强烈暗示)被命令,Ruby的行为肯定就像订购一样:

    >> 'pancakes' =~ /(pan|pancakes)/; puts $1
    pan
    

    因此,在构建正则表达式时,您可以将单词从最长到最短排序:

    re = /(#{a.sort_by{|w| -w.length}.map{|w| Regexp.quote(w)}.join('|')})/
    

    并希望Oniguruma真的会从左到右匹配交替。 AFAIK,Ruby的正则表达式为eager because they support backreferences and lazy/non-greedy matching,因此这种方法应该是安全的。

    或者你可能是偏执狂并分步解析;首先,你要确保你的字符串看起来像你想要的那样:

    if(s !~ /\A(#{a.map{|w| Regexp.quote(w)}.join('|')})+\z/)
      # Bail out and complain that 's' doesn't look right
    end
    

    小组你的话长度:

    by_length = a.group_by(&:length)
    
    对于从最长单词到最短单词的群组,

    scan

    # This loses the order of the substrings within 's'...
    matches = [ ]
    by_length.keys.sort_by { |k| -k }.each do |group|
      re = /(#{a.map{|w| Regexp.quote(w)}.join('|')})/
      s.gsub!(re) { |w| matches.push(w); '' }
    end
    # 's' should now be empty and the matched substrings will be
    # in 'matches'
    

    这些方法仍有可能重叠的空间,但至少你会提取最长的匹配。

答案 2 :(得分:1)

如果你需要重复正则表达式的部分,一个选项是将重复的部分存储在一个变量中,并引用它,例如:

r = "(cat|dog|bird)"
str.match(/#{r}#{r}?#{r}?/)

答案 3 :(得分:1)

You can do it with .Net regular expressions。如果我在PowerShell中编写以下内容

$pat = [regex] "^(cat|dog|bird)+$"
$m = $pat.match('birddogcatbird')
$m.groups[1].captures | %{$_.value}

然后我得到

bird
dog
cat
bird

我跑的时候。我对IronRuby的了解甚至比我对PowerShell的了解要少,但也许这意味着你也可以在IronRuby中做到这一点。