为什么正则表达式引擎允许/在输入字符串的末尾自动尝试匹配?

mkl*_*nt0 23 regex language-agnostic

注意:
*Python用于说明行为,但这个问题与语言无关.
*为了该讨论的目的,假定单行只输入,因为换行(多行输入)的存在下引入变化的行为$.认为是不可避免的手边的问题.

大多数正则表达式引擎:

  • 接受在输入字符串[1]结束显式尝试匹配表达式的正则表达式.

    $ python -c "import re; print(re.findall('$.*', 'a'))"
    [''] # !! Matched the hypothetical empty string after the end of 'a'
    
    Run Code Online (Sandbox Code Playgroud)
  • 当找到/替换全局时,即,当查找给定正则表达式的所有非重叠匹配,并且已到达字符串的末尾时,意外地尝试再次匹配[2],如相对问题的答案中所解释的:

    $ python -c "import re; print(re.findall('.*$', 'a'))"
    ['a', ''] # !! Matched both the full input AND the hypothetical empty string
    
    Run Code Online (Sandbox Code Playgroud)

也许不用说,只有当所讨论的正则表达式匹配空字符串时(例如,默认情况下正则表达式/被配置为报告零长度匹配),这样的匹配尝试才会成功.

这些行为至少乍一看是违反直觉的,我想知道是否有人可以为他们提供设计理由,尤其是因为:

  • 这种行为的好处并不明显.
  • 相反地,在寻找/与模式,例如全球替换的上下文中.*.*$,该行为是彻头彻尾令人惊讶.[3]
    • 更具针对性地提出这个问题:为什么设计用于找到正则表达式的多个非重叠匹配的功能 - 即全局匹配 - 如果它知道整个输入已经被消耗,决定甚至尝试另一个匹配,而不管是什么正则表达式(虽然你永远不会看到一个正则表达式的症状,至少也不匹配空字符串)
    • 以下语言/引擎表现出令人惊讶的行为:.NET,Python(2.x和3.x)[2],Perl(5.x和6.x),Ruby,Node.js(JavaScript)

请注意,正则表达式引擎的行为在零长度(空字符串)匹配后继续匹配的位置有所不同.

任何选择(从相同的角色位置开始,然后从下一个开始)都是可辩护的 - 请参阅www.regular-expressions.info上关于零长度匹配的章节.

相比之下,.*$这里讨论的情况是不同的,与任何非空输入时,第一个匹配的.*$不是一个零长度匹配,这样的行为差异并不能适用-相反,字符位置应提前无条件后的第一个匹配,如果你已经在最后,这当然是不可能的.
同样,令我惊讶的是,尽管如此,仍然会尝试另一场比赛.


[1]我在这里使用$作为输入结束标记,即使在某些引擎中,例如.NET,它可以标记输入的结尾,可选地后跟一个尾随换行符.但是,当您使用无条件输入结束标记时,行为同样适用\z.

[2] Python 2.x和3.x高达3.6.x 在这种情况下看似特殊的替换行为: python -c "import re; print(re.sub('.*$', '[\g<0>]', 'a'))"用于产生[a]- 也就是说,只找到并替换了一个匹配.
从Python 3.7开始,行为现在就像大多数其他正则表达式引擎一样,在这两个引擎中执行了两个替换,产生了[a][].

[3]您可以通过(a)选择旨在找到最多一个匹配的替换方法或(b)用于^.*防止通过输入开始锚定找到多个匹配来避免该问题.
(a)可能不是一种选择,取决于给定语言如何表现功能; 例如,PowerShell的-replace运算符总是取代所有出现的事件; 考虑以下尝试将所有数组元素括起来"...":
'a', 'b' -replace '.*', '"$&"'.由于匹配两次,这产生元素"a""""b""";
选项(b)'a', 'b' -replace '^.*', '"$&"',修复问题.

Tim*_*sen 5

我给出这个答案只是为了说明为什么正则表达式希望允许任何代码出现在模式中的最终$锚之后.假设我们需要创建一个正则表达式来匹配一个字符串与以下规则:

  • 从三个数字开始
  • 后跟一个或多个字母,数字,连字符或下划线
  • 只有字母和数字结尾

我们可以编写以下模式:

^\d{3}[A-Za-z0-9\-_]*[A-Za-z0-9]$
Run Code Online (Sandbox Code Playgroud)

但这有点笨重,因为我们必须使用彼此相邻的两个相似的字符类.相反,我们可以将模式编写为:

^\d{3}[A-Za-z0-9\-_]+$(?<!_|-)
Run Code Online (Sandbox Code Playgroud)

要么

^\d{3}[A-Za-z0-9\-_]+(?<!_|-)$
Run Code Online (Sandbox Code Playgroud)

在这里,我们删除了一个字符类,而是在锚之后使用负向lookbehind $断言最后一个字符不是下划线或连字符.

除了一个外观之外,对我来说,为什么正则表达式引擎允许在$锚之后出现某些东西是没有意义的.我的观点是,正则表达式引擎可能允许在后面出现一个lookbehind $,并且在某种情况下逻辑上有意义这样做.


mkl*_*nt0 2

笔记:

\n
    \n
  • 我的问题帖子包含两个相关但不同的问题,正如我现在意识到的那样,我应该为此创建单独的帖子。
  • \n
  • 这里的其他答案都集中于其中一个问题,因此这个答案在一定程度上提供了一个路线图,说明哪些答案可以解决哪个问题。\n
  • \n
\n
\n

至于为什么允许诸如此类的模式$<expr>(即,在输入结束匹配某些内容)/何时有意义:

