为什么一些正则表达式引擎在单个输入字符串中匹配。*两次?
许多正则表达式引擎在单行字符串中匹配.*
两次 ,例如,在执行基于正则表达式的字符串替换时:
- 根据定义,第一个匹配是整个(单行)字符串,如预期的那样。
-
在许多引擎中有第二个匹配,即空字符串 ; 也就是说,即使第一个匹配消耗了整个输入字符串,
.*
再次匹配,然后匹配输入字符串末尾的空字符串。- 注意:要确保只找到一个匹配项,请使用
^.*
- 注意:要确保只找到一个匹配项,请使用
我的问题是:
-
这种行为有充分的理由吗? 一旦输入字符串被完全消耗,我就不会期望再次尝试找到匹配项。
-
除了试验和错误之外,您是否可以从文档/正则表达式方言/标准中收集哪些引擎表现出这种行为?
更新 : 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然后尝试前进到下一个立即位置以再次运行整个过程,但由于没有剩余位置,它失败了。
你看是否要防止第二场比赛发生,你需要以某种方式告诉引擎:
^.*
或者更好地了解正在发生的事情:
(?!$).*
在这里看现场演示,特别看看调试器窗口 。