JavaScript 与 Python 正则表达式:那些会坑你的差异
同一个正则在 JavaScript 和 Python 中的行为可能不同。这些差异会引发真实的 bug:re.match 与 test、$ 与末尾换行、Unicode 的 \d、findall 与分组、replace 与 sub、后行断言的宽度、命名组语法以及内联修饰符。每一条结论都在 Node 23 和 Python 3.12 中验证过。

· 阅读约 3 分钟
/(?<=\$)\d+(\.\d\d)?/
抓住
- $5
- cost $12.50
- $0.99
- pay $100
放过
- 5
- 5$
- USD 12
- $.50
JavaScript 和 Python 的正则语法大部分是相同的:字符类、量词、分组、锚点、环视。正因如此,差异才危险。一个正则从一边复制到另一边,通常都能编译通过,然后行为却有细微的不同。
下面是会引发真实 bug 的差异,大致按踩坑频率排序。下文的每一种行为都在 Node 23 和 Python 3.12 中验证过。本页的可交互示例在你的浏览器里运行,所以它们展示的是 JavaScript 的行为。
01re.match 只看开头
JavaScript 的 test() 和 match() 会搜索整个字符串。Python 有三个函数,其中名叫 match 的那个只在开头匹配:
import re
re.match(r"\d", "a1") # None: the string doesn't start with a digit
re.search(r"\d", "a1") # matches "1"
re.fullmatch(r"\d", "1") # the whole string must match
/\d/.test("a1"); // true: test() searches
所以 JavaScript 的 test() 相当于 Python 的 search()。而在 Python 中做校验时,re.fullmatch 通常才是你想要的:不会忘记加锚点。
02$ 与末尾换行
在 Python 中,$ 既匹配字符串的最末尾,也匹配最后一个换行符之前的位置。JavaScript 的 $(不加 m 修饰符时)只匹配最末尾:
re.match(r"^\d{5}$", "90210\n") # matches in Python
/^\d{5}$/.test("90210\n"); // false in JavaScript
在 Python 中,用 re.fullmatch 或 \Z 就能堵上这个漏洞。关于锚点与多行模式的文章对此有详细介绍。
03Python 的 \d 和 \w 支持 Unicode
在 JavaScript 中,\d 永远等于 [0-9],即使加了 u 修饰符也一样。在 Python 3 中,对于 str 类型的正则,\d 匹配任何 Unicode 数字,\w 匹配任何文字系统中的字母:
re.fullmatch(r"\d+", "٣٤٥") # matches in Python (Arabic-Indic digits)
re.fullmatch(r"\d+", "٣٤٥", re.ASCII) # None
/^\d+$/.test("٣٤٥"); // false
/^\d+$/u.test("٣٤٥"); // still false
如果这些数字要交给 int() 或存进数据库,在 Python 中请用 [0-9] 或 re.ASCII。
04findall 返回的是分组,而不是匹配
如果正则里有捕获组,Python 的 findall 返回的是分组,而不是完整的匹配:
re.findall(r"\w=\d", "a=1 b=2") # ['a=1', 'b=2']
re.findall(r"(\w)=(\d)", "a=1 b=2") # [('a', '1'), ('b', '2')]
JavaScript 的 match 加上 g 修饰符时,无论正则里有什么分组,总是返回完整的匹配:
"a=1 b=2".match(/(\w)=(\d)/g); // ["a=1", "b=2"]
在 Python 正则里加一个“只为分组”的括号,会悄悄改变 findall 的返回值。请使用非捕获组 (?:…),或者用 finditer 配合 m.group(0)。
05replace 只替换一次,sub 全部替换
JavaScript 的 replace 只替换第一个匹配,除非正则带有 g。Python 的 re.sub 会替换所有匹配,除非你传入 count:
"a-b-c".replace("-", "+"); // "a+b-c"
"a-b-c".replace(/-/g, "+"); // "a+b+c"
"a-b-c".replaceAll("-", "+"); // "a+b+c"
re.sub(r"-", "+", "a-b-c") # "a+b+c"
re.sub(r"-", "+", "a-b-c", count=1) # "a+b-c"
替换字符串的语法也不同:JavaScript 用 $1 和 $<name>,Python 用 \1、\g<1> 和 \g<name>。
06后行断言的宽度
JavaScript 允许任意后行断言,包括长度可变的:
Python 的 re 会拒绝第二个正则,报错 look-behind requires fixed-width pattern。可以把它改写成几个固定宽度的分支 (?:(?<=USD)|(?<=USD ))\d+,或者使用第三方的 regex 模块。
07命名组的写法不同
| JavaScript | Python | |
|---|---|---|
| 定义 | (?<year>\d{4}) |
(?P<year>\d{4}) |
| 反向引用 | \k<year> |
(?P=year) |
| 在替换中使用 | $<year> |
\g<year> |
| 读取 | m.groups.year |
m.group("year") |
Python 3.12 的 re 会拒绝 JavaScript 的写法,报错 unknown extension ?<y。带命名组的正则不改动就无法在两者之间互通。
08内联修饰符
Python 接受写在正则内部开头的修饰符:(?i)abc。全局修饰符放在其他任何位置都会报错。两种语言都支持作用于分组的局部修饰符 (?i:a)b,它只让 a 忽略大小写。这在 JavaScript 中是新特性(ES2025,Node 23 以及当前版本的 Chrome 和 Firefox 都已支持),所以请确认你的目标环境。JavaScript 完全不支持开头的 (?i);修饰符要写在斜杠后面。
09只有一方才有的功能
- 仅 Python: 原子组
(?>…)和占有量词a++(3.11+)、\A和\Z、条件分组(?(1)…)。 - 仅 JavaScript: 配合
u修饰符使用的 Unicode 属性转义\p{L}(Python 需要regex模块)、粘性修饰符y,以及用d获取匹配索引。
10字符串中的转义
Python 的正则写在字符串里,所以要写成原始字符串 r"\d+",否则每个反斜杠都要写两遍。JavaScript 的正则字面量 /\d+/ 不需要额外转义。但 new RegExp("\\d+") 是从字符串构建的,和非原始的 Python 字符串一样,需要把反斜杠写两遍。
11试一试
可交互棋盘运行的是 JavaScript,所以这里可以用 (?<=\$)\d 这样的后行断言。不过标准杆要求更短:
抓住 $ 符号后面紧跟数字的行。
目标 · 抓住 0/4
- $5目标
- cost $12.50目标
- pay $100 now目标
- $0目标
无辜词 · 误抓 0
- 5无辜
- 5$无辜
- USD 12无辜
- $ 5无辜
- $.50无辜
输入正则——棋盘会实时更新。
12检查清单
- JS 的
test()会搜索。Python 的re.match只检查开头。请用re.search或re.fullmatch。 - Python 的
$允许末尾有一个换行。请用re.fullmatch或\Z。 - Python 的
\d和\w支持 Unicode。请用[0-9]或re.ASCII。 - 当正则中有分组时,Python 的
findall返回的是分组。 - JS 的
replace只改第一个匹配,除非使用g或replaceAll。Python 的sub全部替换。 - Python 要求后行断言是固定宽度的。
- 命名组:JS 中是
(?<n>…),Python 中是(?P<n>…)。