正则表达式高阶使用:负向零宽度断言

当我在写 Surge 的规则配置文件时,我发现我需要通过正则表达式使用 policy-regex-filter 进行过滤,具体的需求是:排除包含指定字符串的节点,过滤获得没有包含指定字符串的节点。

这是个难题,平时使用正则表达式主要是“匹配”的思想,这次要使用“否定排除”的思想。

零宽度断言 (zero-width-assertions) 解决了这个问题。 我们这里讨论的是否定的断言。 零宽度断言所匹配到的内容并不会保存到结果中去。

^((?!hede).)*$

上面的正则表达式是用来匹配排除包含 hede 的结果。

一个字符串包含 n 个字符的 list,在每个字符的前后都包涵一个空的字符串,因此含有 n 个字符的 list 拥有 n+1 个空字符串。 我们来看下这个字符串 "ABhedeCD" :

    ┌──┬───┬──┬───┬──┬───┬──┬───┬──┬───┬──┬───┬──┬───┬──┬───┬──┐
S = │e1│ A │e2│ B │e3│ h │e4│ e │e5│ d │e6│ e │e7│ C │e8│ D │e9│
    └──┴───┴──┴───┴──┴───┴──┴───┴──┴───┴──┴───┴──┴───┴──┴───┴──┘
index    0      1      2      3      4      5      6      7

其中,e 们都是空字符串,表达式 (?!hede). 会检查每个的 e 的后面有没有 “hede” 子字符串,如果没有,. 会匹配任意一个字符(除了换行符),这种行为称为“零宽度断言”,因为他们没有消耗任何字符,仅仅只是在断言或证实某些东西。 因此,在上面的例子中,对于每个空字符串都会被验证其之后有无 “hede”,之后再与 . 匹配。 表达式 (?!hede). 只会进行一次上面的验证,因此,我们将其成组重复多次:((?!hede).)*。 最后,我们锚定首尾:^((?!hede).)*$

"ABhedeCD" 最终会在 e3 处失败,因为 e3 之后存在字符串 “hede”

我们再将其拓展一下,可以匹配排除包含多个指定字符串的节点:

^((?!(本站|流量|过期|下架|用户群|官网|精简)).)*$

准确来说,上面使用的属于「正向否定查找」,常规用法是:x(?=y),含义是仅仅当 x 后面不跟着 y 时匹配 x

例如,仅仅当这个数字后面没有跟小数点的时候,\d+(?!\.) 匹配一个数字。正则表达式 \d+(?!\.) 匹配 141 而不是 3.141

(?!hede). 的这种用法属实少见,其思想是将其匹配字符之前的部分看作是一个字符串(空字符串)([空字符串](?!hede).),而检查是这个空字符串的后面是否匹配 (?!hede)