前瞻断言和后瞻断言
零长度断言的前瞻性和后瞻性
前瞻断言 和 后瞻断言,统称为“lookaround”。零长度断言就像行开始和行结束,并单词开始和末尾。这些在前面都解释了。区别在于,lookaround实际上是匹配字符,但随后放弃了匹配,仅返回匹配或不匹配的结果,也就是true或false。这就是为什么它们被称为“断言”的原因。它们不消耗字符串中的字符,而仅声明是否可以匹配。lookaround允许我们创建精简的正则表达式,如果没有lookaround,则正则表达式将变得非常冗长。
肯定前瞻断言和否定前瞻断言
如果我们想匹配不跟随其他内容的内容,则否定前瞻断言是必不可少的。在解释字符类时,本教程说明了为什么不能使用否定的字符类来匹配q后跟u的q 。否定前瞻断言提供了解决方案:q(?!u)
。否定前瞻断言构造是一对圆括号,圆括号的开头是问号和感叹号,(?!regex)
。在前瞻断言内部,我们有一些琐碎的正则表达式regex。
肯定前瞻断言的工作原理相同。q(?=u)
匹配后跟u的q,而不使u成为匹配项。肯定前瞻断言的构造是一对圆括号,圆括号开头是问号和等号,(?=regex)
。
我们可以在前瞻断言(但不是后瞻断言,下面会讲述)中使用任何有效正则表达式。如果它包含捕获组,那么这些组也将会正常捕获,并且对它们的反向引用也将正常工作。(唯一的例外是Tcl,它将前瞻断言内的所有组都视为非捕获组。)前瞻断言本身不是捕获组。它不包括在对反向引用编号的计数中。如果要将正则表达式的匹配项存储在前瞻断言中,则必须在前瞻断言内部的正则表达式周围放置捕获括号,例如:(?=(regex))
。其他的方法是行不通的,因为在捕获组存储其匹配项时,前瞻断言的行为已经丢弃了正则表达式匹配项。
正则表达式引擎内部
首先,让我们将q(!u)
应用于字符串Iraq。然后看一下正则表达式的引擎是如何工作的。正则表达式中的第一个标记是q 。众所周知,这会导致引擎遍历字符串,直到字符串中的q匹配为止。现在,字符串中的位置是字符串之后的空白。下一个标记是前瞻断言。引擎注意到,它现在位于前瞻断言结构中,并开始与前瞻内部的正则表达式匹配。因此,下一个标记是u 。这与字符串后的空白不匹配。引擎注意到前瞻内部的正则表达式失败。因为是否定前瞻断言,这意味着前瞻已在当前位置成功匹配。此时,整个正则表达式已匹配,并且返回q作为匹配项。
让我们尝试使用相同的正则表达式应用于字符串quit。q匹配q 。下一个标记是前瞻断言中的u 。下一个字符是u 。这可以匹配。引擎前进到下一个字符:i 。但是,这是通过前瞻断言内部的正则表达式完成的。引擎记录成功,并放弃正则表达式匹配。这将导致引擎将字符串退回到u 。
由于是否定的前瞻断言,因此内部成功匹配会导致前瞻失败。由于此正则表达式没有其他排列,因此引擎必须从头开始重新启动。由于q在其他任何地方都无法匹配,因此引擎报告了失败。
让我们再看一次内部,以确保我们了解前瞻断言的含义。让我们看看将q(?=u)i
应用于字符串quit。现在,为肯定前瞻断言,并紧随其后的是另一个标记。同样,q
匹配q ,u
匹配u 。同样,必须放弃前瞻的匹配,因此引擎从字符串中的i返回到u 。前瞻成功,因此引擎继续i 。但是token i
无法和字符u匹敌。因此,此匹配尝试失败。所有其余的尝试也会失败,因为字符串中不再有q。
正则表达式q(?=u)i
永远无法匹配任何东西。它试图在同一个位置匹配u和i。如果在q后面紧接着有一个u ,则前瞻成功,但是i不匹配u 。如果紧接q之后的u以外还有其他任何内容,则前瞻将失败。
肯定后瞻和否定后瞻断言
后瞻断言具有相同的效果。它告诉正则表达式引擎暂时向后移动字符串,以检查后面的文本是否可以匹配。(?<!a)b
使用否定的后瞻断言匹配与不以 a 开头的 b 。它与cab不匹配,但与bed或debt中的b (仅b )匹配。(?<=a)b
(肯定后瞻断言)与cab中的b (仅与b )匹配,但与bed或debt不匹配。
肯定后瞻断言的构造为(?<=regex)
:一对括号,括号内有一个问号,“小于”符号和一个等号。否定后瞻断言写为(?<!text)
,使用感叹号代替等号。
更多的了解正则表达式引擎内部
让我们将(?<=a)b
应用于thingamabob 。引擎从后面的字符串和字符串中的第一个字符开始。在这种情况下,后瞻断言会告诉引擎退回一个字符,然后看a是否可以匹配。引擎无法后退一个字符,因为t之前没有字符。因此,后向查找失败,并且引擎在下一个字符h处再次启动。(请注意,在这里后面会出现负向查找。)同样,引擎暂时退后一个字符以检查是否可以在其中找到 a 。它找到一个t ,因此肯定后瞻断言再次失败。
向后查找继续失败,直到正则表达式达到字符串中的m 。引擎再次退回一个字符,并注意到a可以在那里匹配。肯定后瞻断言匹配成功。因为它是零长度,所以字符串中的当前位置保持在m处。下一个标记是b ,此处不能匹配。下一个字符是字符串中的第二个a 。引擎后退,并发现m与a不匹配。
下一个字符是字符串中的第一个b 。引擎退后一步,后瞻断言 a可以匹配成功。b匹配b ,并且整个正则表达式均已成功匹配。它匹配一个字符:字符串中的第一个b 。
关于Lookbehind的重要说明
好消息是,我们不仅可以在开始时在正则表达式中的任何位置都可以使用lookbehind。如果要查找不以“ s”结尾的单词,则可以使用\b\w+(?<!s)\b
。这绝对不同于\b\w+[^s]\b
。当应用于John's时,前者匹配John ,而后者匹配John' (包括撇号)。这个将由我们自己找出原因。(提示:\b在撇号和s之间匹配)。后者也不匹配单个字母的单词,例如“ a”或“ I”。不使用lookbehind的正确正则表达式是\b\w*[^s\W]\b
(星号而不是加号,而字符类中是\ W)。
就个人而言,我发现后面的内容更容易理解。最后一个正则表达式可以正常工作,它具有双重否定(否定字符类中的\ W)。双重否定往往会使人感到困惑。不过,并不会是正则表达式引擎迷惑。(也许Tcl除外,它会将否定字符类中的否定简写符视为错误。)
坏消息是,大多数正则表达式都不允许我们在后瞻断言中使用任何正则表达式,因为它们无法向后应用正则表达式。正则表达式引擎需要能够先确定要退后多少个字符,然后再检查后面的字符。在评估后面的字符时,正则表达式引擎需要确定后瞻断言内部的正则表达式的长度,从而判断需要在目标字符串后退多少个字符,然后像常规正则表达式一样从左向右应用后面内部的正则表达式。
许多regex风格(包括Perl,Python和Boost所使用的那些)仅允许使用固定长度的字符串。我们可以使用文字文本,字符转义符,\X以外的Unicode转义符和字符类。我们不能使用量词或反向引用。我们可以使用选择项,但前提是所有选择项都具有相同的长度。这些正则引擎通过先回退目标字符串中所需数量的字符,然后从左到右尝试正则表达式内部的正则表达式,来匹配正则表达式。
当回头看时,PCRE并不完全与Perl兼容。虽然Perl要求在后瞻断言内部具有相同长度的替代方案,但PCRE允许使用可变长度的替代方案。PHP,Delphi,R和Ruby也允许这样做。每个选择项仍必须是固定长度的。
Java通过允许有限重复来使事情更进一步。我们可以将问号和花括号与指定的max参数一起使用。Java确定后视的最小和最大可能长度。正则表达式(?<!ab{2,4}c{3,5}d)
中的后向检验有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::regex
和Tcl之类的正则,虽然都支持前瞻断言,但是它们不支持后瞻断言。自从诞生以来,JavaScript的使用时间最长。但是现在后瞻断言是ECMAScript 2018规范的一部分。截止2019年末,谷歌的Chrome浏览器是唯一支持后瞻断言的流行JavaScript实现。因此,如果跨浏览器的兼容性很重要,那么我们就不能在JavaScript中使用后瞻断言。
Lookaround是原子的
lookaround是零长度的这一事实自动使它成为原子的。一旦满足环视条件,则正则表达式引擎会忽略环视内部的所有内容。它不会在环顾四周内回溯以尝试不同的排列。
唯一有区别的情况是在lookaround中使用捕获组时。由于正则表达式引擎不会回溯到lookaround,因此它将不会尝试捕获组的不同排列。
因此,正则表达式(?=(\d+))\w+\1
从不匹配123x12 。首先,lookaround功能将123捕获到\1中。然后\w+匹配整个字符串并回溯,直到仅匹配1 。最后,\w +失败,因为\1在任何位置都无法匹配。现在,正则表达式引擎没有什么可回溯的,整个正则表达式将失败。\d+创建的回溯步骤已被丢弃。它永远不会达到前瞻中捕获的12。
显然,正则表达式引擎会在字符串中尝试其他位置。如果我们更改目标字符串,则regex (?=(\d+))\w+\1
确实匹配456x56中的56x56 。
如果我们不在lookaround范围内使用捕获组,那么所有这些都无关紧要。可以满足或不能满足环顾条件;可以满足多少方式这些都是无关紧要的。