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

Posted

技术标签:

【中文标题】为啥正则表达式引擎允许/自动尝试在输入字符串的末尾进行匹配?【英文标题】:Why do regex engines allow / automatically attempt matching at the end of the input string?为什么正则表达式引擎允许/自动尝试在输入字符串的末尾进行匹配? 【发布时间】:2019-02-21 11:44:20 【问题描述】:

注意: * Python 用于说明行为,但这个问题与语言无关。 * 出于本次讨论的目的,假设单行仅输入,因为换行符(多行输入)的存在会导致$ 和@ 的行为发生变化987654325@ 与手头的问题附带

大多数正则表达式引擎:

接受一个正则表达式,该正则表达式显式地尝试匹配一个表达式输入字符串的结尾[1]

$ python -c "import re; print(re.findall('$.*', 'a'))"
[''] # !! Matched the hypothetical empty string after the end of 'a'

当查找/替换全局时,即当查找给定正则表达式的所有非重叠匹配时,并且已经到达字符串的末尾,意外地尝试再次匹配[2],如this answer to a related question 中所述:

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

也许不用说,只有当所讨论的正则表达式匹配 空字符串(并且正则表达式默认 / 配置为报告零长度)时,此类匹配尝试才会成功匹配)。

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

不清楚这种行为的好处是什么。 相反,在使用.*.*$ 等模式全局查找/替换的情况下,这种行为完全令人惊讶。[3] 更尖锐地问这个问题:为什么设计用于查找正则表达式的多个非重叠匹配的功能 - 即,全局匹配 - 决定甚至 尝试另一个匹配如果它知道整个输入已经被消耗了,不管正则表达式是什么(尽管你永远不会看到一个没有的正则表达式的症状最少匹配空字符串) 以下语言/引擎表现出令人惊讶的行为:.NET、Python(2.x 和 3.x)[2]、Perl(5.x 和 6.x)、 Ruby、Node.js (javascript)

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

任何一种选择(从相同的字符位置开始与从下一个字符位置开始)都是合理的 - 请参阅 the chapter on zero-length matches at www.regular-expressions.info。

相比之下,这里讨论的.*$ 情况的不同之处在于,对于任何非空输入,.*$第一个匹配项不是零-length 匹配,所以行为差异 not 适用 - 相反,字符位置应该在第一次匹配之后 unconditionally 前进,如果你已经在这当然是不可能的结束。 同样,令我惊讶的是 another 仍然尝试匹配,即使根据定义没有任何内容。


[1] 我在这里使用$ 作为输入结束标记,尽管在某些引擎中,例如.NET,它可以将结束标记为输入的结束可选地后跟一个尾随换行符。但是,当您使用 unconditional 输入结束标记 \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 运算符 invariably 替换 all 出现;考虑以下尝试将所有数组元素包含在 "...":'a', 'b' -replace '.*', '"$&"' 中。由于匹配两次,这会产生元素"a""""b"""; 选项 (b),'a', 'b' -replace '^.*', '"$&"',解决了这个问题。

【问题讨论】:

这里的重点是空字符串(零长度)匹配在不同的正则表达式风格中被不同地对待,因为行为不标准化,每个人都有自己的方式解决它。有一个很好的理由,因为当你得到一个空字符串匹配时,你可能仍然匹配下一个仍然在字符串中相同索引处的字符。如果正则表达式引擎不支持它,这些匹配将被跳过。对于正则表达式引擎的作者来说,为字符串的结尾做一个例外可能并不那么重要。 考虑$.$.*之间的区别 @dawg:单行输入,$. never 匹配任何东西,$.* always 匹配一些东西,即空字符串。如前所述,使用多行输入时,许多引擎将$ 解释为最后一个字符。 在单个尾随换行符之前,因此如果 . 也配置为匹配 \n$. 将匹配尾随 \n。但是,如果您使用真正的、无条件的输入结束锚,则单行行为确实适用,例如用于 .NET 的 \z。鉴于这一切,你的例子是为了说明什么? @WiktorStribiżew:到目前为止,您关于 为字符串结尾设置异常对于正则表达式引擎作者可能并不那么重要 的陈述最接近 why 我正在寻找。但是,字符串末尾的匹配仅限于在空匹配后继续在 same 索引处匹配的引擎;如果您愿意,请查看my own answer 的准确性。 【参考方案1】:

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

