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

mkl*_*nt0 6 .net ruby python regex node.js

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

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

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

我的问题是:

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

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

更新:雷沃乐于助人的回答解释了如何与当前的行为的; 至于潜在的原因,请参阅此相关问题.

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

 # .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<0>]', 'a'))"
 [a][] 

 # Perl 5
 $ echo a | perl -ple 's/.*/[$&]/g'
 [a][] 

 # Perl 6
 $ echo 'a' | perl6 -pe 's:g/.*/[$/]/'
 [a][]

 # Others?
Run Code Online (Sandbox Code Playgroud)

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

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

# Others?
Run Code Online (Sandbox Code Playgroud)

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<0>]', '$inputStr'))" },
          { python3 -c "import re; print(re.sub('$regex', '[\g<0>]', '$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 '.*', '^.*', '.*$', '^.*$', '.*?'
Run Code Online (Sandbox Code Playgroud)

正则表达式的示例输出^.*,它确认了bobble bubble期望使用start anchor(^)在所有语言中只产生一个匹配:

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<0>]', 'a'))"
[a]
VERBOSE: python3   : python3 -c "import re; print(re.sub('^.*', '[\g<0>]', 'a'))"
[a]
VERBOSE: perl      : "a" | perl -ple "s/^.*/[$&]/g"
[a]
VERBOSE: perl6     : "a" | perl6 -pe "s:g/^.*/[$/]/"
[a]
Run Code Online (Sandbox Code Playgroud)

rev*_*evo 7

有点有趣的问题。我不会先询问您的问题,而是征求您的评论。

一旦输入字符串被完全消耗,为什么要将没有任何剩余的事实视为空字符串呢?

留下一个称为主题字符串末尾的位置。这是一个位置,可以匹配。与其他零宽度断言和断言锚点\b, \B, ^, $... 一样,点星号.*可以匹配空字符串。这高度依赖于正则表达式引擎。例如,TRegEx 的做法有所不同。

如果这样做,这不会导致无限循环吗?

不,这是正则表达式引擎要处理的主要工作。它们会提出一个标志并存储当前游标数据以避免发生此类循环。Perl 文档是这样解释的

对这种能力的常见滥用源于使用正则表达式进行无限循环的能力,其中一些无害的内容如下:

'foo' =~ m{ ( o? )* }x;
Run Code Online (Sandbox Code Playgroud)

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

因此,Perl 通过强制打破无限循环来允许这样的构造。对于贪婪量词给出的较低级别的循环和修饰符或运算符*+{}等较高级别的循环, 其规则是不同的。/gsplit()

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

现在回到你的问题:

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

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

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

答:这要看情况。

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

它匹配它然后提出一个标志,以便不再与(相同)匹配相同的位置?图案。在 PCRE 中,.*匹配整个主题字符串,然后在其后立即停止。在最后,当前位置是 PCRE 中有意义的位置,位置可以匹配或断言,因此还有一个位置(零长度字符串)需要匹配。PCRE 再次遍历正则表达式(如果g启用了修饰符)并在主题末尾找到匹配项。

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

你看,如果你想阻止第二场比赛的发生,你需要以某种方式告诉引擎:

^.*
Run Code Online (Sandbox Code Playgroud)

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

(?!$).*
Run Code Online (Sandbox Code Playgroud)

请参阅此处的现场演示,特别是查看调试器窗口