正则引擎对比:JavaScript、Python、Java、PCRE 和 RE2
逐项实测哪些正则特性能在哪个引擎里用:后行断言、命名分组、Unicode、原子分组、递归、$ 在换行符前的行为,以及哪些引擎会被糟糕的模式卡死。

· 阅读约 3 分钟
/\$\s*(\d+)/
抓住
- $40
- $ 40
- $5
放过
- 40
- USD 40
- $x
JavaScript、Python、Java 和 PCRE 都是回溯型正则引擎,核心语法几乎相同,所以大多数日常模式在这四者中都能用。它们的差异在于那些迁移模式时会让代码出错的细节:允许多大程度的后行断言、命名分组怎么写、\d 是否匹配其他文字的数字,以及 $ 在结尾换行符前的行为。RE2(Go、Google Sheets 和 BigQuery 使用)是个例外:它放弃了反向引用和环视,换来匹配时间永远不超过线性的保证。
下面每一行都是在 2026 年 11 月 4 日实测得出,而非抄自文档:JavaScript 用 Node 22.23,Python 3.12.6(re),Java 17.0.20(java.util.regex),PCRE2 10.44 通过 PHP 8.5 测试,RE2 通过 Google 的 google-re2 1.1 包测试。更新的版本可能支持更多特性。
01各引擎的使用场景
- JavaScript: 所有浏览器、Node、Deno 和 TypeScript。本站的谜题也用这个引擎。
- Python: 内置的
re模块。第三方的regex模块补上了re缺少的很多功能。 - Java:
java.util.regex,JVM 上的 Kotlin 和 Scala 也用它。 - PCRE2: PHP 的
preg_系列函数、grep -P、nginx,以及设置了perl = TRUE的 R。 - RE2: Google 的库。Go 的
regexp包沿用它的语法和速度保证,Google Sheets 的REGEXMATCH和 BigQuery 的REGEXP_CONTAINS也是如此。
02特性对照表
| JavaScript | Python re |
Java | PCRE2 | RE2 | |
|---|---|---|---|---|---|
先行断言 (?=…) |
是 | 是 | 是 | 是 | 否 |
后行断言 (?<=…) |
是,任意长度 | 仅限定长 | 是,长度有上限¹ | 是,长度有上限 | 否 |
反向引用 \1 |
是 | 是 | 是 | 是 | 否 |
| 命名分组 | (?<n>…) |
(?P<n>…) |
(?<n>…) |
两种都支持 | 两种都支持 |
| 命名反向引用 | \k<n> |
(?P=n) |
\k<n> |
两种都支持 | 否 |
原子分组 (?>…) |
否 | 是(3.11+) | 是 | 是 | 否 |
占有量词 a++ |
否 | 是(3.11+) | 是 | 是 | 否 |
Unicode 属性 \p{L} |
是,需加 u 修饰符 |
否 | 是 | 是 | 是 |
| 文字系统,如希腊文 | \p{Script=Greek} |
否 | \p{IsGreek} |
\p{Greek} |
\p{Greek} |
\d 匹配 ٣(阿拉伯-印度数字 3) |
否 | 是 | 否 | 仅在 UCP 模式下² | 否 |
递归 (?R) |
否 | 否 | 否 | 是 | 否 |
条件匹配 (?(1)…|…) |
否 | 是 | 否 | 是 | 否 |
内联修饰符 (?i) |
否³ | 是 | 是 | 是 | 是 |
^abc$ 匹配 "abc\n" |
否 | 是 | 是 | 是 | 否 |
文本末尾 \z |
否 | 否,用 \Z |
是 | 是 | 是 |
| 遇到糟糕的模式可能卡死 | 是 | 是 | 是 | 达到上限后停止⁴ | 否 |
- Java 文档要求后行断言有上限长度,但在我们的测试中 Java 17 也接受了
(?<=a+)。不要依赖这一点。 - PCRE2 默认的
\d只匹配 ASCII。PHP 的/u修饰符会开启 UCP 模式,所以\d在那里匹配了٣。 - Node 22 既不接受
(?i),也不接受限定范围的(?i:…)。限定范围的写法是 ES2025 新增的,只在较新的运行时中可用。 - 见下文的速度测试:PHP 返回了错误,而不是结果。
03迁移模式时会出问题的差异
$ 与结尾换行符
在 Python、Java 和 PCRE 中,^abc$ 能匹配 "abc\n",因为它们的 $ 也会在末尾换行符之前匹配。JavaScript 和 RE2 则不会。从文件读入、仍带着换行符的一行,在一种语言里能通过校验,在另一种语言里却会失败。要表示“真正的末尾”,在 Java、PCRE 和 RE2 中用 \z,在 Python 中用 \Z(Python 的 \Z 就是绝对末尾)。JavaScript 两者都没有:不加 m 修饰符时,它的 $ 本来就表示真正的末尾。
后行断言
JavaScript 允许任何后行断言,包括 (?<=\$\s*)\d+ 这样的变长断言。Python 的 re 只允许定长,PCRE2 允许有上限的长度,RE2 则完全不支持。可移植的做法是匹配前缀,再捕获你真正想要的部分:
在所有引擎中,包括 RE2,第 1 组都是那个数字。
命名分组
(?<year>\d{4}) 在 JavaScript、Java、PCRE2 和 RE2 中都能用,但 Python 3.12 的 re 不接受,要求写成 (?P<year>\d{4})。按名称的反向引用也不同:JavaScript、Java 和 PCRE 用 \k<year>,Python 用 (?P=year)。这两者的完整差异见JavaScript 与 Python 正则表达式。
\d、\w 与 Unicode
在 Python 3 中,\d 和 \w 默认匹配所有文字的数字和字母,所以 \d 接受 ٣。在 JavaScript、Java 和 RE2 中,它们只匹配 ASCII。如果表单字段必须是 0-9,就写 [0-9],这样模式在哪里都是同一个意思(在 Python 中也可以传入 re.ASCII)。
要匹配任何语言的字母,\p{L} 在 JavaScript(需加 u 修饰符)、Java、PCRE2 和 RE2 中都能用,但 Python 的 re 不支持。
原子分组与占有量词
(?>a+) 和 a++ 会尽可能多地匹配,而且绝不吐回,这是阻止失控回溯的一种方法。Java 和 PCRE 早已支持,Python 在 3.11 中加入。JavaScript 和 RE2 不支持;在 JavaScript 中,带反向引用的先行断言 (?=(a+))\1 可以起到同样的作用。
04速度:哪些引擎会卡死
经典的糟糕模式是用 (a+)+$ 去匹配一长串字母 a 后跟一个 !。回溯型引擎会尝试把这串字母分配给内外两个 + 的所有方式,然后才放弃。下面是各引擎处理 24 个字母加一个 ! 的结果:
| 引擎 | 结果 |
|---|---|
| JavaScript (Node 22) | 11.2 秒 |
| Python 3.12 | 1.8 秒 |
| Java 17 | 不到 1 毫秒 |
| PCRE2 (PHP) | 3 毫秒后放弃,报错“Backtrack limit exhausted” |
| RE2 | 不到 1 毫秒,即使是 10 万个字母 |
有两点值得注意。Java 17 对这种形式做了优化,但它仍是回溯型引擎:加上反向引用,比如 ^(a+)+\1$,输入从 16 个字母增加到 20、24 个时,耗时从 9 毫秒涨到 45 毫秒,再到 590 毫秒。PHP 不会卡住,但 preg_match 会返回 false,只检查匹配结果是否为真的代码会把它当成“没有匹配”,而察觉不到出了错。
只有 RE2 对所有模式都保证线性时间,所以它是处理用户输入模式的安全选择。灾难性回溯讲解了如何在任何引擎中发现并修复这类模式。
05写出到处都能用的模式
如果一个模式要在多个引擎中运行,比如同时在浏览器和服务器上,下面这个子集在五个引擎中的行为完全一致:
- 字面文本、
[a-z0-9_]这样的字符类、.,以及量词* + ? {n,m}及其懒惰版本。 - 指行时用锚点
^和$加m修饰符;指整段文本时在服务器端用\A…\z(并且绝不要把结尾换行符交给 JavaScript 的$)。 - 分组
(…)和(?:…),以及分支|。 - 如果必须拒绝其他文字的数字,用
[0-9]而不是\d。 - 如果可能由 RE2 运行,就不要用环视和反向引用;改为捕获你想要的部分。
然后在模式将要运行的每个引擎中测试它。沙盒使用 JavaScript 的引擎,和本页相同。
06练习
练习包和每日狩猎使用 JavaScript 的引擎,所以那里可以放心使用后行断言和命名分组。正则指南用实时示例讲解每个特性。