以三个数字开头 后跟一个或多个字母、数字、连字符或下划线 仅以字母和数字结尾

我们可以写出以下模式:

^\d3[A-Za-z0-9\-_]*[A-Za-z0-9]$

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

^\d3[A-Za-z0-9\-_]+$(?<!_|-)

^\d3[A-Za-z0-9\-_]+(?<!_|-)$

在这里,我们消除了其中一个字符类,而是在$ 锚点之后使用否定的lookbehind 来断言最终字符不是下划线或连字符。

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

【讨论】:

你在混淆概念。 $ 仅断言字符串末尾的位置(或在大多数引擎中的尾随换行符之前),而 (?&lt;!_|-) 是检查字符串末尾 before 文本的后视。这与字符串位置的结尾可以匹配两次无关。 @WiktorStribiżew:Tim 的回答是对我回答中的 first 问题的有益回答;事后看来,我应该创建两个单独的问题帖子。【参考方案2】:

回忆几件事:

    ^$ 是 zero width assertions - 它们在字符串的逻辑开始之后(或在大多数正则表达式实现中以多行模式结束并带有 m 标志的每一行之后)或在字符串的逻辑结尾(或在多行模式下的行尾字符或字符之前的行尾。)

    .* 可能是一个根本不匹配的zero length match。仅长度为零的版本是 $(?:end of line)0 DEMO(我猜这作为评论很有用......)

    . 不匹配 \n(除非您有 s 标志)但匹配 Windows CRLF 行尾中的 \r。因此,$.1 仅匹配 Windows 行结尾(但不要这样做。请改用文字 \r\n。)

除了简单的副作用案例之外,没有特别的好处

    正则表达式$ 很有用; .* 很有用。 正则表达式^(?a lookahead)(?a lookbehind)$ 是常见且有用的。 正则表达式(?a lookaround)^$(?a lookaround) 可能有用。 正则表达式$.* 没有用处,而且很少见,不足以保证实施一些优化以使引擎停止查看该边缘情况。大多数正则表达式引擎在解析语法方面做得不错;例如,缺少大括号或括号。要让引擎将$.* 解析为无用,需要将该正则表达式的含义解析为不同于$(something else) 您获得的结果将在很大程度上取决于正则表达式的风格以及sm 标志的状态。

对于替换示例,请考虑以下来自一些主要正则表达式风格的 Bash 脚本输出:

#!/bin/bash

echo "perl"
printf  "123\r\n" | perl -lnE 'say if s/$.*/X/mg' | od -c
echo "sed"
printf  "123\r\n" | sed -E 's/$.*/X/g' | od -c
echo "python"
printf  "123\r\n" | python -c "import re, sys; print re.sub(r'$.*', 'X', sys.stdin.read(),flags=re.M) " | od -c
echo "awk"
printf  "123\r\n" | awk 'gsub(/$.*/,"X");1' | od -c
echo "ruby"
printf  "123\r\n" | ruby -lne 's=$_.gsub(/$.*/,"X"); print s' | od -c

打印:

perl
0000000    X   X   2   X   3   X  \r   X  \n                            
0000011
sed
0000000    1   2   3  \r   X  \n              
0000006
python
0000000    1   2   3  \r   X  \n   X  \n                                
0000010
awk
0000000    1   2   3  \r   X  \n                                        
0000006
ruby
0000000    1   2   3   X  \n                                            
0000005

【讨论】:

Re (3) 出于讨论的目的,假设 单行 仅输入,换行符(多行)的存在会导致 $ 的行为发生变化和.,正如您所说,但这些是我的问题附带 - 我也已在问题中添加了此说明。 否则,这都是很好的信息,但我希望回答的问题是:使用全局匹配,如果正则表达式根据定义消耗了 整个 输入第一场比赛,为什么引擎会继续寻找更多的比赛? 我将多行输入更改为单行。关于为什么引擎会继续寻找更多的匹配项?因为单独定义$ 是有用的,单独定义.* 是有用的。正则表达式 $.* 没有用。对于正则表达式引擎设计人员来说,为该正则表达式提出不同的行为或对其进行优化可能不值得。 Point take re $.*,但我之前的 cmets 大约是 .* / .*$global 匹配/替换(对不起,应该说清楚 - 在事后看来,我应该问两个单独的问题)。 +1,但您能否将您之前的评论添加到您的答案顶部(说排除无意义的 $&lt;expr&gt; 正则表达式可能不值得实施努力)?【参考方案3】:

