为什么需要一篇「实战」正则文章
互联网上的正则教程大多停在「语法列表」层面:什么是 \d、什么是 +、什么是 (?=...)。但真正写代码时,开发者面对的是更具体的问题:
- 这个邮箱正则够不够严格?要不要拦掉
user@localhost? - 身份证号最后一位校验码怎么用正则表达?
- 同一段正则在 Chrome 与 Safari 上行为不一致,是哪个特性不兼容?
- 生产环境那条
(\w+\s?)+在长字符串上把 CPU 跑到 100%,到底为什么?
本文是正则表达式入门与实战的进阶配套,聚焦四个实战主题:常用模式速查、命名捕获组、ReDoS 防御、性能优化。所有模式均可在在线正则测试工具中直接粘贴验证。
ES2018 命名捕获组:给捕获组起个名字
数字捕获组的痛点
传统捕获组用 (...) 定义,按左括号出现顺序编号为 $1、$2、$3。简单场景没问题,但模式一长就出现两类典型 bug:
- 数错顺序:模式
(\w+)-(\d+)-(\w+)调整成(\w+)-(\w+)-(\d+)后,原来引用$3的替换串必须改成$2,遗漏一处就出 bug。 - 可读性差: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: $$5 → price: $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} | g | 11 位,第二位 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./-]*)? | gi | http/https 链接 |
| IPv4 地址 | \b(?:\d{1,3}\.){3}\d{1,3}\b | g | 不校验范围(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?)\b | g | 每段 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} | gi | 6 段十六进制 |
数字与编码
| 用途 | 模式 | 标志位 | 说明 |
|---|---|---|---|
| 整数 | -?\d+ | g | 含负数 |
| 浮点数 | -?\d+\.\d+ | g | 不含科学计数法 |
| 十六进制颜色 | #(?:[0-9a-f]{3}|[0-9a-f]{6})\b | gi | #fff 或 #ffffff,配套 颜色工具 |
| MD5 | \b[0-9a-f]{32}\b | gi | 32 位十六进制,配套 Hash 工具 |
| SHA-256 | \b[0-9a-f]{64}\b | gi | 64 位十六进制 |
| JWT | eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+ | g | 三段点分,配套 JWT 工具 |
文本与日期
| 用途 | 模式 | 标志位 | 说明 |
|---|---|---|---|
| 中文字符 | [\u4e00-\u9fa5]+ | g | CJK 基本区,需 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}) | g | 24 小时制 |
| Unix 时间戳(秒) | \b\d{10}\b | g | 配套 时间戳工具 |
格式转换
| 用途 | 模式 | 标志位 | 说明 |
|---|---|---|---|
| 驼峰转下划线 | ([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\d | 1[0-2])(?:[0-2]\d | 3[0-1])\d{3}$` |
| 18 位身份证 | `^[1-9]\d{5}(?:19 | 20)\d{2}(?:0\d | 1[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指数级增长
防御策略
- 避免嵌套量词:
(a+)+改为a+(外层量词多余) - 避免重叠分支:
(a|a)*改为a* - 具体字符类替代
.:[^"\n]+比.*?安全得多 - 使用原子组(部分引擎支持):
(?>a+)+匹配成功后不回溯(JS 原生不支持,但可用第三方库) - 限制输入长度:正则工具对输入设 10000 字上限
- 超时熔断:生产环境对正则匹配设超时(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] |
.* 跨行匹配 | 默认 . 不匹配 \n | 加 s 标志位 |
^ $ 匹配每行 | 默认匹配整个字符串首尾 | 加 m 标志位 |
忘记 g 只替换第一个 | replace 默认只替换首个 | 加 g 或用 replaceAll |
(\d{4}) 误用 (?<year>\d{4}) 后 $1 失效 | 命名组同时占用数字编号 | $1 仍可用,或改 $<year> |
正则含 / 未转义 | 字面量 /.../ 中 / 是结束符 | 用 \/ 或 new RegExp |
工具矩阵联动
正则不是孤立工具,它与站内多个工具形成联动:
- 正则测试工具:本博客配套,所有模式可粘贴验证
- JSON 工具:JSON 字符串提取字段用正则
- HTML 实体工具:HTML 标签提取
<([^>]+)>配合实体解码 - Hash 工具:MD5/SHA-256 校验和正则识别
- 时间戳工具:Unix 时间戳
\d{10}识别 - UUID 工具:UUID v4 正则校验
- URL 工具:URL 编码与正则提取联动
- JWT 工具:JWT 三段格式正则识别
- 颜色工具:十六进制颜色
#(?:[0-9a-f]{3}|[0-9a-f]{6})校验
检查清单
上线前对照以下清单检查你的正则:
- 输入长度是否设上限(防 ReDoS)?
- 是否避免嵌套量词与重叠分支?
- 不需要引用的组是否用了
(?:...)非捕获组? - 是否用具体字符类替代
.? - 是否在循环外编译一次复用?
- 含中文/Emoji 的正则是否加了
u标志位? - 替换串中
$$是否正确转义? - 复杂正则是否考虑加注释
(?#...)或拆分(JS 不支持内联注释,可用new RegExp拼接)? - 正则是否在多浏览器测试(特别是后行断言
(?<=...)兼容性)? - 生产环境是否对正则匹配设超时?
小结
正则表达式是开发者的「微型超能力」:写对一次,受益终身;写错一次,背锅一夜。本文覆盖的命名捕获组、常用模式速查、ReDoS 防御、性能优化四个主题,是生产环境正则使用的核心知识。
下一步建议:
- 把 20+ 常用模式速查表加入浏览器书签,遇到具体场景直接复用
- 现有正则若是数字组且模式含 3 组以上,评估迁移到命名捕获组
- 生产环境正则做一次 ReDoS 自查,特别关注用户可输入的文本字段
- 用在线正则测试工具实测本文每个示例,加深肌肉记忆