使用变量分组,用 Ruby 正则表达式解析字符串的更加优雅的方法是什么?

3

目前我有一个正则表达式,看起来像这样:

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

该正则表达式匹配至少1个、最多3个单词列表中的实例,并将每个组的匹配单词通过相应的变量可用。

是否有一种方法可以修改它,以便我可以返回字符串中每个单词的结果,而不需要预先指定组数?

^(cat|dog|bird)+$

该函数可以运行,但是只返回最后一个匹配项,因为只有一个分组。


3
不使用string.scan(/cat|dog|bird/)的原因是什么? - riffraff
3
@riffraff: 'dogpancakesbird'.scan(/cat|dog|bird/)@riffraff:'dogpancakesbird'.scan(/cat|dog|bird/) - mu is too short
我没有注意到锚点,谢谢。 - riffraff
是的。误判。只有当整个字符串被正则表达式吞噬时,匹配才可能为真。实际使用的单词列表非常大,我已经简化了它以使我的问题更容易理解。 - i0n
4个回答

3

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

看起来不可能创建未知数量的组,因此我寻找另一种实现所需结果的方法:能够确定一个字符串是否由给定列表中的单词组成;并匹配每个位置上可能的最长单词。

我一直在阅读Jeffrey E. F. Friedl的《精通正则表达式》,它为我提供了一些启示。原来基于NFA的正则表达式引擎(如Ruby中使用的引擎)是顺序的,同时也是懒惰/贪婪的。这意味着您可以使用给出选择的顺序来指定如何匹配模式。这就解释了为什么扫描返回可变结果,它正在寻找第一个符合条件的列表中的单词,然后转移到下一个匹配项。按设计,它不是在寻找最长匹配,而是第一个匹配项。因此,为了纠正这种情况,我只需要将用于生成正则表达式的单词数组从字母顺序重新排序为长度顺序(从最长到最短)。

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

现在通过扫描找到的第一个匹配项将是最长的可用单词。使用扫描也很容易判断字符串是否仅包含列表中的单词:

if "ascarid".scan(regexp).join.length == word.length
  return true
else
  return false
end

感谢所有回答此问题的人,我希望这些内容能够帮助其他人。


是的,我正在寻找一个保证/a|aa/从左到右匹配的方法,获得额外确认会很好。如果需要一步完成,您可以使用 array.sort_by {|word| -word.length } - mu is too short
顺便说一句,这个问题比起一开始看上去的更有趣,不错。 - mu is too short

2
你可以分两步完成它:
  1. 使用 /^(cat|dog|bird)+$/(或更好的 /\A(cat|dog|bird)+\z/)来确保匹配。
  2. 然后使用 string.scan(/cat|dog|bird/) 来获取这些部分。

你也可以使用 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 的交替 显然被指定为有序,而 Ruby 的行为肯定表现得像是有序的:
>> 'pancakes' =~ /(pan|pancakes)/; puts $1
pan

这样,在构建正则表达式时,您可以按从长到短的顺序排序单词:
re = /(#{a.sort_by{|w| -w.length}.map{|w| Regexp.quote(w)}.join('|')})/

希望Oniguruma确实能够从左到右匹配交替字串。据我所知,Ruby的正则表达式会采用贪婪匹配和惰性/非贪婪匹配,因为它们支持反向引用,因此这种方法应该是安全的。
或者,你可以非常谨慎地分步骤解析它;首先,你需要确保你的字符串看起来像你想要的:
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)

"从最长的单词到最短的单词扫描并查找组:"
# 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'

这些方法仍然可能存在重叠的空间,但至少您会提取最长的匹配项。

也许我应该提一下,但我已经尝试过使用split。问题在于split将返回它匹配到的第一个片段,因此对于正则表达式中有大量单词的情况,会出现许多误报和不匹配的情况。我为了例子缩小了单词列表,因为这会使问题更加混乱。分组是我找到的唯一实现所需功能而又不会出现太多错误的方法,但我必须预先指定组数。有没有一种动态实现这一点的方法?这才是问题的关键,而不是实现匹配。 - i0n
@i0n:所以有些“单词”会重叠,你想先匹配最长的单词再看短一些的吗?这是一个生物学问题吗? - mu is too short
是的,没错。例如,单词“ascarid”理想情况下应该匹配一个单词,“ascarid”。目前它会被匹配为3个单词:“as”、“car”、“id”。我需要模式贪婪,但如果可能的话总是匹配整个字符串! - i0n
@i0n:我已经添加了一些可能性的更新(它们太大了,无法在评论中提出)。 - mu is too short
看起来我们得出了相同的结论。谢谢你的帮助! - i0n

1
如果您需要重复使用正则表达式的某些部分,一种选择是将重复的部分存储在变量中并引用它,例如:
r = "(cat|dog|bird)"
str.match(/#{r}#{r}?#{r}?/)

我已经将单词数组(实际上比示例中的要长得多)存储在变量中,我只是从示例中删除了它,以避免混淆问题。 - i0n

1

您可以使用 .Net 正则表达式完成此操作。如果我在 PowerShell 中编写以下内容

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

然后我得到

bird
dog
cat
bird

当我运行它时,我对IronRuby的了解甚至比我的PowerShell还要少,但也许这意味着你也可以用IronRuby来做到这一点。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接