使用带有全局修饰符的.* 背后的原因是什么?因为有人希望以某种方式返回一个空字符串作为匹配项,或者他/她不知道* 量词是什么,否则不应设置全局修饰符。 .* 没有 g 不会返回两个匹配项。

这种行为的好处并不明显。

应该没有好处。实际上,您是在质疑零长度匹配的存在。您在问为什么存在零长度字符串?

我们有三个有效位置存在零长度字符串:

主题字符串的开始 两个字符之间 主题字符串结束

我们应该寻找原因,而不是使用 .*g 修饰符(或搜索所有匹配项的函数)输出第二个零长度匹配输出的好处。输入字符串后面的零长度位置有一些逻辑用途。下面的状态图是从 debuggex 中针对.* 抓取的,但我在从启动状态到接受状态的直接转换中添加了 epsilon 以演示定义:

(来源:pbrd.co)

这是一个零长度匹配(阅读更多关于epsilon transition)。

这些都与贪婪和不贪婪有关。如果没有零长度位置,像.?? 这样的正则表达式就没有意义。它不会先尝试点,而是跳过它。为此,它匹配一个长度为零的字符串,以将当前状态转换为临时可接受的状态。

如果没有零长度位置 .?? 永远不会跳过输入字符串中的字符,这会产生全新的风格。

贪婪/懒惰的定义导致零长度匹配。

【讨论】:

使用带有全局修饰符的 .* 的原因是什么? 如问题脚注 [3] 中所述,在某些语言中,基于正则表达式的功能 默认情况下并且总是使用全局匹配,所以如果你必须选择你不会opt-into全局匹配,你有时不能选择退出 全局匹配是一项额外功能,几乎可以在所有语言中启用。即使在 powershell 中也不会总是发生这种情况,会有一个选项或单独的函数以另一种方式工作。 是的,它总是在 PowerShell 中使用 -replace 发生(虽然您可以解决这个问题直接使用 Regex类型,这是题外话)。除此之外,我的问题是:对于全局匹配,如果一个正则表达式根据定义消耗了 整个 输入,为什么它会继续寻找 更多匹配?因此,我的问题不是 为什么存在零长度字符串? 如果一个正则表达式根据定义消耗了整个输入,为什么还要继续寻找更多的匹配项?有两个原因:1)存在零长度位置。 2)你有全局修饰符。 @mklement0 同样根据你自己的推理,我想知道如果我们换边'a' -replace '.*|a', '[$&amp;]',你对交替的期望是什么?还可以防御吗? (a 从不匹配)【参考方案4】:

注意:

我的问题帖子包含两个相关但不同的问题,我现在意识到,我应该为此创建单独的帖子。 此处的其他答案分别关注一个问题,因此该答案部分提供了路线图,说明哪些答案解决了哪些问题

至于为什么允许使用诸如$&lt;expr&gt; 之类的模式(即,在输入的end 之后匹配某些东西)/什么时候有意义:

dawg's answer 认为诸如$.+ 之类的荒谬组合可能 不会出于实用 的原因而被阻止;排除它们可能不值得。

Tim's answer 展示了在$ 之后某些 表达式可以 是如何有意义的,即否定后向断言

ivan_pozdeev's answer 答案的后半部分有力地综合了 dawg 和 Tim 的答案。


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

revo's answer 包含有关零长度(空字符串)匹配的重要背景信息,这就是问题最终归结为的原因。

让我通过更直接地将其与 global 匹配上下文中的行为与我的期望相矛盾的方式联系起来来补充他的答案:

从纯粹的常识的角度来看,按理说,一旦输入在匹配时被完全消耗,根据定义,什么都没有了,所以有没有理由寻找更多的匹配。

相比之下,大多数正则表达式引擎会考虑输入字符串的最后一个字符之后的字符位置 - 在某些引擎中,该位置称为主题字符串的结尾 - 一场比赛的有效起始位置,因此尝试另一场比赛

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

