指南

正则引擎对比:JavaScript、Python、Java、PCRE 和 RE2

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

Regex Hunter 标志

· 阅读约 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 是 是 是
遇到糟糕的模式可能卡死 是 是 是 达到上限后停止⁴ 否
  1. Java 文档要求后行断言有上限长度,但在我们的测试中 Java 17 也接受了 (?<=a+)。不要依赖这一点。
  2. PCRE2 默认的 \d 只匹配 ASCII。PHP 的 /u 修饰符会开启 UCP 模式,所以 \d 在那里匹配了 ٣。
  3. Node 22 既不接受 (?i),也不接受限定范围的 (?i:…)。限定范围的写法是 ES2025 新增的,只在较新的运行时中可用。
  4. 见下文的速度测试: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 则完全不支持。可移植的做法是匹配前缀,再捕获你真正想要的部分:

匹配4/4 符合预期
/\$\s*(\d+)/

应当匹配2/2

  • Total: $40
  • Total: $ 40

不应匹配2/2

  • Total: 40
  • Total: USD 40
在沙盒里试试

在所有引擎中,包括 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 的引擎,所以那里可以放心使用后行断言和命名分组。正则指南用实时示例讲解每个特性。