预测回顾断言的功能是非常强大的,但很多正则表达式的初学者不懂得充分发挥它的优势,因为这些概念的理解有些难度。最难理解的部分是预测回顾断言是零宽的,所以如果一个表达式中使用了预测回顾断言,那么那部分字符串将会被匹配两次。
我们现在用一个实例在说明这个特性。我们现在要匹配一个长度为6的单词,并且这个单词中包含连续的字符串 cat。实际上即使不用预测回顾断言我们依旧可以实现这个表达式。我们只要枚举出cat所有可能出现的位置,并且使用选择符把它们连接起来就成功了:cat\w{3}|\wcat\w{2}|\w{2}cat\w|\w{3}cat.
。这个方法很简单,但是当你想扩展它的功能时,它就显得很不方便了,例如你要匹配一个长度为6到12之间的字符串,并且字符串包含连续的字符串 cat,dog 或者 mouse。
在上面这个例子中,匹配如果要成功必须满足两个要求。第一个要求是:字符串的长度为6。第二个:这个字符串必须包含单词 cat。
第一个要求很容易完成,它的表达式为:\b\w{6}\b
。第二个要求也同样容易,它的表达式为\b\w*cat\w*\b
。
将这两个条件组合起来我们很容易就得到,(?=\b\w{6}\b)\b\w*cat\w*\b
。它的工作原理如下,引擎在匹配字符串的过程中,首先与预测断言匹配。也就是说当前位置必须处于一个长度为6点单词的开头。如果这个条件不能满足,预测断言就会失败,引擎将进行回溯并且匹配下一个位置。
注意,预测断言是零宽的,所以预测断言匹配成功后匹配位置仍然处于单词的开始位置。引擎将从这个位置开始匹配剩余的表达式。因为当前位置处于单词的开始位置,所以token\b
一定可以匹配。接下来的token\w*
会匹配到单词中的所有六个字符。但是下一个tokenc
无法匹配成功,引擎随即发起回溯。这个回溯会减少token\w*
所匹配单词的长度,回溯会发生多伦,直到 cat 可以成功地匹配。如果 cat 始终无法匹配,那么引擎会回溯到正则表达式的开头,并且从下一个字符位置重新开始匹配。也就是说引擎会从单词的第二个字符重新匹配,但是此时的位置无法满足预测断言的要求,引擎继续在字符串中向前搜索,直到遇见下一个长度为6的单词。
如果引擎可以在单词中匹配到字符 cat,那么第二个\w*
会匹配这个单词中剩余的字符(如果有剩余的字符)。最后一个token\b
会匹配单词末尾的位置。我们的“双条件”表达式就匹配成功了。
上面这个表达式的功能是满足要求的,但是它不是最优解。如果你只是在编辑器中使用这个表达式,那你不需要考虑性能的问题。但是在日常开发中正则表达经常用于匹配非常大的数据,或是调用的频率很高,在何种情况下我们就必须对表达式进行优化。
如果你仔细研究引擎处理这个表达式的过程(正如上一节中所做的),你就可以发现这其中可以优化的空间。首先表达式中第3和第4个\b
是一定会匹配成功的,并且词语边界是零宽的,它们不会改变匹配的结果,所以\b
可以省略,剩下的表达式为(?=\b\w{6}\b)\w*cat\w*
。这这个表达式中最后一个\w*
同样也是必然会匹配成功的,但是它不能省略,因为这不是一个零宽的匹配,\w*
匹配到的结果会加入最终的匹配结果。
但是第一个\w*
是可以优化的。我们知道引擎在第一次匹配这个token的时候回匹配六个字符,紧接着发生回溯。但是对于任何成功的匹配 cat 前面的字符不可能超过三个,所以我们把它优化成\w{0,3}
。注意这里使用惰性匹配并不是一个高效的优化方案。
如果单词中包含 cat,那么引擎会更早的完成匹配。但是如果单词中不包含 cat,那么引擎会在单词的第5和第6个位置尝试匹配 cat,甚至会去匹配单词之后的一个字符(如果第6个字符是 c 的话)。
我们现在得到的表达式为(?=\b\w{6}\b)\w{0,3}cat\w*
。最后一个优化项是第一个\b
。我们可以把它移到预测断言外,因为它是零宽的,并不会影响匹配结果。所以最终的结果是:\b(?=\w{6}\b)\w{0,3}cat\w*
。
你也可以把最后一个\w*
改为\w{0,3}
。但是这并不会提升效率,因为第一个\w{0,3}
就已经确定了字符串的长度,所以\w*
或是\w{0,3}
都会匹配 cat 之后的所有字符。
如果文章出现错误,请给我提Issues - - Github地址