正则表达式的先行断言与后行断言:用可交互棋盘讲清楚
环视让正则可以检查某个位置前后的内容,而不把它们纳入匹配。本文讲解四种环视如何工作、它们擅长什么,以及什么时候普通正则反而更短。

· 阅读约 4 分钟
/q(?!u)/
抓住
- iraq
- qatar
- qi
放过
- queen
- quiet
- quote
正则表达式的大部分内容都会消耗文本。cat 吃掉三个字符,\d+ 吃掉它能吃的所有数字,引擎随后越过它匹配到的内容继续前进。
环视不会。它们停在字符串的某个位置,向前或向后看一眼,然后回答“是”或“否”。它们匹配零个字符。凭这一个想法,你就能表达普通正则表达不了的东西,比如“后面不跟 u 的 q”,或者“紧跟在美元符号后面的数字,但不包括美元符号”。
下面每个示例都可以交互:修改正则,各行结果会随之更新;也可以点击试试在沙盒中打开它。
01四种环视
| 语法 | 名称 | 成立条件… |
|---|---|---|
(?=X) |
正向先行断言 | X 紧随其后 |
(?!X) |
负向先行断言 | X 不紧随其后 |
(?<=X) |
正向后行断言 | X 紧挨在前面 |
(?<!X) |
负向后行断言 | X 不紧挨在前面 |
名称告诉你方向:先行检查当前位置之后的文本,后行检查它之前的文本。= 表示“必须存在”,! 表示“必须不存在”。
02负向先行断言:后面没有 u 的 q
在英语中,q 后面几乎总是跟着 u。用负向先行断言,一行就能抓住例外:
注意 iraq。一个诱人的替代写法是 q[^u],即“q 后面跟着除 u 以外的任意字符”。但 [^u] 必须匹配一个字符,而 iraq 里的 q 是最后一个字母。先行断言只要求下一个位置不是 u,而字符串末尾当然不是 u。
03正向先行断言:匹配一部分,检查下一部分
正向先行断言让你可以要求某种上下文,却不把它包含进匹配结果。这里我们想要数字,但只要表示美元金额的那个:
只要你在替换或提取文本,这一点就很重要。用 \d+(?= dollars) 做查找替换,改掉的是数字,“dollars”这个词则原封不动。
04叠加先行断言:同时满足多条规则
因为先行断言不会移动位置,你可以在同一个位置放好几个。每一个都在整个字符串上检查一条不同的规则。这就是经典的密码检查:至少一个数字、至少一个大写字母、八个或更多字符:
从左往右读:从开头起,前方某处有一个数字;从同一个开头起,前方某处有一个大写字母;然后匹配 8 个或更多字符直到结尾。顺序无关紧要,这正是先行断言胜过把每种排列都写出来的原因。
05后行断言:检查前面是什么
后行断言是镜像。(?<=\$)\d+ 匹配紧跟在美元符号后面的数字,但不包括 $ 本身:
而否定形式可以找出不是价格的数字:
最后那条说明里藏着一个真正的陷阱。如果没有单词边界,(?<!\$)\d+ 会毫不犹豫地匹配 $100 里的 00,因为那两个 0 并不紧跟在 $ 后面。后行断言只检查匹配开始的位置,所以你常常需要 \b 或锚点来把这个位置钉住。
06环视在高尔夫中什么时候占优,什么时候不占优
在正则高尔夫里,每个字符都算数,所以值得知道环视什么时候能回本。
对于任意顺序出现的两个字母,普通的分支更短:a.*e|e.*a 是 9 个字符,而 (?=.*a).*e 是 10 个。但排列的数量增长得很快(两个字母 2 种,三个 6 种,四个 24 种),而每多一个先行断言只固定多花七个字符。下面这个棋盘里,每个怪物都以任意顺序包含一个 a、一个 e 和一个 i:
棋盘certain · machine · relation · dialect · patience vs garden · pencil · plastic · money · violin
- 所有顺序能解出47
/a.*e.*i|a.*i.*e|e.*a.*i|e.*i.*a|i.*a.*e|i.*e.*a/ - 先行断言能解出17
/(?=.*a)(?=.*e).*i/
先行断言版本之所以可行,是因为正则搜索会依次尝试每个位置。在第一个位置上,两个先行断言都会扫描整个单词,所以 a 和 e 在哪里都无所谓。
07轮到你了
同样的思路,新的棋盘。每个怪物都至少有一个数字和至少一个大写字母,顺序不限:
抓住同时含有数字和大写字母的单词,顺序不限。
目标 · 抓住 0/5
- Hunt3r目标
- R2d2目标
- x9Y目标
- Agent47目标
- 7Seas目标
无辜词 · 误抓 0
- hunter无辜
- r2d2无辜
- AGENT无辜
- 1999无辜
- x9y无辜
输入正则——棋盘会实时更新。
08使用前需要知道的事
- 环视是零宽度的。 单独的
(?=\d)匹配的是数字前面的一个空字符串。这在插入文本时很方便(比如千位分隔符),但如果你本想匹配数字本身,就会让人困惑。 - 支持程度不一。 JavaScript(所有现代浏览器和 Node)、Python、Java、.NET 和 PCRE 都支持先行断言和后行断言。Python 内置的
re模块只允许固定宽度的后行断言,所以(?<=\$\d*)在那里会报错,而 JavaScript 接受它。Go 的标准regexp包完全不支持环视。 - 普通正则清晰时,优先用普通正则。
q[^u]|q$同样可行,有些读者觉得它更好理解。当环视能让意图一目了然,或者能省掉真正的复杂度时,再用它。