JsonMind
编码与文本9 分钟阅读更新于 2026年11月10日

正则表达式速查表:常用语法、模式与测试方法

一份面向实践的正则参考:锚点、字符类、量词、分组与环视,配合常用模式和避免灾难性回溯的检查方法。

JM作者:JsonMind Engineering最后复核: 2026年11月10日依据权威规范与引用来源复核。
快速答案

正则由一组记号组成:字符类匹配单个字符,量词控制重复,锚点固定位置,分组负责组合。用带示例文本的测试工具验证,区分贪婪与懒惰匹配,并警惕可能导致灾难性回溯的嵌套量词。

核心要点
  • 需要匹配字面量时,记得转义元字符。
  • 贪婪量词尽可能多匹配,懒惰量词在第一次机会处停止。
  • 锚点匹配的是位置,不是字符。
  • 命名分组和环视能提高可读性,但不同引擎支持程度不同。
  • 用合法、非法和对抗性输入测试,嵌套量词可能拖垮正则引擎。

正则表达式是一种描述文本模式的紧凑语言,也很容易写的时候明白、过几天就看不懂。本文整理了最常用的记号,以及避免无声错误的测试习惯。

字面量与元字符

大多数字符只匹配自身。以下字符有特殊含义,需要匹配字面量时必须转义:

. * + ? ^ $ { } [ ] ( ) | \

例如 . 匹配字面量点号,* 匹配字面量星号。不同语言的转义规则略有差异,尤其是在字符类和替换字符串内部。

转义问题常常发生在正则引擎之前的那一层。JavaScript 字符串里写 "\d",\d 不是合法字符串转义,结果只剩一个 d,必须写 "\d" 或使用正则字面量 /\d/。Python 的原始字符串 r"\d" 可以避开双反斜杠。字符类内部大多数字符失去特殊含义,但 ]、\、^ 和 - 仍需小心:[-a] 把连字符放在开头,[a-z] 才表示字母 a、连字符和 z,而不是范围。部分引擎支持 \Q...\E 引用一段字面量,JavaScript 和 Python 不支持,只能逐个转义。

字符类

模式匹配内容
.除换行外的任意字符,除非开启 dotAll
\d数字,通常是 [0-9]
\w单词字符:字母、数字和下划线
\s空白字符
[abc]a、b、c 中的一个
[^abc]除 a、b、c 外的任意字符
[a-z]a 到 z 范围内的字符
[A-Za-z0-9_]ASCII 下 \w 的显式写法

否定字符类往往比懒惰量词更清晰。"[^"]*" 匹配从引号开始到下一个引号结束的字符串。

这些简写字符类没有看起来那么通用。JavaScript 不开 u 标志时,\d 等于 [0-9],\w 等于 [A-Za-z0-9_],\s 只覆盖固定的 ASCII 空白;开启 u 后,\d 和 \w 仍只认 ASCII,\s 才扩展到 Unicode 空白。Python 3 的 str 模式默认按 Unicode 解释,\w+ 能匹配西里尔字母和中文,\d 能匹配阿拉伯-印度数字。需要只认 ASCII 时,应加 re.ASCII 或改用显式字符类。浏览器和服务器上表现不一致,常常就是这里造成的。

点号也比很多人以为的窄。默认它不匹配 \n;JavaScript 不开 s 时还不匹配 \r 以及 Unicode 的行分隔符和段分隔符。确实想匹配任意 UTF-16 码元时,JavaScript 可以写 [^],需要跨引擎可移植时写 [\s\S]。

量词

模式含义
*零次或多次
+一次或多次
?零次或一次
{3}恰好三次
{2,5}两到五次
{2,}两次或更多
*?零次或多次,懒惰
+?一次或多次,懒惰

贪婪量词尽可能多匹配,懒惰量词尽可能少匹配。"." 会从第一个引号一直匹配到最后一个引号,".?" 只匹配一个引号片段。

懒惰不等于高效。.*? 仍然是匹配、失败、前进一个字符、再匹配,面对很长的非匹配字符串可能退化成平方级。PCRE、Java 和 Perl 提供占有量词 *+、++、?+,消费后不回溯;原子分组 (?>...) 对子表达式做同样的事。确定量词不应该吐回字符时,它们才是正确工具。JavaScript 没有这两种语法,但可以用先行断言模拟一部分原子分组。

锚点与边界

模式含义
^字符串开头;开启 m 后也匹配行首
$字符串结尾;开启 m 后也匹配行尾
\b单词边界
\B非单词边界
\A部分引擎中的字符串开头
\z部分引擎中的字符串结尾

锚点匹配的是位置,不是字符。^$ 匹配空行,而不是包含空格的行。