\n
    \n
  • dawg 的回答认为,出于务实的原因,$.+ 可能不会阻止诸如此类的无意义组合;排除它们可能不值得付出努力。

    \n
  • \n
  • 蒂姆的回答显示了某些表达式在之后如何有意义$,即否定后向断言

    \n
  • \n
  • ivan_pozdeev 答案的后半部分有力地综合了 dawg 和 Tim 的答案。

    \n
  • \n
\n
\n

至于为什么全局匹配会找到.*等模式的两个.*$匹配项:

\n
    \n
  • revo 的答案包含有关零长度(空字符串)匹配的重要背景信息,这就是问题最终归结为的问题。
  • \n
\n

让我通过更直接地将其与全局匹配背景下的行为如何与我的期望相矛盾来补充他的答案:

\n
    \n
  • 从纯粹常识的角度来看,按理说,一旦输入在匹配时被完全消耗,根据定义就没有剩下任何东西,因此没有理由寻找进一步的匹配。

    \n
  • \n
  • 相比之下,大多数正则表达式引擎认为输入字符串最后一个字符之后的字符位置(在某些引擎中称为主题字符串结尾的位置)是匹配的有效起始位置,因此尝试另一个匹配

    \n
      \n
    • 如果手头的正则表达式恰好匹配空字符串(产生零长度匹配;例如,诸如.*, 或a?表达式),它会匹配该位置并返回空字符串匹配。

      \n
    • \n
    • 相反,如果正则表达式不(也)匹配空字符串,您将不会看到额外的匹配 - 而仍然尝试额外的匹配额外的匹配,但在这种情况下不会找到任何匹配,因为空字符串string 是主题字符串末尾位置唯一可能的匹配。

      \n
    • \n
    \n
  • \n
\n

虽然这提供了该行为的技术解释,但它仍然没有告诉我们为什么在之后进行匹配要在最后一个字符

\n

我们拥有的最接近的东西是Wiktor Stribi\xc5\xbcew在评论中的有根据的猜测(强调已添加),这再次表明了该行为的务实原因:

\n
\n

...当您获得空字符串匹配时,您可能仍会匹配字符串中仍处于同一索引的下一个字符。如果正则表达式引擎不支持它,这些匹配将被跳过。对于正则表达式引擎作者来说,对字符串结尾进行例外处理可能并不是那么重要

\n
\n

ivan_pozdeev 答案的前半部分通过告诉我们[input] 字符串末尾的 void是匹配的有效位置,更详细地解释了该行为,就像任何其他字符边界位置一样。
\n然而,虽然对所有此类位置进行相同的处理肯定是内部一致的,并且可能简化了实现,但这种行为仍然违背常识,并且对用户没有明显的好处。

\n
\n

关于空字符串匹配的进一步观察:

\n

注意:在下面的所有代码片段中,执行全局字符串替换[...]以突出显示生成的匹配项:每个匹配项都包含在 中,而输入的不匹配部分按原样传递。\n

\n

总之,3种不同的、独立的行为适用于空(字符串)匹配的上下文,并且不同的引擎使用不同的组合

\n
    \n
  • 是否是 POSIX ERE 规范的最长最左规则谢谢,revo被服从。

    \n
  • \n
  • 在全局匹配中:

    \n
      \n
    • 空匹配后字符位置是否提前。
    • \n
    • 是否尝试对输入末尾的定义空字符串进行另一次匹配(我的问题帖子中的第二个问题)。
    • \n
    \n
  • \n
\n

主题字符串末尾位置的匹配不限那些在空匹配后在相同字符位置继续匹配的引擎。

\n

例如,.NET 正则表达式引擎不会这样做(PowerShell 示例):

\n
PS> \'a1\' -replace \'\\d*|a\', \'[$&]\'\n[]a[1][]\n
Run Code Online (Sandbox Code Playgroud)\n

那是:

\n
    \n
  • \\d*匹配之前的空字符串 a
  • \n
  • a然后它本身不匹配,这意味着字符位置在空匹配之后被提前。
  • \n
  • 1匹配的是\\d*
  • \n
  • 主题字符串结尾位置再次与 匹配\\d*,导致另一个空字符串匹配。
  • \n
\n

Perl 5 是在相同字符位置恢复匹配的引擎示例:

\n
$ "a1" | perl -ple "s/\\d*|a/[$&]/g"\n[][a][1][]\n
Run Code Online (Sandbox Code Playgroud)\n

请注意如何a匹配。

\n

有趣的是,Perl 6不仅表现不同,而且还表现出另一种行为变体:

\n
$ "a1" | perl6 -pe "s:g/\\d*|a/[$/]/"\n[a][1][]\n
Run Code Online (Sandbox Code Playgroud)\n

看起来,如果交替找到and空匹配以及非空匹配,则仅报告非空匹配。

\n

Perl 6 的行为似乎遵循最长最左规则。

\n

虽然sedawk也这样做,但它们不会尝试在字符串末尾进行另一次匹配:

\n

sed,BSD/macOS 和 GNU/Linux 实现:

\n
PS> \'a1\' -replace \'\\d*|a\', \'[$&]\'\n[]a[1][]\n
Run Code Online (Sandbox Code Playgroud)\n

awk- BSD/macOS 和 GNU/Linux 实现以及mawk

\n
$ "a1" | perl -ple "s/\\d*|a/[$&]/g"\n[][a][1][]\n
Run Code Online (Sandbox Code Playgroud)\n

  • 正则表达式世界中有一个规则叫做“最左最长匹配”。看起来 Perl 6 也遵循它。这是 POSIX 标准。sed 和 awk 也紧随其后。`\d*` 不会在偏移量 `0` 处产生匹配,因为另一侧的 `a` 将产生比 `\d*` 更长的匹配。总的来说,这是一个很好的总结答案。然而,有些陈述没有得到权威参考资料的支持,例如来自 dawg 的陈述或来自 Wiktor Stribiżew 的陈述。 (2认同)