正则表达式实战:常用模式速查 + 命名捕获组 + ReDoS 防御

为什么需要一篇「实战」正则文章

互联网上的正则教程大多停在「语法列表」层面:什么是 \d、什么是 +、什么是 (?=...)。但真正写代码时,开发者面对的是更具体的问题:

  • 这个邮箱正则够不够严格?要不要拦掉 user@localhost
  • 身份证号最后一位校验码怎么用正则表达?
  • 同一段正则在 Chrome 与 Safari 上行为不一致,是哪个特性不兼容?
  • 生产环境那条 (\w+\s?)+ 在长字符串上把 CPU 跑到 100%,到底为什么?

本文是正则表达式入门与实战的进阶配套,聚焦四个实战主题:常用模式速查命名捕获组ReDoS 防御性能优化。所有模式均可在在线正则测试工具中直接粘贴验证。

ES2018 命名捕获组:给捕获组起个名字

数字捕获组的痛点

传统捕获组用 (...) 定义,按左括号出现顺序编号为 $1$2$3。简单场景没问题,但模式一长就出现两类典型 bug:

  1. 数错顺序:模式 (\w+)-(\d+)-(\w+) 调整成 (\w+)-(\w+)-(\d+) 后,原来引用 $3 的替换串必须改成 $2,遗漏一处就出 bug。
  2. 可读性差:6 个月后再看替换串 $3 $1 $5,不查模式根本不知道每个 $N 是什么。

命名捕获组语法

ES2018 起支持命名捕获组 (?<name>...),给每个捕获组起一个有意义的名字:

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const m = re.exec('2026-07-04');
console.log(m.groups);
// { year: '2026', month: '07', day: '04' }
console.log(m[1], m[2], m[3]);
// '2026' '07' '04'  —— 命名组同时占用数字编号

关键性质:

  • 命名组同时占用数字编号,仍可用 $1$2 引用
  • 命名组未参与匹配时 m.groups[name]undefined(如可选组 (?:...) 未匹配)
  • 命名组名必须符合 [A-Za-z_$][\w$]* 标识符规则

替换串中的 $ 引用

替换字符串中除了 $1 数字组、$$ 转义,还可使用 $<name> 命名组:

const re = /(?<user>[\w.+-]+)@(?<domain>[\w-]+)\.(?<tld>[\w.-]+)/g;
const text = 'dev@example.com, admin@site.cn';
const result = text.replace(re, '$<user>[$<domain>.$<tld>]');
// "dev[example.com], admin[site.cn]"

三种引用语法对比

语法含义示例
$1$2数字组引用,按左括号顺序[$1 at $2.$3]
$<name>命名组引用,ES2018[$<user> at $<domain>]
$$转义为字面 $price: $$5price: $5

何时该用命名组:模式含 3 个以上捕获组、模式会被多次复用/重构、替换串需要长期维护时。一次性简单正则用数字组足够。

20+ 常用模式速查表

以下模式均经过实战检验,覆盖开发者高频场景。所有模式可在在线正则测试工具中粘贴验证。

联系方式

用途模式标志位说明
邮箱(宽松)[\w.+-]+@[\w-]+\.[\w.-]+gi拦截明显错误,放行 user@localhost
邮箱(严格 RFC 5322 简化)^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$i顶级域至少 2 字母
中国大陆手机号1[3-9]\d{9}g11 位,第二位 3-9
固话(带区号)(?<area>0\d{2,3})[-\s]?(?<number>\d{7,8})g命名组示例:$<area>-$<number>
邮政编码^[1-9]\d{5}$-6 位,首位非 0

网络与标识

用途模式标志位说明
URL(含协议)https?://[\w.-]+(?:/[\w./-]*)?gihttp/https 链接
IPv4 地址\b(?:\d{1,3}\.){3}\d{1,3}\bg不校验范围(0-255 校验需更长正则)
IPv4 严格\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\bg每段 0-255
UUID v4[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}gi配套 UUID 工具
MAC 地址(?:[0-9a-f]{2}[:-]){5}[0-9a-f]{2}gi6 段十六进制

数字与编码