末尾锚点在不同引擎里并不等价。Python 3.14 及以后,\A 和 \z 始终表示整个字符串的开头和结尾,\Z 允许末尾多一个换行,$ 即使不开多行模式也会匹配结尾换行之前的位置。JavaScript 不开 m 时,$ 只匹配真正的末尾,"abc\n" 无法通过 c$,除非先去掉换行。\b 是单词字符与非单词字符之间的位置,不是字符本身,所以单独一个 \b 匹配零个字符,只高亮边界。输入含中文时,\b 是否把汉字当作单词字符,取决于引擎和 u 标志的定义。

分组与分支

  • 捕获分组:(abc) 会捕获匹配文本。
  • 非捕获分组:(?:abc) 只分组,不捕获。
  • 命名分组:(?<name>abc) 按名称捕获。
  • 反向引用:\1 或 \k<name> 引用之前的捕获。
  • 分支:cat|dog 匹配任意一个。

分支的优先级很低。^cat|dog$ 等价于 ^cat 或 dog$。如果锚点应同时作用于两个分支,应写成 ^(cat|dog)$。

分支是有顺序的,不是对称的。cat|category 会在 category 中匹配到 cat,第一支成功后引擎不会再尝试更长的分支。一个分支是另一个的前缀时,把长的放在前面,或者加锚点限定。捕获组编号按左括号出现的顺序分配,而不是按实际匹配顺序,所以在中间插入一个分组会让后面所有反向引用重新编号。命名分组能避免这种脆弱性,但写法不同:JavaScript 和 PCRE 用 (?<name>...),Python 用 (?P<name>...),反向引用分别是 \k<name> 和 (?P=name)。替换语法又是另一套,这也是为什么测试工具里正常的正则,写进代码后会失败。

环视

模式含义
(?=abc)后面跟着 abc
(?!abc)后面不是 abc
(?<=abc)前面是 abc
(?<!abc)前面不是 abc

环视只断言上下文,不消费字符。\d+(?= dollars) 会匹配“25 dollars”中的数字,但不会匹配“25 euros”。不同引擎对环视的支持和长度限制不同,部分旧引擎不支持后行断言。

后行断言是最容易破坏可移植性的特性。Python 要求定宽:(?<=abc) 可以,(?<=a|bc) 不行,(?<=\d+) 因为长度无界会直接报错。JavaScript 的后行断言更宽松,允许变长,.NET 和许多情况下的 PCRE2 也允许。于是 Chrome 里测试通过的正则,到 Python 里可能连编译都过不了。必须同时运行时,应把后行断言换成捕获组并在替换时调整,或者在代码里先过滤候选。

标志

标志作用
i忽略大小写
m多行模式,^ 和 $ 匹配行边界
s点号匹配换行
g全局查找所有匹配
uUnicode 模式
y粘性匹配,从当前位置开始

标志会改变语义,不只是方便程度。没开 m 时测试通过的模式,开启 m 后可能表现不同。

有两个标志会制造状态 bug。JavaScript 的 g 正则会维护 lastIndex,对同一个正则对象连续调用两次 test(),同一个字符串可能先返回 true 再返回 false,因为第二次从上次停下的位置继续。要么每次重置 lastIndex,要么把正则创建在循环内部。开启 u 后,引擎按 Unicode 码点解释模式,点号会把一个 emoji 当作一个码点而不是两个代理项,也能使用 \p{Letter}。较新的 v 标志在 u 之上增加集合运算,例如 [\p{Letter}--[a-z]]。但 u 和 v 都不会让 JavaScript 的 \d 匹配非 ASCII 数字,那仍然需要显式的 Unicode 属性类。

常用模式

  • 纯数字:^\d+$
  • 带符号整数:^[+-]?\d+$
  • 十六进制颜色:^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$
  • ISO 日期形状:^\d{4}-\d{2}-\d{2}$
  • URL slug:^[a-z0-9]+(?:-[a-z0-9]+)*$
  • 简单邮箱形状:^[^\s@]+@[^\s@]+.[^\s@]+$
  • 带协议的 URL:^https?://[^\s/$.?#].[^\s]*$

这些模式校验的是形状,不是语义。2026-99-99 可以通过日期形状校验。涉及真实有效性时,应配合日期解析器使用。简单邮箱模式也无法确认地址是否存在。

上面的 URL 模式故意写得很浅:它接受 http:// 后面几乎任何内容,却拒绝带端口、IPv6 字面量、用户信息或国际化域名的合法地址。要验证 URL,应使用平台的 URL 解析器并检查协议和主机,正则处理不了这套语法。邮箱模式一旦被加上嵌套量词,还很容易变成拒绝服务风险;保持简短,并用发信验证。日期应先用正则检查形状,再构造 Date 或交给日期库,因为 2026-02-30 和 2026-13-01 都能通过形状检查。颜色模式里 #abc 和 #aabbcc 合法,#abcd 不合法,上面的表达式已经覆盖这一点。

贪婪、懒惰与灾难性回溯

