本节书摘来自异步社区《正则表达式经典实例(第2版)》一书中的第2章,第2.13节,作者: 【美】Jan Goyvaerts , Steven Levithan著,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.13 选择最小或最大重复次数
问题描述
匹配一对XHTML标签<p>
和</p>
,以及二者之间的所有文本。在标签之间的文本也可以包含其他XHTML标签。
解决方案
<p>.*?</p>
正则选项:点号匹配换行符
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby
讨论
在实例2.12中讨论的所有量词都是贪心的(greedy,又称匹配优先),意味着它们会试图尽量多次重复,只有当后面的正则表达式不能匹配的时候才会回吐(已匹配的文本)。
这对于把XHTML(它是XML的一个版本,因此要求每个起始标签都存在匹配的结束标签)中的标签进行配对来说,可能会变得困难。看看这个简单的XHTML片段:
<p>
The very <em>first</em> task is to find the beginning of a paragraph.
</p>
<p>
Then you have to find the end of the paragraph
</p>
在该断码片段中,存在两个起始<p>
标签和两个结束</p>
标签。你想要把第一个<p>
与第一个</p>
进行匹配,因为它们标记了一个独立的段落。注意这个段落还嵌套了一个<em>
标签,因此该正则表达式不能在遇到<符号的时候就直接停止。
我们来看一个错误的解答。
<p>.*</p>
正则选项:点号匹配换行符
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby
这个错误解答中唯一的不同是它缺少了星号之后额外的问号。这个不正确的答案中使用的星号与实例2.12中讲解的星号同样贪心。
在匹配了目标文本中的第一个<p>
之后,引擎会到达‹.›。其中的点号可以匹配任意字符,其中也包括换行符。星号则把它重复0次或更多次。这里的星号是贪心的,因此‹.›会匹配直到目标文本结束的所有内容。我再重申一遍:‹.*›会吃掉你的整个XHTML文件,从第一段开始。
当‹.*›把肚子吃饱之后,引擎才会试图去在目标文本的末尾匹配‹<›。显然会失败。但是这还没完:正则引擎会进行回溯(backtrack)。
星号更喜欢占有尽可能多的文本,但是它对于不匹配任何东西(0次重复)同样也会感到十分满足。在超过量词最小次数之后的每次重复,正则表达式都会保存一个回溯位置。如果在该量词之后的正则表达式匹配失败,那么正则引擎可以回到这些位置。
当‹<›匹配失败之后,引擎会进行回溯,让‹.›放弃它匹配的一个字符。接着‹<›会再次尝试匹配,这次是文件中的最后一个字符。如果它依然失败的话,那么引擎会再一次进行回溯,在文件的倒数第二个字符处尝试匹配‹<›。这个过程会一直继续,直到‹<›匹配成功为止。如果‹<›一直没有匹配成功,那么最终‹.›会回退完所有的回溯位置,然后整体匹配宣布失败。
如果在整个回溯的过程中,‹<›的确在某个点上成功匹配了,就会接着尝试匹配‹/›。如果‹/›匹配失败的话,那么引擎会继续进行回溯。这个过程会一直重复,直到‹</p>
›可以被完整匹配为止。
那么问题在哪里呢?因为星号是贪心的,所以上面给出的错误的正则表达式会匹配XHTML文件中的第一个‹p›到最后一个‹/p›之间的所有内容。但是要想正确地匹配一个XHTML段落,我们需要的是匹配第一个‹p›与其后的第一个‹/p›。
这个时候,我们就需要使用懒惰(lazy,又称忽略优先)量词了。你可以在其后放一个问号来使任何量词变成懒惰的:‹*?›、‹+?›、‹??›和‹{7,42}?›都是懒惰量词。
懒惰量词也会进行回溯,但却是从不同的方向进行的。一个懒惰量词会重复尽可能少的次数,然后保存一个回溯位置,并且允许正则表达式继续。如果后面的正则表达式匹配失败了,那么引擎会进行回溯,此时懒惰量词会再重复一次。如果正则表达式持续回溯,那么量词会扩展直到它允许的最大重复次数,或者直到它所重复的正则表达式记号匹配失败。
‹<p>.*?</p>
›使用了一个懒惰量词来正确地匹配一个XHTML段落。当‹<p>
›匹配成功的时候,‹.?›作为懒惰量词,最初什么也不做,只是稍作停顿。如果‹</p>
›在<p>
之后立即出现,就会匹配一个空段落。如果不是这样,那么引擎会回溯到‹.?›,这次会匹配一个字符。如果‹</p>
›还是匹配失败,‹.?›会接着匹配下一个字符。这个过程会持续进行下去,直到‹</p>
›匹配成功,或者‹.?›扩展失败。因为点号会匹配任意字符,所以直到XHTML文件结束‹.*?›匹配完了所有内容,都不会出现匹配失败。
量词‹›和‹?›允许所有相同的正则表达式匹配。唯一的区别是这些可能的匹配尝试的顺序不同。贪心量词会找到最长可能的匹配。懒惰量词则会找到最短可能的匹配。
如果可能,最佳解决方案是确保只存在一个可能的匹配。在实例2.12中,如果你把所有量词都变成懒惰的,用来匹配数字的正则表达式还会匹配相同的数字。原因是这些正则表达式中拥有量词的部分和紧跟其后的部分是互斥的。‹d›匹配一个数字,而只有当下一个字符不是数字(或字母)的时候‹b›才会匹配‹d›之后的位置。
为了有助于更好地理解贪心和懒惰量词重复的操作过程,我们可以比较一下‹d+b›和‹d+?b›在几个不同目标文本之上的表现。贪心和懒惰版本会产生相同的结果,但是却会按照不同的顺序来检查目标文本。
如果我们使用‹d+b›来匹配1234,‹d+›会匹配所有的数字。接着‹b›匹配,就会找到一个完整匹配。如果我们使用‹d+?b›,‹d+?›首先只会匹配1。‹b›在1和2之间匹配失败。‹d+?›会扩展到12,但是‹b›还是会失败。这将持续下去,直到‹d+?›匹配1234,以及‹b›成功匹配。
如果我们的目标文本是1234X,第一个正则式‹d+b›依然会让‹d+›先匹配1234。但是接着‹b›会匹配失败。‹d+›回溯到123。‹b›还是会匹配失败。这会继续下去,直到‹d+›回溯到最小可能的1,‹b›仍然匹配失败。这样整体匹配尝试就会宣告失败。
如果我们使用‹d+?b›来匹配1234X,‹d+?›首先只会匹配1。‹b›在1和2之间匹配失败。‹d+?›会扩展到12,但是‹b›还是会失败。这将一直继续,直到‹d+?›匹配1234,‹b›仍然匹配失败。正则引擎会试图再一次扩展‹d+?›,但是‹d›无法匹配X。整体匹配尝试宣告失败。
如果我们把‹d+›放到单词边界之中,那么它必须匹配目标文本中的所有数字,否则就会匹配失败。把量词变成懒惰的,并不会影响正则匹配最终是成功还是失败。事实上,‹bd+b›更好,因为不用任何回溯。下一个实例会解释如何可以使用一个占有量词‹bd++b›来达到这个目标,至少在某些流派中,这样是可行的。