前瞻断言和后瞻断言
断言的定义
断言的定义比较简单
一个断言就是一个对当前匹配位置之前或之后的字符的测试, 它不会实际消耗任何字符。
断言在正则中还是比较复杂的,但是它又非常重要。所以这里我们着重来说一说断言。
简单的断言代码有\b
、\B
、\A
、\z
、\A
、^
和$
等等,这些在前面的转义字符一章介绍过,这里不再介绍。下面我们来看比较复杂的断言,前瞻断言
和后瞻断言
。这两个名称是由英文翻译过来的,光看名称也不知道在说什么东西(我还是习惯用英文名称 lookahead
和 lookbehind
)。不过,名称不重要啦,我们主要看作用和实现这个功能的过程。
前瞻断言 和 后瞻断言 又分为 肯定/否定前瞻(positive/negative lookahead
)和 肯定/否定后瞻(positive/negative lookbehind
)。
实现代码
- 肯定前瞻断言:以
(?=
开始,以)
结束。代码形式(?=regex)
; - 否定前瞻断言:以
(?!
开始,以)
结束。代码形式(?!regex)
; - 肯定后瞻断言:以
(?<=
开始,以)
结束。代码形式(?<=regex)
; - 否定后瞻断言:以
(?<!
开始,以)
结束。代码形式(?<!regex)
;
不管是lookahead
还是lookbehind
,它们都可以被认为只是一个判断条件,都不会捕获匹配到的内容。如果非要捕获匹配的内容的话,可以在断言里面使用子组,比如:(?=(regex))
。
还有一点,在上面定义中有说它不会消耗字符,该怎么理解不消耗字符呢。简单来说就是在字符串的某个位置P
开始,匹配到了断言里的正则,该断言为真,当开始正则表达式下一个token匹配时,还是从字符串P
的位置开始进行匹配。
比如说,将正则表达式/(?=foot)ball/
应用于字符串football
。对于正则中的第一个token
是断言中的foot
,匹配字符串中的前四个字符——foot
,断言为真。然后开始正则中的第二个token
——b
。按照常规理解,此时应该是开始在字符串的第五个位置b
这里开始匹配,但是就因为前面是断言,所以还是从字符串的第一个位置f
处开始匹配。因为token b
不能匹配字符f
,所以匹配失败,从而导致整个匹配失败。
肯定前瞻(positive lookahead
)和 否定前瞻(negative lookahead
)。
如果想匹配某个字符串,该字符串后面不能有某些指定的字符的时候,否定前瞻断言
是你的首选方案。举个例子,我想匹配foot
,但是后面不能跟着ball
的这样一个字符串。也就是无论你后面是什么字符,只要不是ball
。/foot(?!ball)/
这能匹配到foot
后面不是ball
的任意的字符串,例如 footwhat
footshow
等。匹配出来的结果是foot
。
/foot(?=ball)/
肯定前瞻断言要匹配的是foot
后面跟着ball
。字符串football
能匹配成功,和否定前瞻是相反的。
下面我们看正则/(?!foot)ball/
,它的作用不是匹配ball
的前面不是foot
的字符串,而是可以匹配任意的含有ball
的字符串。 PHP官网也有这个例子,说法很简单,按照PHP官网的说法就是:因为 (?!foot)
这个断言在接下来四个字符是 ”ball”
的时候是永远都是true。 但是为什么永远都是true,官网没给解释。
下面补充一下
我们匹配字符串football
。正则中的第一个token
,lookahead
中的 foot,匹配字符串中的foot
,匹配成功,正则引擎移动到下一个token
,b。但是,由于是否定断言,所以lookahead
失败,字符串移动到第二个字符o
,正则引擎从正则表达式首部开始匹配,lookahead
中的 foot 匹配字符串第二个字符o
开始后面四个字符ootb
,没匹配成功,但是由于是否定前瞻断言,lookahead
成功。因此正则引擎开始第二个token
b 和字符串的第二个字符o
匹配,因为前瞻断言lookahead
不消耗字符,所以字符串的指针还是指向第二个字符o
。token
b 不能匹配 o
所以匹配失败。字符指针继续后移到第三个字符o
,正则引擎继续从正则表达式的开始部分进行匹配,第一个token
foot
从第三个字符o
开始匹配,lookahead
成功,第二个token
b
和 第三个字符o
匹配,失败。所以字符指针继续移动到下一个字符t
,重复前面的过程。直到字符b
的位置,第一个token
foot
不能和 ball
匹配,lookahead 匹配成功,下一个token
b 和 字符 b
匹配成功,正则引擎后移,下一个token
a 和 下一个字符 a
匹配成功... 从而整个正则匹配成功。
由这个匹配过程,可以得出结论,对于/(?!foot)ball/
,在匹配到ball
的字符串的位置上,(?!foot)
这个断言永远为true。所以说字符串 ball 前面是什么, football 也好,handball也好抑或是只是字符串 ball,whatever。
为了更加确认各位读者能了解前瞻断言的过程,下面我们将正则/(?=foot)ball/
应用于字符串football
。正则中我们使用肯定前瞻断言。按照一般的解读,是匹配到foot然后后面跟着ball的字符串。那football
咋一看起来是能匹配成功的。然而事实却恰恰相反,和上一个否定前瞻断言相反,什么也匹配不到。football 也好,handball也好抑或是只是字符串 ball 统统不行。
就拿football
举例,正则中的第一个token lookahead中的foot匹配字符串中的foot
,匹配成功,正则引擎移到下一个token b。 然而,前面我们说过,lookahead不消耗字符,所以字符指针还是指向第一个字符f
。token b 和 字符f
不能匹配,所以匹配失败。这时字符指针指向下一个字符o
。正则引擎继续从头开始匹配。第一个token foot 不能匹配字符o
开始的四个字符ootb
,所以lookahead失败。字符指针继续下一个字符o
,重复前面的过程直到最后也字符串中也没有能匹配token foot。所以整个正则匹配失败。
所以这就是断言不消耗字符产生的差异。是不是到了这里就对前瞻断言有了一个比较清晰的认识了。
肯定后瞻(positive lookbehind
) 和 否定后瞻 (negative lookbehind
)
有了前面前瞻断言
的匹配过程的经验,我们再看后瞻断言的时候就感觉是不是有点思路了。不同的是,后瞻断言是从字符串的当前位置向后匹配的。怎么理解向后匹配呢,其实就是相当于字符指针回溯。
举个例子, 将正则表达式/(?<=t)b/
应用于字符串catball
。首先正则引擎从第一个token lookbehind
中的 t 和字符串中的第一个字符c
开始。由于是lookbehind,这就告诉正则引擎是字符指针从字符c
开始回退一个字符,不能和t 匹配,因为c
是第一个字符,回退一个字符没有内容。又因为是 positive lookbehind,所以断言失败。因为断言不消耗字符,所以字符指针移动到字符c
的位置,然后移动到下一个字符a
。从a
的位置回退一个字符是c
不能和 t 匹配,所以断言失败,继续后移到t
,t
回退一个字符为a
,也不能和 t 匹配,断言失败。直到字符b
,回退一个字符是t
,和 token t 匹配成功,断言成功。正则引擎开始第二个token b 进行匹配。同样的,由于断言不消耗字符,所以字符串的当前位置是b
。token b 和 字符b
匹配成功,整个正则匹配成功。因为正则引擎遵循最左原则,所以到此整个正则表达式就匹配完成了。
从上面的过程大概就能看出为什么叫lookbehind了,应该不难理解向后匹配这个说法了。
为了加深理解,我们使用上面讲解lookahead的时候的例子。首先看否定后瞻断言(negative lookbehind)/(?<!foot)ball/
,匹配ball前面不是foot的字符串。和否定前瞻断言/(?!foot)ball/
不同的是它不能匹配 football,除此之外 紧跟在ball前面是任何的字符串都能匹配。而肯定后瞻断言(positive lookbehind)/(?<=foot)ball/
就只能匹配ball
前面紧跟foot
的字符串。好像这个更符合我们的常规思路。分析过程可以结合上面例子的匹配过程和前瞻断言例子中的匹配过程自行分析。
对于后瞻断言来说有个需要注意的地方,就是lookbehind中给定的正则是固定长度的。也就是说(?<=\w+)
,(?<!\w{1,})
,(?<!\w{1,4})
这类的表达式是会报错的(正则引擎不能确定这个的长度)。正则引擎一定要能确定里面的长度 比如 (?<=text)
或者 (?<=\w{4})
这种都是可以的,因为正则引擎在解析正则表达式的时候,能确定lookbehind里的长度。为什么要确定长度,很显然,对于lookbehind来说,正则引擎是要从字符串的当前位置回退固定个字符来和lookbehind中的表达式进行匹配的。如果不能确定lookbehind中表达式的长度,那就不知道该从字符串的当前位置回退几个字符。所以正则引擎就要皱着眉头摸后脑勺了。 对于lookahead
来说就不存在这个问题。所以对于正则引擎来说,还不是很全面的支持lookbehind
。
本篇中涉及的内容很重要,但是我们这里却没有代码的例子。即使有代码的例子,也无非就是写符合上面说的情况的正则,看一下执行的结果。所以这里就不浪费篇幅去写这些没太大意义的代码。主要是以分析这个匹配过程为主,下来可以根据这些过程,自己多写一些正则,然后按照这个匹配的过程去分析,这样理解才能更深刻。