嵌套量词可能导致指数级爆炸。模式 (a+)+$ 面对一长串 a 后面跟一个 b 的输入时,可能尝试指数多种切分方式。

实用的防御措施:

  • 避免对同一批字符使用嵌套量词。
  • 用明确的字符类代替点号加重复。
  • 引擎支持时,使用占有量词或原子分组。
  • 引擎允许时,设置匹配超时。
  • 不只测试能匹配的输入,也要测试对抗性输入。

“指数级”具体是什么样:对 (a+)+$ 来说,30 个 a 后面跟一个 b,可以切成 2^29 种分组,约 5.36 亿条路径。JavaScript 或 PCRE 这类回溯引擎可能要跑几秒到几分钟才报告失败;到 40 个 a,同样的输入能拖过任何请求超时。^(a|aa)+$ 和 ^(\w+\s?)*$ 在同样短的输入上表现相同。曲线非常陡,10 个字符的测试看起来没事,生产环境 50 个字符就会挂住。

引擎选择很关键。RE2、Go 的 regexp 和 Rust 的 regex 不回溯,时间复杂度是线性的,但也不支持反向引用和部分环视。模式确实需要这些特性时,应留在回溯引擎上,同时限制输入长度并设置超时。在 Node.js 里,灾难性回溯会阻塞整个事件循环,而不是只卡住一个请求,最终会拖垮无关流量。

替换字符串

JavaScript 用 $1 和 $<name> 插入捕获,Python 用 \1 和 \g<name>,部分工具用 $& 表示整个匹配。替换语法因语言和工具而异,使用前应确认目标方言。

还有几个替换记号值得记住。JavaScript 支持 $$ 表示字面量美元符号,$& 表示整个匹配,$` 表示匹配之前的文本,$' 表示匹配之后的文本。Python 的 \g<0> 表示整个匹配;\1 后面紧跟数字时会产生歧义,写 \g<1>0 更安全。两种语言里,未参与匹配的可选分组在替换时都会插入空字符串,而不是保留原文,这会在不知不觉中删掉内容。替换需要逻辑时,应传函数:JavaScript 支持 replacer 函数,Python 的 re.sub 第二个参数也可以传函数,两者拿到的都是匹配对象,而不是模板字符串。

正则测试工具可以在示例文本上高亮匹配和捕获分组,正则生成器可以根据自然语言需求生成模式,查找替换工具可以把模式应用到真实文本。生成的正则应先经过测试用例验证,再写入代码。

测试清单

  1. 至少测试一个合法输入。
  2. 测试一个不应匹配的非法输入。
  3. 测试空输入。
  4. 测试包含换行和 Unicode 字符的输入。
  5. 检查有锚点和无锚点的差异。
  6. 确认生产环境使用的引擎和标志。
  7. 用很长的非匹配字符串测试性能。
  8. 为复杂的正则保存注释和示例。
  9. 同样的用例要在生产所用引擎上再跑一遍,而不只是在测试工具里。
  10. 正则执行前先限制输入长度,即使模式看起来安全。

正则也是代码。保持可读,用真实示例测试,并把任何带嵌套量词的模式当作需要重点审查的对象。

有些任务根本不该用正则。用正则解析 HTML 会在嵌套标签、注释和含 > 的属性上出错,应使用 HTML 解析器。用正则校验 JSON、CSV 或 URL,等于重新实现一套已有解析器的语法,边界情况几乎必错。判断字符串是否包含固定子串、是否以某个前缀开头、按分隔符切分,用 includes、startsWith 或 split 更快也更清楚。只有当模式确实不规则、但仍然属于正则语言时才用正则;输入存在嵌套或正式语法时,交给解析器。

常见问题

点号匹配什么?

默认情况下,点号匹配除换行符以外的任意字符。开启 s 或 dotAll 标志后,它也会匹配换行。需要精确控制时,可以用 [^\n] 这样的否定字符类。

* 和 + 有什么区别?

星号匹配零次或多次,因此模式可以匹配空字符串。加号匹配一次或多次,至少需要出现一次。

为什么正则匹配了太多内容?

贪婪量词会尽可能多地消费文本,然后回溯。如果只想要最短匹配,可以使用 .*? 这样的懒惰量词,或让前后模式更具体。

用正则校邮箱靠谱吗?

简单正则可以拦截明显错误,但完整的邮箱校验非常复杂,而且规则会变化。可以用正则检查基本形状,再通过发送验证邮件确认地址真实存在。

什么是灾难性回溯?

某些模式,尤其是 (a+)+ 这样的嵌套量词,在不匹配的长输入上可能需要指数级时间。应减少嵌套,让分支彼此区分,并用对抗性字符串测试。

参考资料

把指南用起来

浏览全部工具

打开浏览器工具。输入内容保留在浏览器中,不会上传到业务服务器。

相关指南

全部指南