之前在处理短信验证码问题的时候,碰到了关于验证码正则的问题,其中涉及到正则表达中的 前瞻(lookahead)
和 后瞻(lookbehind)
,借此机会总结一下正则表达式的这种高级用法。
问题分析
先看以下案例:
1 | 您的验证码为 1247 ,请注意查收。 |
针对以上 4 个例子,给出正则表达式,如何才能保证只提取其中的 4 位数验证码,并过滤掉 6 位数的验证码呢?(也就是对于情况123,提取出 1247,对于情况4,则返回无法提取。)
最开始想当然的认为这个正则很好写,[0-9]{4}
就搞定了,然而在匹配第 4 条的时候,匹配出来的结果也是 1247,显然不满足要求。然后再考虑到,4 位数字的字符串两边是有边界的,所以可以用 \b[0-9]{4}\b
来表示,然而这却没办法满足情况 2,在情况 2 下,匹配出来的结果为空。
最后的想法是,只要保证 匹配 4 位数字字符串,且其前后都不再有数字
,则可以匹配以上所有情况。要满足这样的需求,则需要引申出 分组
以及 前瞻
,后瞻
的概念。
相关概念
分组(Group)
1 | hellohellohello |
针对以上文本,我们可以用 hellohellohello
正则表达式去匹配,更好的写法是 (hello){3}
。其中被圆括号 ()
括起来的部分称之为 分组
。
分组的引用
对于一个正则表达式来说,其中的分组是有相应编号(引用)的。
对于文本 ABCDEFG
,可以用正则表达式 ((A(BC))((DE)F))G
来匹配:
编号 | 组 | 匹配内容 |
---|---|---|
0 | ((A(BC))((DE)F))G | ABCDEFG |
1 | ((A(BC))((DE)F)) | ABCDEF |
2 | (A(BC)) | ABC |
3 | (BC) | BC |
4 | ((DE)F) | DEF |
5 | (DE) | DE |
关于分组的编号,其实就是二叉树的前序遍历(根节点 -> 左子树 -> 右子树),排除其中不是分组的部分的即可:
1 | 对于 (A(BC)(DE)F)G,我们可以在最外层先设一对圆括号: |
分组的种类
分组的种类可以分为两大类,即 捕获型分组
和 非捕获型分组
:
- 捕获型分组:将捕获(即匹配)到的内容放进分组中,简单来说
捕获型分组的内容是要进入分组编号
的,用(pattern)
来表示。上面用到的分组都是捕获型分组。 - 非捕获型分组: 不将捕获(即匹配)到的内容放进分组中,简单来说
非捕获型分组的内容是不会进入分组编号
的,包括了(?:pattern)(标准的非捕获型分组)
,(?=pattern)(肯定前瞻分组)
,(?!pattern)(否定前瞻分组)
,(?<=pattern)(肯定后瞻分组)
,(?<!pattern)(否定后瞻分组)
标准非捕获型分组 - (?:pattern)
(?:pattern)
是标准的非捕获型分组。
还是以前面的文本 ABCDEFG
为例,可以用包含了标准的非捕获分组的正则表达式 (?:(A(BC))((DE)F))G
去匹配,(?:pattern)
不参与分组,也就是 (?:(A(BC))((DE)F))
不参与分组,所以:
编号 | 组 | 匹配内容 |
---|---|---|
0 | (A(BC)(DE)F)G | ABCDEFG |
1 | (A(BC)) | ABC |
2 | (BC) | BC |
3 | ((DE)F) | DEF |
4 | (DE) | DE |
前瞻 & 后瞻
我们一般把文本开头的方向称之为 前
面,文本结尾称之为 后
面。而 正则表达式解析引擎默认是从左往右解析的,因此对于解析引擎来说,文本尾部方向就是前方
。 其实通过英文 lookahead
和 lookbehind
也能快速理解 前
后
的正确含义。
前瞻
和 后瞻
都分别包含 肯定
和 否定
,都属于 非捕获型分组
。值得注意的是,并非所有的计算机语言都支持正则表达式的后瞻。
肯定前瞻
,用(?=pattern)
表示。通俗解释:匹配到的文本后面要跟着 pattern 代表的文本
,也就是说(?=pattern)
本身仅参与文本匹配时的预测,匹配到的文本不会包含pattern
的内容。比如
ab(?=cd)
,表示 匹配文本中包含的 ab 字符串,且该 ab 字符串后面要紧跟着 cd,否则无法匹配。比如该表达式能匹配到abcd
中的ab
, 但不能匹配到abef
中的ab
。注意到匹配到的文本(ab)并不会包含pattern
((?=cd)) 的内容。
其他三种 否定前瞻
, 肯定后瞻
, 否定后瞻
概念可以类比出来,就不再赘述。
前瞻 & 后瞻 匹配规则及其匹配内容举例:
表达式 | 肯定前瞻 ab(?=cd) | 否定前瞻 ab(?!cd) | 肯定后瞻(?<=ab)cd | 否定后瞻 (?<!ab)cd | |
---|---|---|---|---|---|
文本 | |||||
abcdefgh | ab | 空 | cd | 空 | |
abefcdgh | 空 | ab | 空 | cd |
问题解决
那么,针对文章开头的 匹配 4 位数字字符串,且其前后都不再有数字
的正则表达式的书写就迎刃而解了:
1 | (?<![0-9])[0-9]{4}(?![0-9]) |