正则表达式的问题很少是「不会写」,多数是「写了但匹配结果和预期差一点」。差的那一点往往就在贪婪、锚点、转义这三处。
这篇给一个固定的排查顺序,再补一些实际会用到的写法。别在编辑器里改一行跑一次代码,直接在正则校验工具里把样本贴进去,改一个字符就能立刻看到高亮命中了什么。
一、排查顺序:从「能不能匹配」到「匹配得准不准」
第 1 步:去掉所有约束,先让它匹配上。 把正则删减到最核心的一小段,确认能命中,再逐步把限制加回去。一次写一长串然后猜哪里错了,效率最低。
第 2 步:检查标志位。 三个最常用的:
g全局。不加只返回第一个匹配。用replace只替换了第一处,基本都是漏了g。i忽略大小写。m多行,让^和$匹配每行的开头结尾,而不是整个字符串的开头结尾。处理多行文本时经常需要它。
还有一个 s(dotAll),让 . 能匹配换行符。默认情况下 . 不匹配换行,跨行抓取内容时不加 s 会一直失败。
第 3 步:检查锚点。 ^abc 要求开头,abc$ 要求结尾,两个都不加就是「包含」。校验类需求(判断整个输入是否合法)必须两头都加锚点,否则 ^\d{6}$ 写成 \d{6} 时,abc123456xyz 也会通过。
第 4 步:检查转义。 这些字符有特殊含义,要匹配字面量必须加反斜杠:. * + ? ^ $ ( ) [ ] { } | \ /。最常见的是点号——\d+.\d+ 里的 . 会匹配任意字符,写 IP 或版本号校验时必须写成 \.。
第 5 步:检查贪婪。 见下一节。
二、贪婪与懒惰
*、+、{n,} 默认是贪婪的:尽可能多吃。加个 ? 变成懒惰:尽可能少吃。
经典例子,从 <b>粗体</b><i>斜体</i> 里抓标签内容:
<(.+)> 贪婪,一口吃到最后一个 >,捕获到 b>粗体</b><i>斜体</i
<(.+?)> 懒惰,遇到第一个 > 就停,捕获到 b
判断方法:如果你的匹配结果「比预期长了一大截、把中间不该要的内容也吞了」,先给量词加 ?。
反过来也有坑:懒惰匹配在需要「尽可能长」的场景会截断。比如提取一整段引号内容而中间本身含引号时,两种都不合适,得改用排除型字符类 [^"]*。排除法通常比贪婪懒惰之争更可靠:明确说「除了引号什么都行」,语义清楚且不依赖回溯。
三、几个真正会用到的写法
中文。[\u4e00-\u9fa5] 是基本汉字区,覆盖绝大多数常用字,但不含扩展区的生僻字。现代浏览器更推荐带 u 标志用 Unicode 属性:\p{Script=Han},覆盖完整。
手机号。国内号段:^1[3-9]\d{9}$。不要写更复杂的号段枚举——运营商随时放新号段,写得越细失效越快。真正要确认可用性只能靠短信验证。
邮箱。完全符合 RFC 5322 的正则有几百个字符且没有实用价值。工程上够用的是 ^[^\s@]+@[^\s@]+\.[^\s@]+$:有且仅有一个 @、两侧非空、域名部分带点。剩下的交给发信验证。
去掉首尾空白。^\s+|\s+$,配合 g 和 m 可以逐行清理。注意 \s 包含全角空格以外的常见空白,中文文本里的全角空格 \u3000 需要单独列入:[\s\u3000]。
捕获组与非捕获组。(...) 会捕获,(?:...) 只分组不捕获。只是为了给 | 划定范围时用非捕获组,能少一个无用的捕获结果。
命名分组。(?<year>\d{4})-(?<month>\d{2}),取值时用 groups.year,比数 $1 $2 可靠得多,改正则时也不会因为插入一个括号导致下游全错位。
四、别让正则把页面卡死
嵌套量词加上不匹配的输入,会让引擎在回溯上花掉指数级时间,这叫回溯爆炸(catastrophic backtracking)。典型的危险形状:
(a+)+$ 对一长串 a 后面跟个 b,直接卡住
(\s*\w+)*$ 看着无害,同样危险
(.*),(.*), 多个宽泛量词叠加
共同特征是两个量词嵌套,且内层能匹配的内容互相重叠。规避办法:
- 把
.*换成排除型字符类[^,]*,明确边界; - 能用字符串的
split、indexOf解决的就别用正则; - 用户输入的正则不要直接拿来跑(在线工具、搜索框场景),这是一类真实的拒绝服务风险;
- 写完拿一个「长且刚好不匹配」的字符串测一下,这种输入才会触发最坏情况。
五、正则不适合的场合
有几类东西是嵌套结构,正则天生表达不了,硬写一定出错:
- HTML、XML 的解析。用 DOM 解析器。
- JSON 的解析。用 JSON 检验或
JSON.parse。 - 带引号转义的 CSV。字段里可以出现逗号和换行,正则处理不干净,用专门的表格转换工具。
正则的定位是「在扁平文本里找模式」。一旦需要理解层级,就该换工具。
常见问题
同一个正则在 JavaScript 里能用,在其他语言里报错?
方言不同。JavaScript 不支持部分 Perl 扩展语法,\p{...} 需要 u 标志,后行断言 (?<=...) 在较老的 Safari 上不可用。跨语言使用前要确认目标环境的支持情况。
为什么 replace 只替换了第一个?
缺 g 标志。或者改用 replaceAll。
test 反复调用结果时真时假?
带 g 标志的正则对象有 lastIndex 状态,复用同一个对象连续调用 test 会从上次位置继续匹配。校验场景不要加 g,或者每次新建正则对象。
怎么统计某个模式在文本里出现了多少次? 匹配结果的条数就是次数。批量统计字词频率、行数这类需求,用文本统计与去重排序更直接。
测试用的样本数据会上传吗? 不会。正则匹配在浏览器本地执行,日志、用户数据这类样本不会离开你的设备。