快速入门
教程
工具和语言
示例
参考
书评
正则表达式教程
简介
目录
特殊字符
不可打印字符
正则表达式引擎内部
字符类
字符类减法
字符类交集
简写字符类
锚点
词边界
交替
可选项
重复
分组和捕获
反向引用
反向引用,第 2 部分
命名组
相对反向引用
分支重置组
自由间距和注释
Unicode
模式修饰符
原子分组
独占限定符
前瞻和后顾
环顾,第 2 部分
让文本远离匹配
条件
平衡组
递归
子例程
无限递归
递归和限定符
递归和捕获
递归和反向引用
递归和回溯
POSIX 方括号表达式
零长度匹配
继续匹配
本网站上的更多内容
简介
正则表达式快速入门
正则表达式教程
替换字符串教程
应用程序和语言
正则表达式示例
正则表达式参考
替换字符串参考
书评
可打印 PDF
关于本网站
RSS 提要和博客
RegexBuddy—Better than a regular expression tutorial!

前瞻和后顾零长度断言

前瞻和后顾(统称为“环顾”)是零长度断言,就像本教程前面解释的 行首和行尾 以及 词首和词尾 锚点一样。不同之处在于,环顾实际上匹配字符,但随后放弃匹配,只返回结果:匹配或不匹配。这就是它们被称为“断言”的原因。它们不消耗字符串中的字符,而只断言是否可能匹配。环顾允许你创建没有它们就无法创建,或者没有它们就会变得非常冗长的正则表达式。环顾允许你创建没有它们就无法创建,或者没有它们就会变得非常冗长的正则表达式。

正向和负向前瞻

如果你想匹配后面不跟其他内容的内容,那么负向前瞻是必不可少的。在解释 字符类 时,本教程解释了为什么不能使用否定字符类来匹配后面不跟 uq。负向前瞻提供了解决方案:q(?!u)。负向前瞻构造是圆括号对,其中开括号后跟问号和感叹号。在向前瞻中,我们有平凡的正则表达式 u

正向先行断言的工作原理与此相同。q(?=u) 匹配后跟 u 的 q,但不将 u 部分作为匹配的一部分。正向先行断言结构是一对括号,其中开括号后跟问号和等号。

你可以在先行断言中使用任何正则表达式(但不能使用后行断言,如下所述)。可以在先行断言中使用任何有效的正则表达式。如果它包含捕获组,则这些组将正常捕获,并且对它们的回引将在正常情况下起作用,即使在先行断言之外也是如此。(唯一的例外是Tcl,它将先行断言中的所有组都视为非捕获组。)先行断言本身不是捕获组。它不包括在对回引进行编号的计数中。如果你想存储先行断言中正则表达式的匹配,则必须在先行断言中的正则表达式周围放置捕获括号,如下所示:(?=(regex))。反之则不行,因为在捕获组存储其匹配时,先行断言已经丢弃了正则表达式匹配。

正则表达式引擎内部

首先,让我们看看引擎如何将q(?!u)应用于字符串Iraq。正则表达式中的第一个标记是文字q。正如我们已经知道的,这将导致引擎遍历字符串,直到字符串中的q匹配为止。字符串中的位置现在是字符串之后的空位。下一个标记是先行断言。引擎注意到它现在位于先行断言结构中,并开始匹配先行断言中的正则表达式。因此,下一个标记是u。这与字符串之后的空位不匹配。引擎注意到先行断言中的正则表达式失败了。由于先行断言是否定的,这意味着先行断言已在当前位置成功匹配。此时,整个正则表达式已匹配,并且q作为匹配返回。

我们尝试将相同的正则表达式应用于 quitqq 匹配。下一个标记是前瞻中的 u。下一个字符是 u。它们匹配。引擎前进到下一个字符:i。但是,它使用前瞻中的正则表达式完成。引擎注意到成功,并丢弃正则表达式匹配。这导致引擎在字符串中向后移动到 u

因为前瞻是负的,所以它内部的成功匹配导致前瞻失败。由于没有其他此正则表达式的排列,因此引擎必须从头开始重新启动。由于 q 无法在其他任何地方匹配,因此引擎报告失败。