相反,如果正则表达式不(也)匹配空字符串,您将不会看到额外的匹配 - 虽然在所有情况下仍然尝试了额外的匹配,但不会找到匹配在这种情况下,假设空字符串是主题字符串末尾位置唯一可能的匹配项。

虽然这提供了对行为的技术解释,但它仍然没有告诉我们为什么匹配最后一个字符被实现之后。

我们所拥有的最接近的是Wiktor Stribiżew 在评论中的有根据的猜测(已添加重点),这再次表明了这种行为的实用原因: p>

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

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


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

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

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

是否遵守 POSIX ERE 规范的 longest leftmost rule谢谢,revo.

在全局匹配中:

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

主题字符串结尾位置的匹配仅限于那些在字符位置继续匹配的引擎em> 匹配。

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

PS> 'a1' -replace '\d*|a', '[$&]'
[]a[1][]

即:

\d* 匹配空字符串 before a a 本身然后 not 匹配,这意味着字符位置在空匹配之后 advanced1\d* 匹配 主题字符串的结尾位置再次与\d* 匹配,导致另一个空字符串匹配。

Perl 5 是一个确实相同字符位置恢复匹配的引擎示例:

$ "a1" | perl -ple "s/\d*|a/[$&]/g"
[][a][1][]

注意a 也是如何匹配的。

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

$ "a1" | perl6 -pe "s:g/\d*|a/[$/]/"
[a][1][]

看起来,如果一个交替发现 both 和一个非空匹配,则只报告非空匹配。

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

虽然sedawk 也可以,但它们不会在字符串末尾尝试另一个匹配:

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

$ echo a1 | sed -E 's/[0-9]*|a/[&]/g'
[a][1]

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

$ echo a1 | awk '1  gsub(/[0-9]*|a/, "[&]"); print '
[a][1]

【讨论】:

正则表达式世界中有一条规则叫做leftmost最长匹配。 Perl 6 似乎紧随其后。这是一个 POSIX 标准。 Sed 和 awk 也随之而来。 \d* 不会在偏移量0 处产生匹配,因为另一侧的a 会产生比\d* 更长的匹配。总的来说,这是一个很好的总结答案。然而,有些陈述没有权威参考支持,例如来自 dawg 或来自 Wiktor Stribiżew 的那个。 @revo Posix 最左边的最长匹配似乎非常重要,我认为应该是答案的一部分。计算领域的又一个问题。 @js2010,我认为这种行为不一定与 POSIX ERE 规范的 leftmost longest rule 相矛盾,因为它适用于 single 匹配行为。相比之下,手头的问题是为什么在 global 匹配中,会尝试 another 匹配,即使字符串已经被完全使用。 @revo,我在底部添加了额外的示例,我得出结论,遵守最左边最长的匹配规则独立于在-结束时再次匹配字符串行为:所有 Perl 6、sedawk 似乎都遵守最左边最长的规则,但只有 Perl 6(和不遵守最左边最长规则的 Perl 5)也在结尾处再次匹配字符串。【参考方案5】:

“字符串末尾的空”是正则表达式引擎的单独位置,因为正则表达式引擎处理输入字符之间的位置:

|a|b|c|   <- input line

^ ^ ^ ^
positions at which a regex engine can "currently be"

所有其他位置都可以描述为“在第 N 个字符之前”,但对于结尾,没有可参考的字符。

根据Zero-Length Regex Matches -- Regular-expressions.info,还需要支持零长度匹配(并非所有正则表达式都支持):

例如正则表达式 \d* 超过字符串 abc 将匹配 4 次:在每个字母之前和结尾。

$ 允许在正则表达式中的任何位置进行统一: 它被视为相同的 as any other token 并匹配那个神奇的“字符串结尾”位置。使其“最终确定”正则表达式工作将导致引擎工作出现不必要的不​​一致,并阻止其他可以匹配的有用的东西,例如向后看或\b(基本上,任何可以是零长度匹配的东西)——即既是设计复杂性又是功能限制,没有任何好处。


最后,回答为什么正则表达式引擎可能会或可能不会尝试在同一位置“再次”匹配,让我们参考Advancing After a Zero-Length Regex Match -- Zero-Length Regex Matches -- Regular-expressions.info:

