正则表达式怎么写?测试、贪婪匹配与速查表

2026-10-14约 9 分钟网络工具涉及 5 个工具

正则写起来像密码,调起来靠反复试。其实大部分失败集中在三个原因上:模式没有锚定、量词贪婪、以及正则在坏输入上耗尽了 CPU。理解了这三点,剩下的只是查表。

核心要点
  • 不加锚定符的正则会在字符串任意位置匹配,所以用来「校验」时必须写 ^ 和 $,否则只校验了整个字符串的一小段
  • 量词默认是贪婪的:.* 会一直吃到最后一个能匹配的位置,改为 .*? 才是最短匹配
  • 点号(.)默认不匹配换行符,跨行提取要显式开启 dotall 或改用 [\s\S]
  • 嵌套量词如 (a+)+ 遇到不匹配的长输入会出现灾难性回溯,耗时呈指数增长,能把服务卡死
  • 不同语言的正则实现有细节差异,例如 \d 在 Python 中会匹配全角数字,在 JavaScript 中只匹配 0-9

#三个坑,占了正则调试的绝大多数

正则调试失败的常见原因就那么几个,逐个排查比反复修改模式有效得多。

现象原因改法
校验通过了,但明显是非法输入没有锚定,正则只匹配到一小段首尾加 ^ 和 $
只想取第一个词,结果取回整行量词贪婪把 .* 改成 .*?
跨行提取拿不到内容点号默认不匹配换行符用 [\s\S]+? 或开启 dotall
测试数据一跑就卡住嵌套量词触发灾难性回溯改写量词结构,加长度上限
同样正则在不同语言结果不同方言差异(Unicode、转义规则)确认目标语言的引擎行为

#锚定:为什么「校验」必须加 ^ 和 $

正则默认的行为是「在字符串中找一段匹配的内容」,不是「整串必须完全符合」。 这两件事差别很大:

模式:  \d{4}
输入:  abc1234def
结果:  匹配成功(命中了 1234)

如果你把这个正则当作「校验输入是 4 位数字」,那么 abc1234def 会被错误地判为合法。加上锚定符才是真正的整串校验:

模式:  ^\d{4}$
输入:  abc1234def
结果:  不匹配 ✓
要点

用正则做「格式校验」时,^ 和 $ 不是可选项。需要匹配内容时(比如从日志里抽取 IP)才不加锚定 —— 这两种用途必须分清,混用是校验逻辑失效的头号原因。

#贪婪与懒惰:一次说清

**量词(* + {n,m})默认尽可能多地匹配,这就是「贪婪」;在量词后加 ? 变成「懒惰」,改为尽可能少地匹配。**

用提取 HTML 标签之间的内容来对比,区别非常直观:

输入:  <b>第一段</b> 中间 <b>第二段</b>

贪婪:  <b>.*</b>      → 命中从第一个 <b> 到最后一个 </b>,把中间内容全吃掉
懒惰:  <b>.*?</b>     → 命中第一个 <b>第一段</b>,符合预期

判断方法:结果比你预期的「长」,就说明该换成懒惰量词。

不过要注意一个现实:用正则解析 HTML 是脆弱做法。. 不匹配换行、属性顺序变化、嵌套标签都会让模式失效。结构化格式请用专门的解析器,正则只适合抽取结构稳定的片段。

注意

