跳到主要内容
AZ Tools

正则表达式实战

正则表达式素有「只写不读」之名:写起来容易,回头看很难,而且常常只在真实数据上悄悄出错。这些问题大多源于对引擎行为的几处误解。本文逐一拆解,最后讲清哪些场合该把正则收起来。

引擎实际在做什么

正则表达式不是对目标字符串的「描述」,而是引擎在输入的每个位置、自左向右运行的一段小程序。它先从下标 0 尝试匹配,失败就移到下标 1 再试,直到匹配成功或输入耗尽。几乎所有令人意外的结果都源自这次扫描。

所以 `\d{3}` 会心安理得地匹配 "abc1234def":它在中间找到了 "123",而模式里没有任何一处说不许这样。「包含三个数字」和「恰好是三个数字」是两个问题,模式只回答你问的那个。

锚点与单词边界

`^` 和 `$` 把模式绑到输入的开头和结尾——若开启多行标志,则是绑到每「行」的开头和结尾,这是校验器悄悄变宽松的常见原因。`\b` 是单词字符与非单词字符之间的零宽位置,因此 `\bcat\b` 能在 "the cat sat" 中找到那只猫,却不会命中 "concatenate" 里的字母。

做校验时,请把整个模式锚住,并且用你「想拒绝」的值去测试,而不只是想通过的值。一个放行所有正确值、同时也放行垃圾的校验器还不如没有:它把失败推到下游,而在那里追查这个坏值要困难得多。

贪婪、懒惰与灾难性回溯

量词默认是贪婪的:`.*` 会尽可能多地吞掉字符,只有当模式的其余部分失败时才往回吐。加上 `?` 就变成懒惰。对 `<b>one</b> and <b>two</b>`,`<.*>` 会吞下整行,而 `<.*?>` 在第一个 `>` 处停下——意图相同,结果截然不同。

这种「往回吐」也正是性能崩塌之处。在互相重叠的字符集合上嵌套量词——教科书式的例子是 `(a+)+b`——会让引擎在承认无法匹配之前尝试指数级数量的切分方式。若输入由攻击者提供,这就是一个拒绝服务漏洞,通常称为 ReDoS。让带量词的分组彼此不重叠,并尽量用具体的字符类代替 `.`。

字符类、点号与 Unicode

除非开启 dotall 标志,`.` 表示「除换行外的任意字符」;而 `\d`、`\w`、`\s` 在多数实现中以 ASCII 为中心:`\w` 不含带重音的字母,`\d` 是否包含其他书写系统的数字则因引擎而异。处理非纯 ASCII 文本时,请开启 Unicode 模式,并使用 `\p{L}`(任意字母)、`\p{N}`(任意数字)这类属性转义。

字符类中的区间是码位区间,所以 `[A-z]` 会悄悄把 `Z` 与 `a` 之间的六个标点也包进来。请写出你真正想要的字符类。另外别忘了,外观相同的文本可能是不同的序列:作为单个码位的 "é" 并不匹配为「e + 组合重音」设计的模式,比较前请先做规范化。

什么时候正则是错的工具

带嵌套和引号的格式——HTML、JSON、CSV、源代码——都不是正则语言。模式可以啃下一小块,但每多一种带引号的分隔符、每多一层嵌套,就多一个特例,直到表达式既无法阅读、又在你尚未见过的输入上依然出错。请用真正的解析器:CSV 用尊重引号的解析器,HTML 用 DOM。

电子邮件地址是另一个经典陷阱。规范里的文法远比人们从网上复制来的模式庞大,因此看起来严格的正则会日常性地拒绝有效地址——包括带加号的、或使用新的长顶级域名这类再普通不过的地址。只需确认存在一个 `@` 且两侧都有像样的内容,然后用唯一有效的办法去证明:给它发一封邮件。

  • 在你想要的位置匹配:校验就用 `^`…`$` 锚住,搜索就不要锚。
  • 已知分隔符时,`[^"]*` 优于 `.*?`——更清晰,且不会回溯。
  • 用你打算拒绝的输入来测试,而不只是打算接受的输入。
  • 格式若有嵌套或引号,请改用解析器。

相关工具