0%

正则表达式 - 分组 & 前瞻 & 后瞻

之前在处理短信验证码问题的时候,碰到了关于验证码正则的问题,其中涉及到正则表达中的 前瞻(lookahead)后瞻(lookbehind),借此机会总结一下正则表达式的这种高级用法。

问题分析

先看以下案例:

1
2
3
4
您的验证码为 1247 ,请注意查收。
您的验证码为1247,请注意查收。
Your verification code is 1247 .
您的验证码为 124789 ,请注意查收。

针对以上 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
2
3
4
5
6
7
8
9
10
11
12
13
对于 (A(BC)(DE)F)G,我们可以在最外层先设一对圆括号:

((A(BC))((DE)F))G ->0
↓ ↓
((A(BC))((DE)F)) ->1 G
↓ ↓
(A(BC)) ->2 ((DE)F) ->4
↓ ↓ ↓ ↓
A (BC) ->3 (DE) ->5 F
↓ ↓
BC DE

根据编号 0~5, 可以找到对应编号的分组

分组的种类

分组的种类可以分为两大类,即 捕获型分组非捕获型分组:

  1. 捕获型分组:将捕获(即匹配)到的内容放进分组中,简单来说 捕获型分组的内容是要进入分组编号 的,用 (pattern) 来表示。上面用到的分组都是捕获型分组。
  2. 非捕获型分组: 不将捕获(即匹配)到的内容放进分组中,简单来说 非捕获型分组的内容是不会进入分组编号 的,包括了 (?: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

前瞻 & 后瞻

我们一般把文本开头的方向称之为 面,文本结尾称之为 面。而 正则表达式解析引擎默认是从左往右解析的,因此对于解析引擎来说,文本尾部方向就是前方 。 其实通过英文 lookaheadlookbehind 也能快速理解 的正确含义。

前瞻后瞻 都分别包含 肯定否定,都属于 非捕获型分组。值得注意的是,并非所有的计算机语言都支持正则表达式的后瞻

  • 肯定前瞻,用 (?=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])

参考

Java 正则表达式
JavaScript 正则表达式的分组匹配
使用正则表达式找出不包含特定字符串的条目