lookahead and lookbehind的概念

假设有字符串“ABC” ,如果我们要匹配B,可以有两种描述方法:
lookahead:即后面跟着“C”的字符串。
lookbehind:即前面以“A”开头的的字符串。
这两种描述都能匹配到“B”,只是描述的条件不一样。

lookahead

我们知道正则表达式中“|”等价于 OR, 即[aaa|bbb],同时可以匹配 aaa或bbb。那么正则表达式中是否有对应的类似 AND 的操作呢,例如,我们验证一个密码强度是否同时满足以下几个要求:
  1. 必须包含小写字母
  2. 必须包含大写字母
  3. 必须包含数字
  4. 必须包含特殊符号
  5. 长度必须大于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)
参考:

 

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.