假设我们有正则表达式\d*|x,主题字符串x1

第一个匹配是字符串开头的空白匹配。现在,我们如何在不陷入无限循环的情况下给其他代币一个机会?

大多数正则表达式引擎使用的最简单的解决方案是在上一个匹配结束后开始下一个匹配尝试一个字符

这可能会产生违反直觉的结果——例如上面的正则表达式将在开始时匹配'',在最后匹配1''——但不是x

Perl 使用的另一种解决方案是总是在前一个匹配结束时开始下一个匹配尝试,无论它是否为零长度。如果它是零长度,引擎会记下这一点,因为它不允许在同一位置进行零长度匹配。

哪些“跳过”匹配较少,但代价是一些额外的复杂性。例如。上面的正则表达式最后会产生''x1''

文章继续表明这里没有建立最佳实践,各种正则表达式引擎正在积极尝试新方法以尝试产生更“自然”的结果:

一个例外是 JGsoft 引擎。 JGsoft引擎前进一 零长度匹配后的字符,就像大多数引擎一样。但它有 一个额外的规则来跳过零长度匹配的位置 上一场比赛结束了,所以你永远不会有一个零长度的比赛 紧邻非零长度匹配。在我们的例子中 JGsoft 引擎只找到两个匹配项: 字符串的开头和 1.

零长度匹配后的 Python 3.6 和更早版本。 gsub() 搜索和替换函数跳过零长度匹配 前一个非零长度匹配结束的位置,但是 finditer() 函数返回这些匹配项。所以一个搜索和替换 Python 提供与 Just Great Software 应用程序相同的结果, 但列出所有匹配项会在末尾添加零长度匹配项 字符串。

Python 3.7 改变了这一切。它像 Perl 一样处理零长度匹配。 gsub() 现在确实替换了与 另一场比赛。这意味着可以找到的正则表达式 零长度匹配在 Python 3.7 和之前版本之间不兼容 Python 版本。

PCRE 8.00 及更高版本和 PCRE2 处理零长度匹配,如 Perl 回溯。他们不再在零长度后推进一个字符 像 PCRE 7.9 过去那样匹配。

R 和 php 中的正则表达式函数是基于 PCRE 的,因此它们避免了 通过像 PCRE 那样的回溯而陷入零长度匹配。 但是在 R 中搜索和替换的 gsub() 函数也会跳过 零长度匹配前一个非零长度的位置 匹配结束,就像 Python 3.6 和之前版本中的 gsub() 一样。另一个 R 中的正则表达式函数和 PHP 中的所有函数都允许 紧邻非零长度匹配的零长度匹配, 就像 PCRE 本身一样。

【讨论】:

谢谢(+1); expression-after-$ 解释对我来说很有意义(从某种意义上说,它有力地综合了 Tim 和 dawg 的答案)。 (防止无意义模式的一个潜在好处是提醒用户注意这一事实,但我知道这可能不值得。) 至于再次匹配问题:.NET 正则表达式引擎是 notsame 匹配的示例在空匹配后再次定位,但在我的问题前提下的 nonempty 匹配之后,它也会在字符串的末尾再次匹配。 (实际上,它是在引入复杂性的空匹配之后再次在同一位置匹配,因为您需要防止无限循环)。纯粹从逻辑上讲,在最后一个字符之后对我来说听起来不像个字符之间,因为没有第二个参考点。那么为什么要一视同仁呢? @mklement0 我发现了另一个可以匹配到最后的有用案例 @mklement0 "在最后一个字符对我来说听起来不像是在字符之间" -- 如果这能让你感觉更好,就说它是"在字符边界":- ) 是的,如果你通过字符边界(也包括第一个字符之前的位置),那么对待它们都一样是内部一致。但在决定是否再次匹配方面,它仍然违反常识。我得到了关于如何在 empty 匹配后继续进行的行为差异,但我的(第二个)问题的前提是 nonempty 第一次匹配,其中主要引擎(.Net 、Node.js、Python 2/3、Ruby、Perl、Perl 6、PCRE) 的作用相同。 (并且在最后一个 empty 匹配,无限循环预防逻辑会阻止找到另一个匹配。)【参考方案6】:

