为什么一些正则表达式引擎在单个输入字符串中匹配。*两次?

许多正则表达式引擎在单行字符串中匹配.* 两次 ,例如,在执行基于正则表达式的字符串替换时:

  • 根据定义,第一个匹配是整个(单行)字符串,如预期的那样。
  • 在许多引擎中有第二个匹配,即空字符串 ; 也就是说,即使第一个匹配消耗了整个输入字符串, .* 再次匹配,然后匹配输入字符串末尾的空字符串。

    • 注意:要确保只找到一个匹配项,请使用^.*

我的问题是:

  • 这种行为有充分的理由吗? 一旦输入字符串被完全消耗,我就不会期望再次尝试找到匹配项。

  • 除了试验和错误之外,您是否可以从文档/正则表达式方言/标准中收集哪些引擎表现出这种行为?

更新 : revo的有用答案解释了当前行为的方式; 至于潜在的原因 ,请参阅此相关问题 。

表现出行为的语言/平台:

  # .NET, via PowerShell (behavior also applies to the -replace operator) PS> [regex]::Replace('a', '.*', '[$&]' [a][] # !! Note the *2* matches, first the whole string, then the empty string # Node.js $ node -pe "'a'.replace(/.*/g, '[$&]')" [a][] # Ruby $ ruby -e "puts 'a'.gsub(/.*/, '[\\0]')" [a][] # Python 3.7+ only $ python -c "import re; print(re.sub('.*', '[\g]', 'a'))" [a][] # Perl 5 $ echo a | perl -ple 's/.*/[$&]/g' [a][] # Perl 6 $ echo 'a' | perl6 -pe 's:g/.*/[$/]/' [a][] # Others? 

不表现出这种行为的语言/平台:

 # Python 2.x and Python 3.x <= 3.6 $ python -c "import re; print(re.sub('.*', '[\g]', 'a'))" [a] # !! Only 1 match found. # Others? 

bobble bubble带来了一些很好的相关点:

如果你像懒惰一样.*? ,你甚至可以在一些比赛中获得3场 比赛,在其他 比赛中获得2场比赛 。 同样的.?? 。 一旦我们使用了一个起始锚,我认为我们应该只得到一个匹配,但有趣的是它似乎是^.*? 在PCRE中为a给出两个匹配 ,而^.*应该在任何地方产生一个匹配。


这是一个PowerShell代码段,用于测试跨语言的行为 ,具有多个正则表达式:

注意:假设Python 3.x可用作python3 ,Perl 6可用作perl6
您可以将整个代码段直接粘贴到命令行上,并从历史记录中调用它以修改输入。

 & { param($inputStr, $regexes) # Define the commands as script blocks. # IMPORTANT: Make sure that $inputStr and $regex are referenced *inside "..."* # Always use "..." as the outer quoting, to work around PS quirks. $cmds = { [regex]::Replace("$inputStr", "$regex", '[$&]') }, { node -pe "'$inputStr'.replace(/$regex/g, '[$&]')" }, { ruby -e "puts '$inputStr'.gsub(/$regex/, '[\\0]')" }, { python -c "import re; print(re.sub('$regex', '[\g]', '$inputStr'))" }, { python3 -c "import re; print(re.sub('$regex', '[\g]', '$inputStr'))" }, { "$inputStr" | perl -ple "s/$regex/[$&]/g" }, { "$inputStr" | perl6 -pe "s:g/$regex/[$/]/" } $regexes | foreach { $regex = $_ Write-Verbose -vb "----------- '$regex'" $cmds | foreach { $cmd = $_.ToString().Trim() Write-Verbose -vb ('{0,-10}: {1}' -f (($cmd -split '\|')[-1].Trim() -split '[ :]')[0], $cmd -replace '\$inputStr\b', $inputStr -replace '\$regex\b', $regex) & $_ $regex } } } -inputStr 'a' -regexes '.*', '^.*', '.*$', '^.*$', '.*?' 

regex ^.*示例输出,它确认了bobble bubble的期望,即使用起始锚点( ^ )在所有语言中只产生一个匹配:

 VERBOSE: ----------- '^.*' VERBOSE: [regex] : [regex]::Replace("a", "^.*", '[$&]') [a] VERBOSE: node : node -pe "'a'.replace(/^.*/g, '[$&]')" [a] VERBOSE: ruby : ruby -e "puts 'a'.gsub(/^.*/, '[\\0]')" [a] VERBOSE: python : python -c "import re; print(re.sub('^.*', '[\g]', 'a'))" [a] VERBOSE: python3 : python3 -c "import re; print(re.sub('^.*', '[\g]', 'a'))" [a] VERBOSE: perl : "a" | perl -ple "s/^.*/[$&]/g" [a] VERBOSE: perl6 : "a" | perl6 -pe "s:g/^.*/[$/]/" [a] 

有点有趣的问题。 我会先回复你的评论,而不是先提问你的问题。

一旦输入字符串被完全消耗掉,为什么你会把剩下的空字符串留下来呢?

保留称为主题字符串结尾的位置 。 这是一个位置,可以匹配。 像其他零宽度断言和锚点\b\B^$ …断言,点星.*可以匹配空字符串。 这高度依赖于正则表达式引擎。 例如,TRegEx采用不同的方式。

如果你这样做,这不应该导致无限循环吗?

不,这是正则表达式引擎的主要工作。 它们引发一个标志并存储当前的游标数据,以避免发生这种循环。 Perl文档以这种方式解释它 :

对这种力量的普遍滥用源于使用正则表达式创建无限循环的能力,其中包含以下内容:

 'foo' =~ m{ ( o? )* }x; 

o?foo的开头匹配,并且由于匹配不移动字符串中的位置, o? 由于*量词,会一次又一次地匹配。 另一种创建类似循环的常用方法是使用循环修饰符/g

因此,Perl通过强制打破无限循环来允许这样的结构。 对于由贪婪量词*+{}给出的低级循环,以及对于更高级别的循环(如/g修饰符或split()运算符split() ,此规则是不同的。

当Perl检测到重复表达式与零长度子字符串匹配时,下级循环被中断 (即,循环被破坏)。

现在回到你的问题:

这种行为有充分的理由吗?

就在这里。 每个正则表达式引擎都必须满足大量挑战才能处理文本。 其中一个是处理零长度匹配 。 你的问题提出了另一个问题,

问:匹配零长度字符串后引擎应该如何进行?

答:这完全取决于。

PCRE(或Ruby)不会跳过零长度匹配。

它匹配它然后引发一个标志与(相同)再次匹配相同的位置 模式 。 在PCRE .*匹配整个主题字符串然后在它之后停止。 最后,当前位置在PCRE中是一个有意义的位置,位置可以匹配或被断言,因此有一个位置(零长度字符串)需要匹配。 PCRE再次通过正则表达式(如果启用了g修饰符)并在主题末尾找到匹配项。

PCRE然后尝试前进到下一个立即位置以再次运行整个过程,但由于没有剩余位置,它失败了。

你看是否要防止第二场比赛发生,你需要以某种方式告诉引擎:

 ^.* 

或者更好地了解正在发生的事情:

 (?!$).* 

在这里看现场演示,特别看看调试器窗口 。