正则表达式高阶使用:负向零宽度断言
当我在写 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)
。