让我们再深入了解一下,以确保您理解前瞻的影响。我们对 quit 应用 q(?=u)i。前瞻现在为正,后面跟着另一个标记。同样,qq 匹配,uu 匹配。同样,必须丢弃来自前瞻的匹配,因此引擎从字符串中的 i 向后移动到 u。前瞻成功,因此引擎继续使用 i。但 i 无法与 u 匹配。因此,此匹配尝试失败。所有剩余的尝试也会失败,因为字符串中没有更多 q。

正则表达式 q(?=u)i 永远无法匹配任何内容。它尝试在相同位置匹配 ui。如果 q 后面紧跟 u,则前瞻成功,但随后 i 无法与 u 匹配。如果 q 后面紧跟的不是 u,则前瞻失败。

正向和负向后顾

后顾具有相同的效果,但向后工作。它告诉正则表达式引擎暂时向后移动字符串,以检查后顾中的文本是否可以在那里匹配。 (?<!a)b 使用负向后顾匹配一个没有“a”前缀的“b”。它不匹配 cab,但匹配 beddebt 中的 b(仅 b)。 (?<=a)b(正向后顾)匹配 cab 中的 b(仅 b),但不匹配 beddebt

正向后顾的构造为 (?<=文本):一对括号,左括号后跟一个问号、“小于”符号和一个等号。反向后顾写为 (?<!文本),使用感叹号代替等号。

更多 Regex 引擎内部

让我们将 (?<=a)b 应用于 thingamabob。引擎从后顾和字符串中的第一个字符开始。在这种情况下,后顾告诉引擎后退一个字符,看看是否可以在那里匹配 a。引擎无法后退一个字符,因为在 t 之前没有字符。因此,后顾失败,引擎从下一个字符 h 重新开始。(请注意,反向后顾在这里会成功。)同样,引擎暂时后退一个字符,检查是否可以在那里找到一个“a”。它找到一个 t,因此正向后顾再次失败。

后顾一直失败,直到正则表达式到达字符串中的 m。引擎再次后退一个字符,并注意到 a 可以在那里匹配。正向后顾匹配。因为它是一个零长度,字符串中的当前位置仍然停留在 m。下一个标记是 b,它无法在此处匹配。下一个字符是字符串中的第二个 a。引擎后退,发现 m 不匹配 a

下一个字符是字符串中的第一个 b。引擎后退并发现 a 满足后顾。b 匹配 b,整个正则表达式已成功匹配。它匹配一个字符:字符串中的第一个 b

关于后顾的重要说明

好消息是,您可以在正则表达式中的任何位置使用后向引用,而不仅仅是开头。如果您想找到一个不以“s”结尾的单词,可以使用 \b\w+(?<!s)\b。这绝对不等于 \b\w+[^s]\b。当应用于 John's 时,前者匹配 John,而后者匹配 John'(包括撇号)。我会让您自己弄清楚原因。(提示:\b 匹配撇号和 s 之间)。后者也不匹配“a”或“I”等单字母单词。不使用后向引用的正确正则表达式为 \b\w*[^s\W]\b(星号代替加号,字符类中使用 \W)。就我个人而言,我发现后向引用更容易理解。最后一个正则表达式工作正常,它有一个双重否定(否定字符类中的 \W)。双重否定往往会让人感到困惑。不过,对于正则表达式引擎来说却不是这样。(除了 Tcl,它将否定字符类中的否定简写视为错误。)

坏消息是,大多数正则表达式风格不允许您在后向引用中使用任何正则表达式,因为它们不能反向应用正则表达式。正则表达式引擎需要能够弄清楚在检查后向引用之前要后退多少个字符。在评估后向引用时,正则表达式引擎确定后向引用中正则表达式的长度,在目标字符串中后退那么多字符,然后从左到右应用后向引用中的正则表达式,就像使用普通正则表达式一样。

包括 PerlPythonBoost 使用的许多正则表达式风格只允许固定长度的字符串。您可以使用 文字字符转义Unicode 转义\X 除外)和 字符类。您不能使用 量词反向引用。您可以使用 交替,但仅当所有备选项具有相同长度时。这些风格通过首先在目标字符串中后退与后向引用需要的字符数相同数量的字符,然后从左到右尝试后向引用中的正则表达式来评估后向引用。

