断言用来声明一个应该为真的事实。正则表达式中,只有当断言为真时才会继续进行匹配。断言匹配的是一个事实,而不是内容。本文介绍四个断言,它们用于查找在某些内容(但并不包括这些内容)之前或之后,也就是一个位置(如\b、^、$)应该满足的一定条件(即断言),因此也称为零宽断言。


  1. 顺序肯定环视(?=exp)

    零宽度正预测先行断言,又称顺序肯定环视,断言自身出现位置的后面能匹配表达式exp。

    比如,匹配以“ing”结尾的单词前面部分(除了“ing”以外的部分(通俗说:首先,要匹配的文本必须满足此子模式前面的表达式(本例:\b\w+);其次,此子模式不参与匹配)):


    \b\w+(?=ing\b)


    以上表达式查找以下句子时,会匹配“sing”和“danc”:


    I'm singing while you're dancing.


  2. 逆序肯定环视(?<=exp)

    零宽度正回顾后发断言,又称逆序肯定环视,断言自身出现位置的前面能匹配表达式exp。

    比如,以re开头的单词的后半部分(除了re以外的部分(通俗说:首先,要匹配的文本必须满足此子模式 后面 的表达式(本例,“\w+\b”);其次,此子模式不参与匹配)):


    (?<=\bre)\w+\b


    以上表达式在查找以下句子时匹配“ading”:

    reading a book.

    假如在很长的数字中,每3位间加1个逗号(当然是从右边加起),可以在前面和里面添加逗号的部分:


    ((?<=\d)\d{3})+\b



    用以上表达式对“1234567890”进行查找,结果是“,234,567,890”。这里的逗号只是匹配需要添加逗号的位置,还没有实际添加逗号。


    下面这个例子同时使用这两种断言,匹配以空白符间隔的数字(再次强调,不包括这些空白符):


    (?<=\s)\d+(?=\s)



    前面提到过反义,用来查找不是某个字符或不在某个字符类里的字符。如果只是想要确保某个字符没有出现,但并不想去匹配它时怎么办?例如,如果想查找这样的单词---出现字符q,但是q后面跟的不是字母u。可以尝试这样:


    \b\w*q[^u]\w*\b



    以上表达式匹配包含后面不是字母u的字母q的单词。但是如果多做几次测试就会发现,如果q出现在单词的结尾,例如lraq、Benq,这个表达式就会出错。这事因为[^u]总要匹配一个字符,如果q是单词的最后一个字符,后面的“[^u]”将会匹配q后面的单词分隔符(可能是空格、句号或其它),后面的“\w*\b”将会匹配下一个单词,于是以上表达式就能匹配整个lraq fighting。


    逆序肯定环视能解决这样的问题,因为它只匹配一个位置,并不消费任何字符。现在,解决这个问题如下所示:


    \b\w*q(?!u)\w*\b




  3. 顺序否定环视(?!exp)

    零宽度负预测先行断言,又称顺序否定环视,断言此位置的后面不能匹配表达式“exp”。例如:1)匹配3位数字,而且这3位数字的后面不能是数字(通俗说:首先,要匹配的文本必须满足此子模式前面的表达式(本例:\d{3});其次,此子模式不参与匹配):


    \d{3}(?!\d)



    2)匹配不包含连续字符串abc的单词:


    \b((?!abc)\w)+\b



    如果匹配的单词是c开头、t结尾,中间有一个字符,但不能是u(也就是说,整个单词不能是cut),直接用“c[^u]t”就可以了,若中间的字符不能是a或u(也就是说,整个单词不能是cat或cut),则表达式改为“c[^au]t”。


    如果认真读过关于排除型字符组的章节的读者肯定会知道,这个表达式能匹配的只是cot之类的单词,因为中间的排除型字符组“[^au]”必须匹配一个字符。可是,如果还想匹配chart、conduct和court怎么办?最简单的想法是:去掉排除型字符组的长度限制,改成“c[^au]+t”。


    不幸的是,这样行不通,因为这个表达式的意思是:c和t之间由多于一个“除a或u之外的字符”构成,而chart、conduct和court都包含a或u。


    我们发现,其实要否定的是“单个出现的a或u”,而不仅仅是“出现的a或u”,所以才出现这样的问题。要解决这个问题,就应当把意思准确表达出来,变成“在结尾的t之前,不允许只出现一个a或u”。想到这一步,就可以用顺序否定环视(?!......)来解决。表示在这个位置向右,不允许出现子表达式能够匹配的文本,把子表达式规定为“[au]t\b”(最后的“\b”很重要,它出现在t之后,保证t是单词的结尾字母)。有了限制,匹配a和t之间文本的表达式就随意很多,可以用匹配单词字符的简记法“\w”表示,于是整个表达式变成:


    c(?![au]t\b)\w+t



    注意 这里出现的并不是排除型字符组“[^au]”,而是普通的字符组[au],因为顺序否定环视本身已经标识否定。

    进一步思考,整个匹配文本中都不能出现字符串“cat”,要怎么办呢?这个表达式应该是:


    ^(?:(?!cat).)+$



    即在文本中的任意位置,都不能出现该字符串。


  4. 4.逆序否定环视(?<!exp)

    零宽度负回顾后发断言,又称逆序否定环视,可以用(?<!exp)断言此位置的前面不能匹配表达式exp。例如,前面不是小写字母的7位数字(通俗说:首先,要匹配的文本必须满足此子模式 后面 的表达式(本例,“\d{7}”);其次,此子模式不参与匹配):


    (?<![a-z])\d{7}



    分析以下表达式,匹配不包含属性的简单html标签内的内容:


    (?<=<\w+>.*(?=<\/\1>))



    以上表达式最能表现零宽断言的真正用途。(?<=<(\w+)>)指定前缀为:被尖括号括起来的单词(比如可能是“<b>”),然后是“.*”(任意数量的字符串),最后是一个后缀(?=<\/\1>)。注意后缀里的“\/”,用到了前面提过的字符转义;“\1”则是反向引用,引用的正是捕获的第一组,即前面(\w+)匹配的内容,如果前缀实际上是“<b>”,后缀就是“</b>”。整个表达式匹配的是“<b>”和“</b>”之间的内容(再次提醒,不包括前缀和后缀本身)。


    总体而言,环视相当于对“所在位置”附加一个条件,难点就在于找到这个“位置”。这一点解决了,环视就没有什么秘密可言了。