先接受一个事实:正则不是万能的
正则擅长结构清晰、规则固定的匹配:日志行、配置片段、格式初筛。它不擅长嵌套结构——HTML、JSON、代码语法应使用对应的解析器,用正则解析嵌套内容迟早会出错。
高频配方
| 需求 | 正则 | 说明 |
|---|---|---|
| 邮箱(宽松) | [\w.+-]+@[\w-]+\.[\w.-]+ | 只做格式初筛,最终以发信验证为准 |
| 中国大陆手机号 | 1[3-9]\d{9} | 号段会调整,不要硬编码全部号段 |
| IPv4(四段) | (?:\d{1,3}\.){3}\d{1,3} | 不校验 0-255 范围,需另做数值判断 |
| ISO 日期 | \d{4}-\d{2}-\d{2} | 不校验合法性,2026-19-99 也会通过 |
| 去除首尾空白 | ^\s+|\s+$ | 多数语言直接用 trim() 更合适 |
| 连续重复行去重 | ^(.*)(?:\r?\n\1)+$ | 需开启多行模式 |
四个必须掌握的机制
- 贪婪与懒惰:
.*会尽可能多匹配,加?变成.*?改为尽可能少,这是最常见的 Bug 来源; - 分组与捕获:
()捕获、(?:)不捕获;反向引用\1可匹配与前面相同的内容; - 环视:
(?=...)正向先行、(?!...)负向先行,用于「后面必须是 / 不能是」的判断; - 锚点与模式位:
^$配合 m(多行)、s(点匹配换行)使用,语义完全不同。
灾难性回溯(ReDoS)
像 (a+)+$ 这类嵌套量词在遇到不匹配的长字符串时,匹配时间会指数级增长,导致 CPU 打满。规避方式:避免嵌套量词、在支持的语言里使用独占量词或原子组、对用户输入长度设上限、不要让用户可控的正则进入服务端匹配。
实用建议
- 先准备「应匹配」与「不应匹配」两组样例,再写正则;
- 复杂校验拆成「正则初筛 + 代码精确校验」,别指望一条正则做完;
- 使用命名分组让后续取值更易读;
- 把正则当代码维护:加注释、存为常量、参与评审。
常见问题
为什么 .* 匹配了整行?因为它贪婪,改用 .*? 或限定字符类(如 [^"]*)。为什么 \d 匹配到了全角数字?取决于引擎是否开启 Unicode 模式,需要时显式写 [0-9]。正则能校验密码强度吗?可做长度与字符类检查,但强度本质是熵,专门的强度评估更合适。
调试建议
用正则测试工具(或编辑器的正则搜索)分三段验证:先验证字符类,再加量词,最后加锚点。把量词与分组拆开测试,能快速定位是哪一部分让匹配超出预期。同时准备「不应匹配」的样例——只测试能匹配的情况,会漏掉最严重的过度匹配问题。
相关阅读:强密码的真相:长度比复杂度更重要
让正则长期可维护
正则的难点不在写出来,而在半年后还能看懂和修改。
- 命名与注释:把常用正则定义为具名常量,并在上方注释说明它接受什么、拒绝什么。把一段表达式直接内联在业务代码里,是最容易失控的写法。
- 拆分组合:把复杂表达式拆成若干可单独测试的小片段,再组合使用。既能复用,也能在出错时快速定位是哪一段不匹配。
- 配套测试用例:为每个正则准备一组“应匹配”与“不应匹配”的样本,尤其是临近边界的输入,例如只有一位数字、超长字符串与包含换行的文本。
- 优先用更简单的工具:判断是否包含子串、是否以某前缀开头这类需求,用普通字符串方法即可,更快也更易读,不必动用正则。
- 注意语言差异:不同语言的实现对手写语法、锚点与转义的处理并不一致,跨语言移植时应重新验证,而不是直接复制。
把这些习惯固定下来,正则会从“只有原作者敢改”的代码,变成团队里可以放心维护的普通逻辑。