[转载]深入浅出之正则表达式(五)

作者在 2008-07-13 16:45:33 发布以下内容

14.  向前查看与向后查看

Perl 5 引入了两个强大的正则语法:“向前查看”和“向后查看”。他们也被称作“零长度断言”。他们和锚定一样都是零长度的(所谓零长度即指该正则表达式不消耗被匹配的字符串)。不同之处在于“前后查看”会实际匹配字符,只是他们会抛弃匹配只返回匹配结果:匹配或不匹配。这就是为什么他们被称作“断言”。他们并不实际消耗字符串中的字符,而只是断言一个匹配是否可能。

几乎本文讨论的所有正则表达式的实现都支持“向前向后查看”。唯一的一个例外是Javascript只支持向前查看。

 

·        肯定和否定式的向前查看

如我们前面提过的一个例子:要查找一个q,后面没有紧跟一个u。也就是说,要么q后面没有字符,要么后面的字符不是u。采用否定式向前查看后的一个解决方案为<<q(?!u)>>。否定式向前查看的语法是<<(?!查看的内容)>>

肯定式向前查看和否定式向前查看很类似:<<(?=查看的内容)>>

如果在“查看的内容”部分有组,也会产生一个向后引用。但是向前查看本身并不会产生向后引用,也不会被计入向后引用的编号中。这是因为向前查看本身是会被抛弃掉的,只保留匹配与否的判断结果。如果你想保留匹配的结果作为向后引用,你可以用<<(?=(regex))>>来产生一个向后引用。

 

·        肯定和否定式的先后查看

向后查看和向前查看有相同的效果,只是方向相反

否定式向后查看的语法是:<<(?<!查看内容)>>

肯定式向后查看的语法是:<<(?<=查看内容)>>

我们可以看到,和向前查看相比,多了一个表示方向的左尖括号。

例:<<(?<!a)b>>将会匹配一个没有“a”作前导字符的“b”。

值得注意的是:向前查看从当前字符串位置开始对“查看”正则表达式进行匹配;向后查看则从当前字符串位置开始先后回溯一个字符,然后再开始对“查看”正则表达式进行匹配。

 

·        深入正则表达式引擎内部

让我们看一个简单例子。

把正则表达式<<q(?!u)>>应用到字符串“Iraq”。正则表达式的第一个符号是<<q>>。正如我们知道的,引擎在匹配<<q>>以前会扫过整个字符串。当第四个字符“q”被匹配后,“q”后面是空字符(void)。而下一个正则符号是向前查看。引擎注意到已经进入了一个向前查看正则表达式部分。下一个正则符号是<<u>>,和空字符不匹配,从而导致向前查看里的正则表达式匹配失败。因为是一个否定式的向前查看,意味着整个向前查看结果是成功的。于是匹配结果“q”被返回了。

我们在把相同的正则表达式应用到“quit”。<<q>>匹配了“q”。下一个正则符号是向前查看部分的<<u>>,它匹配了字符串中的第二个字符“i”。引擎继续走到下个字符“i”。然而引擎这时注意到向前查看部分已经处理完了,并且向前查看已经成功。于是引擎抛弃被匹配的字符串部分,这将导致引擎回退到字符“u”。

因为向前查看是否定式的,意味着查看部分的成功匹配导致了整个向前查看的失败,因此引擎不得不进行回溯。最后因为再没有其他的“q”和<<q>>匹配,所以整个匹配失败了。

为了确保你能清楚地理解向前查看的实现,让我们把<<q(?=u)i>>应用到“quit”。<<q>>首先匹配“q”。然后向前查看成功匹配“u”,匹配的部分被抛弃,只返回可以匹配的判断结果。引擎从字符“i”回退到“u”。由于向前查看成功了,引擎继续处理下一个正则符号<<i>>。结果发现<<i>>和“u”不匹配。因此匹配失败了。由于后面没有其他的“q”,整个正则表达式的匹配失败了。

 

·        更进一步理解正则表达式引擎内部机制

让我们把<<(?<=a)b>>应用到“thingamabob”。引擎开始处理向后查看部分的正则符号和字符串中的第一个字符。在这个例子中,向后查看告诉正则表达式引擎回退一个字符,然后查看是否有一个“a”被匹配。因为在“t”前面没有字符,所以引擎不能回退。因此向后查看失败了。引擎继续走到下一个字符“h”。再一次,引擎暂时回退一个字符并检查是否有个“a”被匹配。结果发现了一个“t”。向后查看又失败了。

向后查看继续失败,直到正则表达式到达了字符串中的“m”,于是肯定式的向后查看被匹配了。因为它是零长度的,字符串的当前位置仍然是“m”。下一个正则符号是<<b>>,和“m”匹配失败。下一个字符是字符串中的第二个“a”。引擎向后暂时回退一个字符,并且发现<<a>>不匹配“m”。

在下一个字符是字符串中的第一个“b”。引擎暂时性的向后退一个字符发现向后查看被满足了,同时<<b>>匹配了“b”。因此整个正则表达式被匹配了。作为结果,正则表达式返回字符串中的第一个“b”。

 

·        向前向后查看的应用

我们来看这样一个例子:查找一个具有6位字符的,含有“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个字符的条件后,引擎会从开始判断前的位置继续对后面的正则表达式进行匹配。

最后作些优化,可以得到下面的正则表达式:

<<\b(?=\w{6}\b)\w{0,3}cat\w*>>

 

15.  正则表达式中的条件测试

条件测试的语法为<<(?ifthen|else)>>。“if”部分可以是向前向后查看表达式。如果用向前查看,则语法变为:<<(?(?=regex)then|else)>>,其中else部分是可选的。

如果if部分为true,则正则引擎会试图匹配then部分,否则引擎会试图匹配else部分。

需要记住的是,向前先后查看并不实际消耗任何字符,因此后面的thenelse部分的匹配时从if测试前的部分开始进行尝试。

 

16.  为正则表达式添加注释

在正则表达式中添加注释的语法是:<<(?#comment)>>

转载 | 阅读 4015 次
文章评论,共0条
游客请输入验证码