零宽断言之顺序环视和逆序环视
这是RegexBuddy的帮助文档里有一篇关于环视的原理介绍,是我能找到的为数不多的解释环视原理的文章,所以尝试翻译了一下,之前的理解都基于过客大大的正则应用之——逆序环视探索和正则匹配原理之——逆序环视深入 当然,这两篇已经说的非常之详细了,但是资料越多越不嫌多,这篇也放在这里,以后忘记了再看。因为英文很烂,所以翻译的也不是很到位,但是作为一种体验倒是很有意思,但愿以后能持之以恒的多学着翻译点东西。
Lookahead and Lookbehind Zero-Width Assertions
零宽断言之顺序环视和逆序环视
Perl 5 引用了两个非常牛逼的概念: "顺序环视" 和 "逆序环视". 它们统称为“环视”,或者叫做 “零宽断言”. 因为它们是零宽度的,就像我们之前解释的开始和结束锚点一样,
不同点在于环视可以匹配多个字符,但是它会放弃匹配然后只返回结果:匹配或者没有匹配。 这是它为啥叫断言的原因。它们不消耗字符串里的字符,只是断言是否是能匹配。
环视可以让你的正则表达式做到以前做不到的事情,或者以前需要很冗长的正则才能解决的问题。
肯定的和否定的顺序环视
如果你想匹配一些字符,这些字符后面不是跟着某些别的东西,就可以使用否定顺序环视。之前解释字符组时,我已经解释了为什么你不能用否定字符组去匹配 后面没有跟着"u"的"q".
否定顺序环视提供了解决方法: q(?!u). 否定顺序环视结构使用一对圆括号,前一个括号后面跟着问号和感叹号,在环视结构里面,我们放上可怜的 u
顺序肯定环视和否定环视的工作方式差不多。q(?=u) 只匹配后面有u的q, 如果没有u就不会被匹配到。肯定顺序环视结构使用一对圆括号,前一个括号后面跟着问号和等于号。
你可以在顺序环视里放任何有效的正则表达式(注意 逆序环视可不是这样,我后面会解释到)。如果里面包含了捕获组, 那么反向引用也会被保存下来,注意顺序环视本身是不会
创建反向引用的, 所以也不会参与反向引用。如果你保存顺序环视里面匹配给反向引用使用的话,你可以用捕获括号把顺序环视结构里面包起来,像这样 (?=(regex)).
其他的途径都是无效的,因为顺序环视在反向引用保存的之后就已经丢弃了匹配的结果。
正则引擎内幕
首先,让我们看看引擎怎么用 q(?!u) 去匹配 字符串 Iraq. 正则表达式的第一个标记是字符q. 我们已经知道, 引擎会遍历字符串,一直找到被匹配的q,这时候引擎定位在 q的后面
的位置,接下来的标记是顺序环视,引擎现在注意到环视结构了,然后开始匹配环视结构里的正则,所以接下来的标记是环视里的u。它不匹配字符串最后面的空位。
引擎报注意到环视结构里的正则匹配失败了。因为顺序环视是否定的,这意味着顺序环视在当前位置匹配成功。这时候,整个表达式匹配完成,q最为匹配结果返回。
让我们尝试看看同样的正则去匹配 quit. q 匹配 q. 接下来的标记是顺序环视里的u. 接下来的字符也是u, 它们匹配成功,引擎前进到下个字符i,但是,lookahead里的正则成功了,
引擎会记录成功并且抛弃掉顺序环视的match结果,这使得引擎回退到字符u
因为这个顺序环视是否定的,内部成功的匹配意味着顺序环视本身是失败的。 因为正则里没有别的匹配可能性,引擎会重新开始匹配。因为q不能在任何其他地方匹配到,引擎最终会报告失败。
让我们再来看看更多细节,保证你弄明白顺序环视。 我们用 q(?=u)i 匹配 quit。 我使用了肯定顺序环视,然后在后面放了一个字符。 接着, q 匹配 q ,u匹配u,
顺序环视的匹配必须丢弃掉,所以引擎从u回退到i, 顺序环视是成功的,所以引擎从i开始继续匹配,但是i不能匹配u。 这轮匹配尝试失败了。其他剩下的尝试最后都将失败,
因为在字符串里没有qi了。
肯定和否定的逆序环视
逆序环视拥有和顺序环视相同的效果, 只是工作方式相反。 它会告诉引擎临时的在字符里进行回退, 去检查字符串能否逆序环视中被匹配。 使用否定逆序环视(?<!a)b 匹配一个前面没有
“a”的“b”,它不会匹配 "cab", 但是会匹配在bed或者debt里面的b(只匹配b),(?<=a)b(肯定逆序环视)匹配cab里面的b(仅匹配b),但是不匹配 bed或者debt里面的b
更多引擎的内幕
让我们用 (?<a)b 匹配 thingamabod, 引擎开始于正则里的逆序环视和字符串的首个字符,在这里,逆序环视告诉引擎回退一个字符,然后看看 "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 看起来要清楚的多。
而且用这个正则(\b\w+[^s]\b)来匹配 john's时, 它还会匹配 john 和 后来的 john'(包含逗号),而且像"a"或者"I"之类的单个字符的单词它也不会匹配。
原因由你自己来发现吧(提示: \b匹配逗号和s之间). 正确的不使用逆序环视的正事是 \b\w*[^s\W]\b(星号代替加号,\w被放进字符组里).
就我而言,我认为逆序环视更容易看明白。 最后的正则虽然可以正常工作,但是包含了两个否定(\W在否定字符组里). 尽管正则引擎不会晕,但是一般人都会被两个否定弄糊涂。
坏消息是很多正则流派不允许你在逆序环视结构里使用所有的正则语法,因为它们没法处理逆序的正则表达式。因此,正则表达式引擎必须知道在逆序环视检查之前需要回退多少步。
因为这个原因,许多正则流派,包括perl和python,只允许固定长度的东西。你可以使用任何明确了匹配次数的的正则。这意味着你可以使用普通字符和字符组,但是不能使用重复量词和可选项,
你可以使用选择分支结构,但是所有的选项都必须拥有相同的长度(未确定)
还有一些流派,像PCRE和Java对上面所说的都支持,再加上不同长度的选择分支结构。只不过重复数量一定要有上限。这意味着你可以依然不能使用星号和加号,
但是可以使用问号和带有最大参数的花括号。 这些正则流派事实上可以用选择分支结构来模拟固定长度量词,不幸的是,JDK1.5和1.5在你在逆序环视里使用使用选择结构都有一些bug,
这些都在JDK1.6里被修复了。
只有jGoft和.net允许你在逆序环视里使用所有的正则表达式
最后,像js,ruby和tcl流派都不支持逆序环视,但是它们都支持顺序环视。
环视的原子特性
事实上,零宽度环视成就了原子特性,当环视条件满足时,引擎会忘记环视里发生的一切事情,也不会回溯到环视内部去尝试其他可能性。
唯一的特殊情况是你在环视内部使用了捕获组。但是正则引擎不会回溯到环视里,也不会在捕获组里尝试其余可能性
因为这个原因, 正则(?=(\d+))\w+\1永远不会匹配123x12, 首先环视捕获了123 并放在\1里, \w+匹配所有的字符然后回溯多次直到只匹配1,因为\1没办法在任何位置匹配,最终
\w+失败,现在正则引擎没有东西可供回溯了,然后全部的正则都匹配失败。从\d+那里创建的回溯都被丢弃了。顺序环视是不会只捕获12的
很明显,正则不会尝试字符串里更远的位置, 如果我们改变匹配的字符串成为456x56,用(?=(\d+))\w+\1 将会匹配56x56
如果你不在环视里使用捕获组,那么就没啥可担心的了,环视就是能满足条件或者不能满足,其他也没啥了。