我不知道混乱从何而来。 正则表达式引擎基本上是愚蠢。 他们就像 Mikey,他们什么都吃。

$ python -c "import re; print(re.findall('$.*', 'a'))"
[''] # !! Matched the hypothetical empty string after the end of 'a'

您可以在$ 之后放置一千个可选表达式,它仍然会匹配 EOS。引擎是愚蠢的。

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

这样想,这里有两个独立的表达式.* | $。原因是第一个表达式是可选的。 它恰好与 EOS 断言相抵触。 因此,您在非空字符串上获得 2 个匹配项。

为什么设计用于查找正则表达式的多个非重叠匹配的功能 - 即全局匹配 - 如果知道整个输入已经被消耗,甚至决定尝试另一个匹配,

在字符位置不存在称为断言的类。 它们仅存在于 BETWEEN 个字符位置。 如果它们存在于正则表达式中,则您不知道是否已使用整个输入。 如果它们可以作为一个独立的步骤满足,但只有一次,它们将匹配 独立。

请记住,正则表达式是一个 left-to-right 命题。 还要记住,引擎是愚蠢的。 这是设计使然。 每个构造都是引擎中的一个状态,它就像一个管道。 增加复杂性肯定会导致失败。

顺便说一句,.*a 实际上是从头开始检查每个字符吗? 否。.* 立即从字符串(或行,取决于)的末尾开始并开始 回溯。

另一个有趣的事情。我看到很多新手在他们的结尾处使用.*? 正则表达式,认为它将从字符串中获取所有剩余的 kruft。 它没用,它永远不会匹配任何东西。 即使是独立的 .*? 正则表达式也总是不匹配尽可能多的字符 字符串中有。

祝你好运!别担心,正则表达式引擎只是......好吧,愚蠢

【讨论】:

谢谢,但 regex 引擎很愚蠢 并不是一个令人满意的解释。至于查找全部/替换行为:只是 .* 本身会产生相同的结果,我只是添加了 $ 以更明显地表明我希望 everything 被匹配。不尝试在定义为 输入结尾 的地方进行另一次匹配不会增加复杂性。如前所述,即使 没有 断言,该行为也会浮出水面,但为什么引擎不知道与 $ 匹配的内容(至少使用单行输入)已经消耗了 all 的输入? $ 匹配最后一个字符之后,对吧? @mklement0 - 嘿,伙计,对不起,你有这种感觉,只是说实话。在我的辩护中,我确实添加了一大堆东西。我认为.*$ 是一个很好的例子。如果.* 可以匹配它是在$ 的条件下,其中$ 本身不是作为独立项匹配的。 $ 本身,一次只能匹配一个地方。更奇怪的是,$ 可以在换行符之前匹配 之后。使用此目标"abc\n",并使用.*$,实际上有 3 个匹配项。 "abc&lt;1&gt;\n" abc,"abc&lt;2&gt;\n" 在换行之前,"abc\n&lt;3&gt;" 在换行之后。祝你好运 ! regex101.com/r/YNRSJk/1 我也很诚实 - 没有难过的感觉:我感谢你的努力,只是碰巧他们没有说服我。我的问题的前提是 single-line 输入,因此 $ 定义匹配输入的最末端(.NET 有 \z 匹配多行输入的绝对末端输入,例如,但我不想进入)。我的困惑仍然没有解决:为什么在输入的最后匹配(再次),这不是 between 字符位置,因为 没有字符出现在之后。跨度> @mklement0 - 引擎的主要指令是永远不要在同一位置匹配两次。根据起始位置消耗匹配。结束位置的唯一相关性是它是下一个匹配的开始,从最后一个匹配的结束和下一个字符之间开始。在这种情况下,这恰好是$ 的物理位置,不能被消费。因此,引擎按照描述设置新位置,发现它可以忽略.* 并继续匹配$。就这么简单。

以上是关于为啥正则表达式引擎允许/自动尝试在输入字符串的末尾进行匹配?的主要内容,如果未能解决你的问题,请参考以下文章

正则表达式——正则表达式的匹配过程

来聊聊正则表达式

从字符串末尾获取所有换行符的正则表达式是啥

Python中正则表达式的使用

C#?正则表达式

C#--正则表达式