用途模式标志位说明
整数-?\d+g含负数
浮点数-?\d+\.\d+g不含科学计数法
十六进制颜色#(?:[0-9a-f]{3}|[0-9a-f]{6})\bgi#fff#ffffff,配套 颜色工具
MD5\b[0-9a-f]{32}\bgi32 位十六进制,配套 Hash 工具
SHA-256\b[0-9a-f]{64}\bgi64 位十六进制
JWTeyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+g三段点分,配套 JWT 工具

文本与日期

用途模式标志位说明
中文字符[\u4e00-\u9fa5]+gCJK 基本区,需 u 标志位正确处理扩展区
HTML 标签<([^>]+)>g$1 为标签名
ISO 日期(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})g命名组便于提取
ISO 时间(?<h>\d{2}):(?<m>\d{2}):(?<s>\d{2})g24 小时制
Unix 时间戳(秒)\b\d{10}\bg配套 时间戳工具

格式转换

用途模式标志位说明
驼峰转下划线([a-z])([A-Z])g替换 $1_$2 后转小写
下划线转驼峰_(\w)g替换 $1 大写
连字符转驼峰-(\w)g替换 $1 大写
千分位\B(?=(\d{3})+$)g替换为 ,,零宽断言
HTML 实体解码&#(\d+);g替换为 String.fromCharCode($1),配套 HTML 实体工具

中国证件

