为什么 #match 返回正确的结果,而 #scan 只找到一个字母?

Why does #match return the correct result, but #scan finds only only one letter?

提问人:Todd A. Jacobs 提问时间:3/25/2023 更新时间:3/26/2023 访问量:100

问:

问题背景

我正在使用 Ruby 3.2.1,并且我知道对 Ruby 的正则表达式引擎进行了更改。这可能与此相关,也可能无关。但是,在使用反向引用时,我得到了与 String#match 和 String#scan 出乎意料的不同行为,我不明白为什么。请参阅下面的示例代码。

我的例子,有评论和期望

匹配的工作结果
# Using #match finds the longest string of
# repeated letters, and the letter that is
# repeated.
"gjhfgfcttttdfs".match /(\p{alpha})\1{2,}/
=> #<MatchData "tttt" 1:"t">
扫描时无法正常工作,出现意外结果
# Here, #scan returns only a single sub-
# array with a single letter, which is
# the correct letter. However, I was
# expecting an array-of-arrays with all
# repeated letters.
"gjhfgfcttttdfs".scan /(\p{alpha})\1{2,}/
=> [["t"]]

澄清问题

假设键盘和椅子之间存在问题,为什么 String#scan 没有返回更多匹配项,甚至没有返回一个更长的匹配项?我假设这是我在捕获表达式中的错误,但我无法真正弄清楚我在这里做错了什么。

正则表达式 Ruby 字符串 反向引用

评论

1赞 mu is too short 3/25/2023
我认为问题始于“如果模式包含组,则每个结果都是一个数组,每个组包含一个条目”部分。似乎这是因为分组而为您提供的,并且只是因为这是唯一与反向引用匹配的部分。 例如,将为您提供两个单字符结果。String#scanscan't''t''gjhfgfffcttttdfs'scan
1赞 Jared Beck 3/25/2023
FWIW,我在 Ruby 2.7 中得到了相同的结果,所以我认为您没有发现 Ruby 中的重大变化。
0赞 Todd A. Jacobs 3/25/2023
@muistooshort 你可能是对的,如果有组,那么 #scan 基本上会返回每个组的值作为子数组的一部分,但我想我也在问“如果这不是我的明显错误,我怎么能让 #scan 返回我期望的结果?我用 #match 得到了正确的答案,但是如果我能获得对正确分组的正确回溯引用,似乎应该有一种方法可以以某种方式对 #scan 做同样的事情。如果使用得当,它们似乎都应该能够返回类似的结果。
2赞 Cary Swoveland 3/25/2023
请注意,当 的参数是具有一个或多个捕获组的正则表达式时,可以使用具有一个参数且没有块的 String#gsub 形式来代替,但与 不同的是,捕获组在返回值方面没有特殊含义。这里。这种形式的 返回一个枚举器,该枚举器仅生成 的参数匹配项(尽管方法名称与字符串替换无关)。scangsubscan"gjhfgfcttttdfs".gsub(/(\p{alpha})\1{2,}/).to_a #= ["tttt"]gsubgsub

答:

2赞 Alex 3/25/2023 #1

如果模式包含组,则每个结果都是一个包含 每组条目。https://rubyapi.org/3.2/o/string#method-i-scan

似乎有点模糊,但我认为已经很久了。

>> "aaabccc".match /(\p{alpha})\1{2,}/
=> #<MatchData "aaa" 1:"a">
>> "aaabccc".scan /(\p{alpha})\1{2,}/
=> [["a"], ["c"]]

要获取整个匹配项,您可以捕获它,成为结果的一部分:scan

>> "aaabccc".match /((\p{alpha})\2{2,})/
=> #<MatchData "aaa" 1:"aaa" 2:"a">
#                    ^       ^
# two captures will return two captured results for each match
>> "aaabccc".scan /((\p{alpha})\2{2,})/
=> [["aaa", "a"], ["ccc", "c"]]

# that's the best i could come up with this
>> "aaabccc".scan(/((\p{alpha})\2{2,})/).map(&:first)
=> ["aaa", "ccc"]

完全匹配仅在块中可用:scan

>> a = []; "aaabccc".scan(/(\p{alpha})\1{2,}/) { a << $& }; a
=> ["aaa", "ccc"]

评论

0赞 Todd A. Jacobs 3/25/2023
你肯定在做某事!我认为需要添加第二个捕获组:,但是是否可以使用单个捕获组执行此操作?尝试使其中一个组不捕获是行不通的,因为您需要对单个字母的反向引用,然后创建工作来剥离子数组中的第二个结果。我想这就是 #match 无论如何都在幕后做的事情,使其成为更好的选择。这让我有点困扰,如果不在 #scan 上进行额外的链接,我无法提取重复的字符。"gjhfgfcttttdfs".scan /((\p{alpha})\2{2,})/ #=> [["tttt", "t"]]
0赞 mu is too short 3/25/2023
您可以混合命名捕获和未命名捕获以获得与 after () 相反的结果,但反过来混合捕获类型会产生 ,一旦引入命名捕获,引用就仅适用于命名捕获。"aaabccc".scan(/((?<c>\p{alpha})\k<c>{2,})/)SyntaxError
2赞 Stefan 3/25/2023 #2

这两种方法的返回值不同:返回/生成 MatchData 对象,而返回/生成与捕获组内容(如果存在)相对应的字符串。的行为可能在某种程度上不方便,但您可以解决它。matchscanscan

如果你想要整个对象(工作方式),但对于每个匹配项,你可以调用一个块并使用 $~(或 )来检索它:MatchDatamatchscanRegexp.last_match

"aaabccc".scan(/(\p{alpha})\1{2,}/) { p $~ }
# #<MatchData "aaa" 1:"a">
# #<MatchData "ccc" 1:"c">

要获取对象数组,您可以利用 enum_for 获取“每个匹配项”枚举器,然后通过以下方式将它们发送到各自的枚举器:MatchDatamapMatchData$~

"aaabccc".enum_for(:scan, /(\p{alpha})\1{2,}/).map { $~ }
#=> [#<MatchData "aaa" 1:"a">, #<MatchData "ccc" 1:"c">]