.* 在长文本上还有性能问题:它会先贪婪地吃到最后,再逐步回退(回溯)寻找匹配。文本越长,试错次数越多。对几十 KB 以上的输入,优先用更具体的字符类(如 [^"<]+)代替 .*。

#分组、捕获与反向引用

圆括号同时承担两个职责:分组和捕获。 (abc)+ 表示把 abc 作为一个整体重复;同时它也会在匹配结果里生成一个捕获组。

写法名称作用
(abc)捕获组会被记录到结果里,可用 $1 或 \1 引用
(?:abc)非捕获组只分组、不记录,少量提升性能、结果更干净
(?<name>abc)具名捕获用名字取,比数字下标更可读
(?=abc)前瞻后面必须是 abc,但不消耗字符
(?!abc)负前瞻后面不能是 abc
(?<=abc)后顾前面必须是 abc

一个实用的例子:把 key: value 格式的 key 换成大写,用前瞻避免吃掉分隔符:

模式:  ^(\w+)(?=\s*:)
替换:  ${1} 的大写形式

前瞻和后顾只做「条件判断」,不消耗字符,所以在需要「只改一部分、保留另一部分」时特别有用。

#常用模式速查表

以下模式可直接复制,注意都是整串校验(带锚定符)。用 正则表达式测试 验证一遍再放进代码。

用途模式备注
中国大陆手机号^1[3-9]\d{9}$号段会变,宽松些可放宽为 ^1\d{10}$
邮箱(宽松)^[\w.+-]+@[\w-]+(\.[\w-]+)+$邮箱的完整规范极复杂,实践中用宽松版
IPv4^(?:\d{1,3}\.){3}\d{1,3}$会把 999.1.1.1 判为合法,需额外范围校验
日期 YYYY-MM-DD`^\d{4}-(0[1-9]\1[0-2])-(0[1-9]\[12]\d\3[01])$`不校验月份天数(如 2 月 30 日会通过)
时间 HH:MM`^([01]\d\2[0-3]):[0-5]\d$`24 小时制
18 位身份证^\d{17}[\dXx]$末位校验码需单独算法验证
十六进制颜色`^#(?:[0-9a-fA-F]{3}\[0-9a-fA-F]{6})$`支持 3 位与 6 位
URL^https?://[^\s]+$严格校验 URL 建议用专门解析器
技巧

用 查找替换 的正则模式可以批量改文本。先在测试工具里把模式调对、确认高亮范围无误,再拿去替换 —— 直接在大量文本上试错,改错了不容易回退。

说明

有一类常见需求不必自己写正则:解析 User-Agent 字符串。UA 格式变体极多,直接用 UA 解析 会按浏览器、系统、设备类型拆好,比自己维护一套匹配规则可靠。

#性能陷阱:灾难性回溯

嵌套量词是最危险的写法,(a+)+ 这类模式在长输入上的耗时呈指数增长。 触发条件很具体:

模式:  ^(a+)+$
输入:  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX     ← 末尾一个不匹配的字符

引擎会尝试所有可能的分组切分方式,30 个 a 就能产生上亿次尝试,表现为接口卡死、CPU 打满。这个问题的学术名称是 ReDoS。

规避方法:

  1. 不要嵌套量词。(a+)+ 改用 a+ 即可。
  2. 用更具体的字符类代替宽泛的 . 和 \w。
  3. 加锚定符。不锚定的模式会尝试所有起点,代价成倍增加。
  4. 对输入长度设上限。用户输入超过一定长度时直接拒绝,比事后补救简单。
  5. 设置匹配超时。部分语言的库支持超时参数,超时即放弃。

#常见问题

#为什么同样的正则在 Python 能匹配,在 JavaScript 不行?

引擎方言不同。最典型的是 \d:JavaScript 中它只匹配 ASCII 的 0-9;Python 3 处理字符串时 \d 会匹配包括全角数字在内的 Unicode 数字。其他差异还有转义规则、\b 的边界定义、具名分组语法((?<name>) 与 (?P<name>))。写跨语言的正则时,用显式字符类([0-9])替代简写最稳。

#怎么匹配换行?

. 默认不匹配换行符。三种做法:用 [\s\S](匹配任意字符,含换行)、用 [\d\D]、或者开启 dotall 标志(各语言名称不同,Python 是 re.S,JavaScript 是 /s)。第三种最简洁,但要注意它会同时改变整个模式里所有 . 的行为。

#贪婪和懒惰能混用吗?

能,而且经常需要。典型场景是前面贪婪、后面懒惰:^.*?(\d+)$ 表示「尽量少地跳过前缀,然后抓到结尾的数字」。原则是在你能确定目标位置的地方用懒惰,在需要稳定边界的地方用贪婪。

#正则能解析 HTML 或 JSON 吗?

不能可靠地解析,只能抽取结构稳定的片段。嵌套结构(标签套标签、对象套对象)超出了正则的能力范围 —— 正则没有栈,无法匹配任意深度嵌套。抓 JSON 里的某个字段用字符串查找配合专门的解析器更稳,JSON 格式化校验 可以直接把结构展开看清层级。至于文本的行数、字数这类统计,用 文本统计 比写正则快得多。

#正向预查和普通匹配到底差在哪?

差在「是否消耗字符」。abc(?=def) 匹配 abc,但匹配结束后位置停在 def 之前;abcdef 则把 def 一起吃掉了。区别体现在替换操作上:用前瞻可以只替换 abc 而不动 def。

#怎么调试一个复杂正则?

拆开测,不要整体调。先单独验证每一段(字符类、量词、分组),用测试工具的高亮确认每段命中的范围,再拼起来。复杂模式可读性极差,用非捕获组和具名分组分段标注,比事后猜测高效得多。

本文提到的工具

相关文章