lookahead and lookbehind的概念
假设有字符串“ABC” ,如果我们要匹配B,可以有两种描述方法:
lookahead:即后面跟着“C”的字符串。
lookbehind:即前面以“A”开头的的字符串。
这两种描述都能匹配到“B”,只是描述的条件不一样。
lookahead
我们知道正则表达式中“|”等价于 OR, 即[aaa|bbb],同时可以匹配 aaa或bbb。那么正则表达式中是否有对应的类似 AND 的操作呢,例如,我们验证一个密码强度是否同时满足以下几个要求:
- 必须包含小写字母
- 必须包含大写字母
- 必须包含数字
- 必须包含特殊符号
- 长度必须大于8
通常,对于上面的几个要求,我们都是把这5个要求拆分成5个单独的正则表达式来进行验证,但是现在要求你用一行正则搞定,怎么办呢? 答案就是正则中的“ a non-consuming regular expression”,其中 lookahead就是最常用的一种,又可分为positive lookahead assertion (?=expr) 和negative lookahead assertion(?!expr)。至于什么是non-consuming regular expression,后面我会举例说明。
positive lookahead
典型语法:(?=expr)
假设有字符串:1212aa21bb ,我们需要匹配以“aa”结尾的数字,则可以使用positive lookahead,如下:
字符串 | 正则 |
1212aa21bb | \d*(?=aa) |
匹配结果:
1 | 1212 |
这就是positive lookahead的效果,后面的“(?=aa)”等价于一个非捕获分组,只是作为一个捕获的条件,但是却不把捕获的内容输出。
除此之外,lookahead作为“a non-consuming regular expression”,还有一个重要的特性,就是“ non-consuming”,类似于正则中的位置匹配符:”^、$、\A”,就属于“zero-length matches”,意思是,当lookahead遇到匹配的字符后,下一次匹配并不会从当前匹配字符的下一个字符进行匹配。这个解释听着确实莫名其妙,我们举个例子说明:
字符串 | 正则 |
aaaaaaaa | aa(?=aa) |
那么会输出几个匹配结果呢?你可能会说是2个,但实际是3个,如下:
1 | aa |
2 | aa |
3 | aa |
使用正则在线工具:https://regexr.com/ ,可以看到这三个匹配结果的位置如下:
原因:由于lookahead是zero-length matches匹配,前面我们说这是“ non-consuming”的,意思是lookahead并不会消耗匹配的字符。当正则第一次匹配到“aaaa”后,第二次匹配的开始位置并不是索引4,而是索引2,可见第一次匹配的“aaaa”后,并没有消耗索引2和索引3上
字母a,这就是为什么会有三个匹配结果的原因。
利用lookahead的“ non-consuming”特性,那么上面的验证密码强度的如下写法(javascript):
var strongRegex = new RegExp("^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})");
REGEX | DESCRIPTION |
---|---|
^ | 从字符串头部开始匹配 |
(?=.*[a-z]) | 字符串至少包含一个小写字母 |
(?=.*[A-Z]) | 字符串至少包含一个大写字母 |
(?=.*[0-9]) | 字符串至少包含一个数字 |
(?=.*[!@#\$%\^&\*]) | 字符串至少包含一个!@#$%^&*中的一个 |
(?=.{8,}) | 字符串长度必须大于等于8 |
为什么可以这么写呢?
这是因为positive lookahead是non-consuming的,即上面五个正则中的任意一个匹配,都不会消耗字符,所以每个正则都会从字符串的头部位置开始匹配。如果不使用positive lookahead(即把上面正则中的“?=”)去掉,则在匹配第一个分组“(.*[a-z])”的时候,就可能会把整个字符串给消耗了(假设密码为:121@ARdsewr),导致后面4个正则无法得到匹配结果,从而导致整个字符串匹配失败。
需要注意的是,这五个正则的顺序是可以随机的(逻辑层面上)。但前提是必须保证在五个正则执行完之前,字符串不会被消耗掉,举例:
正则:^Start (?=.*kind)(?=.*good).* deed$
可以匹配:
Start with a good word and end with a kind deed
或者
Start with a kind word and end with a good deed
如果我们将正则改为:^Start (?=.*kind).*(?=.*good) deed.$
则上面两个句子都不会匹配到,这是因为“.*“会把整个字符串给消耗掉,导致在匹配“(?=.*good) ”时,已经没剩下任何字符了,所以就匹配失败了。
negative lookahead
典型语法:(?!expr)
意思与positive lookahead相反,其他的属性基本一样。
举例:假设我们需要匹配那些不以“aa”结尾的数字,实现如下
字符串 | 正则 |
11aa22dd | \d*(?!aa) |
匹配结果:
1 | 22 |
lookbehind
lookahead是用字符串后面跟的字符来做条件,而lookbehind是以字符串前面跟的字符来做条件。
(?<=expr) – positive lookbehind : 匹配前面有expr的字符串
(?<!expr) – negative lookbehind :匹配前面没有expr的字符串
实例
假设有字符串: foobarbarfoo
:
bar(?=bar) finds the 1st bar ("bar" which has "bar" after it) bar(?!bar) finds the 2nd bar ("bar" which does not have "bar" after it) (?<=foo)bar finds the 1st bar ("bar" which has "foo" before it) (?<!foo)bar finds the 2nd bar ("bar" which does not have "foo" before it)
参考: