之前在处理短信验证码问题的时候,碰到了关于验证码正则的问题,其中涉及到正则表达中的 前瞻(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]) |