Perl 5.30 支持可变长度的后向引用作为一项实验性功能。但是,在许多情况下它不能正确工作。因此,在实践中,上述内容仍然适用于 Perl 5.30。

PCRE 在回顾时与 Perl 并不完全兼容。虽然 Perl 要求回顾中的备选方案长度相同,但 PCRE 允许可变长度的备选方案。 PHPDelphiRRuby 也允许这样做。每个备选方案仍然必须是固定长度。每个备选方案都被视为单独的固定长度回顾。

Java 通过允许有限重复将事情更进了一步。您可以使用 问号花括号,并指定 max 参数。Java 确定回顾的最小和最大可能长度。正则表达式 (?<!ab{2,4}c{3,5}d)test 中的回顾有 5 种可能的长度。它的长度可以是 7 到 11 个字符。当 Java(版本 6 或更高版本)尝试匹配回顾时,它首先在字符串中后退最小数量的字符(此示例中为 7),然后从左到右照常评估回顾中的正则表达式。如果失败,Java 会再后退一个字符,然后重试。如果回顾继续失败,Java 会继续后退,直到回顾匹配或后退到最大数量的字符(此示例中为 11)。当回顾的可能长度数量增加时,这种在主题字符串中反复后退会降低性能。请记住这一点。不要选择任意大的最大重复次数来解决回顾中缺乏无限量词的问题。Java 4 和 5 存在一些错误,导致在某些情况下应该成功时,带有交替或可变量词的回顾会失败。这些错误已在 Java 6 中修复。

Java 13 允许你在环视中使用 星号加号,以及没有上限的 花括号。但 Java 13 仍然使用 Java 6 引入的费力的环视匹配方法。如果其中一个量词是无界的,Java 13 也不会正确处理带有多个量词的环视。在某些情况下,你可能会收到错误。在其他情况下,你可能会得到不正确的匹配。因此,为了正确性和性能,我们建议你仅在 Java 6 到 13 中使用带有低上限的量词进行环视。

唯一允许你在环视中使用完整正则表达式的正则表达式引擎(包括无限重复和反向引用)是 JGsoft 引擎.NET RegEx 类。这些正则表达式引擎真正地从后向前应用环视中的正则表达式,从右到左遍历环视中的正则表达式和主题字符串。无论它有多少不同的可能长度,它们只需要评估一次环视。

最后,std::regexTcl 等风格根本不支持环视,即使它们支持前瞻。自创建以来,JavaScript 一直都是这样。但现在环视是 ECMAScript 2018 规范的一部分。截至本文撰写之时(2019 年末),谷歌的 Chrome 浏览器是唯一支持环视的流行 JavaScript 实现。因此,如果跨浏览器兼容性很重要,你不能在 JavaScript 中使用环视。

环视是原子的

环视是零长度的事实自动使其成为 原子的。一旦满足环视条件,正则表达式引擎就会忘记环视中的所有内容。它不会在环视中回溯以尝试不同的排列。

唯一在这种情况下有所不同的情况是,当你使用 捕获组 在环视中。由于正则表达式引擎不会回溯到环视中,因此它不会尝试捕获组的不同排列。

因此,正则表达式 (?=(\d+))\w+\1 永远不会匹配 123x12。首先,环视捕获 123\1。然后,\w+ 匹配整个字符串,并回溯直到仅匹配 1。最后,\w+ 失败,因为 \1 在任何位置都无法匹配。现在,正则表达式引擎没有任何内容可以回溯,并且整个正则表达式失败。由 \d+ 创建的回溯步骤已被丢弃。它永远无法达到环视仅捕获 12 的点。

显然,正则表达式引擎确实尝试字符串中的其他位置。如果我们更改主题字符串,则正则表达式 (?=(\d+))\w+\1456x56 中确实匹配 56x56

如果您不在环视中使用捕获组,那么所有这些都不重要。环视条件要么可以满足,要么不能满足。它可以满足多少种方式无关紧要。