用途模式标志位说明
15 位身份证`^[1-9]\d{7}(?:0\d1[0-2])(?:[0-2]\d3[0-1])\d{3}$`
18 位身份证`^[1-9]\d{5}(?:1920)\d{2}(?:0\d1[0-2])(?:[0-2]\d

替换实战:三种引用语法对比

场景:把邮箱 dev@example.com 转成 [dev at example.com]

数字组写法:

模式:      (\w+)@(\w+)\.(\w+)
替换串:    [$1 at $2.$3]

命名组写法:

模式:      (?<user>\w+)@(?<domain>\w+)\.(?<tld>\w+)
替换串:    [$<user> at $<domain>.$<tld>]

两者结果完全一致,但命名组写法在 6 个月后回看时无需查模式就能读懂。重构正则调整捕获组顺序时,命名组替换串无需改动——这是命名组最大的长期价值。

场景:金额格式化 1999.5$1,999.50

模式:      (\d)(?=(\d{3})+\.)(\.\d+)
替换串:    $1,$3
标志位:    g

这个例子展示了零宽断言 (?=...) 的用法:先行断言不消耗字符,让替换只在正确位置插入逗号。

ReDoS:正则的「正则灾难」

什么是 ReDoS

ReDoS(Regular Expression Denial of Service)是指恶意构造的输入让正则在匹配时进入指数级回溯,导致 CPU 100%、请求超时、服务不可用。它是真实生产事故的常见来源,不是理论威胁。

2016 年 Stack Overflow 全球宕机 34 分钟,根因就是一个用于剥离 HTML 标签的正则在长字符串上回溯爆炸。

经典灾难模式

以下正则在特定输入下会产生指数级回溯:

// 灾难 1:嵌套量词
/(a+)+b/.test('aaaaaaaaaaaaaaaaaaaaaaaa!');  // 数秒级卡顿

// 灾难 2:重叠交替
/(a|a)*b/.test('aaaaaaaaaaaaaaaaaaaaaaaa!');

// 灾难 3:量词与可选重叠
/(\s*)*b/.test('                        !');

// 灾难 4:含 . 的嵌套
/(.*a){20}/.test('aaaaaaaaaaaaaaaaaaaab');

识别要点

  • 同一字符可被多个量词重复匹配(如 (a+)+
  • 交替分支有重叠(如 (a|a)*
  • 模式末尾有「难以匹配」的字符(如 b),导致前面所有路径都试完才失败
  • 输入长度增长时,匹配时间呈 2^n 指数级增长

防御策略

  1. 避免嵌套量词(a+)+ 改为 a+(外层量词多余)
  2. 避免重叠分支(a|a)* 改为 a*
  3. 具体字符类替代 .[^"\n]+.*? 安全得多
  4. 使用原子组(部分引擎支持)(?>a+)+ 匹配成功后不回溯(JS 原生不支持,但可用第三方库)
  5. 限制输入长度正则工具对输入设 10000 字上限
  6. 超时熔断:生产环境对正则匹配设超时(JS 原生不支持,可用 Web Worker + setTimeout 模拟)

性能优化清单

除 ReDoS 之外,正则还有这些常见性能优化点:

1. 优先使用非捕获组 (?:...)

捕获组 (...) 会占用内存存储匹配文本,非捕获组 (?:...) 不会。不需要引用的组用非捕获组

// 慢:捕获了 4 个组但只用 1 个
/(\w+)\s+(\w+)\s+(\w+)\s+(\w+)/.exec(text);

// 快:仅捕获需要的
/(?:\w+)\s+(?:\w+)\s+(?:\w+)\s+(\w+)/.exec(text);

2. 字符类优于 .

. 会匹配换行符以外的任意字符,回溯空间极大。能用具体字符类就用具体字符类:

// 慢:. 匹配范围过大
/<.+>/;

// 快:[^>] 不含 >,匹配空间小
/<[^>]+>/;

3. 量词精确化

.*.*? 是性能黑点,能用具体量词就用:

// 慢:贪婪 + 回溯
/".*"/;

// 快:不含引号
/"[^"]*"/;

4. 锚点尽早定位

^$ 让引擎从首尾开始匹配,减少扫描:

// 慢:全字符串扫描
/\d{4}-\d{2}-\d{2}/;

// 快:锚定起止
/^\d{4}-\d{2}-\d{2}$/;

5. 编译一次复用多次

在循环中复用同一个 RegExp 实例,避免重复编译:

// 慢:每次循环重新编译
lines.forEach(line => /^(\w+):/.test(line));

// 快:循环外编译
const re = /^(\w+):/;
lines.forEach(line => re.test(line));

常见误用速查表

误用问题正确写法
[email protected]. 在字符类中是字面点,不转义也可[\w.+-]+@...
\d 匹配中文数字\d 仅匹配 [0-9][\d〇一二三四五六七八九]
\w 包含中文\w[A-Za-z0-9_],不含中文[\u4e00-\u9fa5]
.* 跨行匹配默认 . 不匹配 \ns 标志位
^ $ 匹配每行默认匹配整个字符串首尾m 标志位
忘记 g 只替换第一个replace 默认只替换首个g 或用 replaceAll
(\d{4}) 误用 (?<year>\d{4})$1 失效命名组同时占用数字编号$1 仍可用,或改 $<year>
正则含 / 未转义字面量 /...// 是结束符\/new RegExp

工具矩阵联动

正则不是孤立工具,它与站内多个工具形成联动:

检查清单

上线前对照以下清单检查你的正则:

  • 输入长度是否设上限(防 ReDoS)?
  • 是否避免嵌套量词与重叠分支?
  • 不需要引用的组是否用了 (?:...) 非捕获组?
  • 是否用具体字符类替代 .
  • 是否在循环外编译一次复用?
  • 含中文/Emoji 的正则是否加了 u 标志位?
  • 替换串中 $$ 是否正确转义?
  • 复杂正则是否考虑加注释 (?#...) 或拆分(JS 不支持内联注释,可用 new RegExp 拼接)?
  • 正则是否在多浏览器测试(特别是后行断言 (?<=...) 兼容性)?
  • 生产环境是否对正则匹配设超时?

小结

正则表达式是开发者的「微型超能力」:写对一次,受益终身;写错一次,背锅一夜。本文覆盖的命名捕获组、常用模式速查、ReDoS 防御、性能优化四个主题,是生产环境正则使用的核心知识。

下一步建议:

  1. 把 20+ 常用模式速查表加入浏览器书签,遇到具体场景直接复用
  2. 现有正则若是数字组且模式含 3 组以上,评估迁移到命名捕获组
  3. 生产环境正则做一次 ReDoS 自查,特别关注用户可输入的文本字段
  4. 在线正则测试工具实测本文每个示例,加深肌肉记忆