Skip to content

同一个正则,在 Java、Python、JavaScript 里结果不同 ​

前端用 JavaScript 校验手机号,后端用 Java 再校验一次,数据分析脚本用 Python 清洗。三处的正则是从同一份文档里复制的,一字不差。某天一个用户填了全角数字,前端拒绝了,Python 脚本却把它当成了合法号码。

正则表达式看起来是一种通用语言,但每种编程语言的正则引擎都有自己的默认行为:\d 包不包括非 ASCII 数字,$ 会不会匹配换行前的位置,哪个 API 要求整串匹配,替换模板用 $1 还是 \1,split 会不会丢掉末尾的空串。同一个表达式在三个地方复制粘贴,得到的是三套略有不同的规则。

本文把同一组用例分别在 JDK 25、Python 3.14、Node 24 上运行,逐项对比结果。实验见文末。

JavaPythonJavaScript^\d+$ 对「123」falseTruefalse^\w+$ 对「中文」falseTruefalseabc$ 对 "abc\n"trueTruefalse\d{3} 对 "1234" 的常用 APImatches: falsematch: Truetest: true(?<y>…) 命名分组支持报错支持替换模板 "$2/$1"10/2026$2/$110/2026"a,b,,".split(",")[a, b]4 项4 项忽略大小写 é 与 ÉfalseTruetrue共享规则时:字符集写明 [0-9];整串校验用明确的整串 API 或 \z;替换逻辑每种语言单独写和测JS 带 g 的正则反复 test 四个数字:[true, false, true, false]
图 1 · 实测(JDK 25.0.4.1、Python 3.14.8、Node 24.21.0):Python 的字符类默认按 Unicode 解释,全角数字和汉字都算;Java 与 Python 的 $ 能匹配末尾换行之前;Python 的 match 只锚定开头;替换模板一边用 $、一边用反斜杠;Java 的 split 丢掉末尾空串。颜色相同表示结果一致。

一、字符类的范围:\d 和 \w 包括什么 ​

text
^\d+$ 匹配「١٢٣」(阿拉伯-印度数字)   Java false   Python True   JS false
^\d+$ 匹配「123」(全角数字)         Java false   Python True   JS false
^\w+$ 匹配「中文」                       Java false   Python True   JS false

Python 3 的 str 正则默认按 Unicode 解释字符类:\d 是所有 Unicode 十进制数字,\w 包括汉字。Java 和 JavaScript 默认只认 ASCII。开头那个全角数字的问题就出在这里:Python 认为 123 是数字,而后续代码用 int() 转换它也能成功(int('123') 等于 123),于是一个前端会拒绝的输入,在数据管道里畅通无阻。

要在三处得到一致的结果,最可靠的做法是写明字符集:[0-9] 而不是 \d。也可以统一打开 Unicode 模式:Java 用 Pattern.UNICODE_CHARACTER_CLASS(实测加上之后全角数字也匹配了),Python 用 re.ASCII 反过来限制为 ASCII。

二、锚点与整串匹配 ​

text
abc$ 匹配 "abc\n"          Java true   Python True   JS false
\d{3} 对 "1234"            Java matches() false / find() true
                           Python match() True / fullmatch() False
                           JS test() true;加上 ^$ 后 false

Java 和 Python 的 $ 默认也能匹配末尾换行符之前的位置,JavaScript 的不能。用 ^…$ 校验输入时,Java 和 Python 会接受一个结尾带换行的字符串,这个换行可能一路被存进数据库。严格的整串校验在 Java 里用 \z,在 Python 里用 \Z 或 fullmatch。

各语言「是否匹配」的 API 名字相似、语义不同:

语言整串匹配只锚定开头任意位置
Javamatches()lookingAt()find()
Pythonfullmatch()match()search()
JavaScript表达式里写 ^…$—test()、exec()

最容易出错的是 Python 的 match:它的名字听起来像「匹配整个字符串」,实际只要求开头匹配,re.match(r'\d{3}', '1234') 是成功的。

三、命名分组与替换模板 ​

text
(?<y>\d{4}) 命名分组        Java 支持   Python 报错(PatternError),要写 (?P<y>…)   JS 支持
替换模板 "$2/$1"            Java 10/2026   Python 原样输出 $2/$1   JS 10/2026
替换模板 "\2/\1"            Java 2/1       Python 10/2026        JS 原样输出 \2/\1
命名分组的引用              Java ${m}      Python \g<m>          JS $<m>

替换模板是三层转义里最混乱的一层:Java 和 JavaScript 用 $,Python 用反斜杠;写错了不一定报错,而是原样输出或者得到一个奇怪的结果(Java 把 \2 解释成转义的字符 2)。在三种语言之间移植替换逻辑时,替换模板需要逐个重写。转义的问题在反斜杠数了三遍里单独讨论。

四、split 的边界 ​

text
"a,b,,".split(",")       Java [a, b]   Python ['a', 'b', '', '']   JS ["a","b","",""]
按 "." 切分 "a.b.c"       Java []       Python 6 个空串            JS 6 个空串

Java 的 String.split 默认丢掉末尾的空串,解析 CSV 时最后几列为空就会少几列,要用 split(",", -1) 保留。split 的参数在 Java 里是正则,. 匹配任意字符,所以 "a.b.c".split(".") 返回空数组;Python 的 str.split 和 JavaScript 传字符串时按字面切分,只有显式用正则(实验里的 re.split、split(/./))才会这样。

五、忽略大小写与 JS 的 lastIndex ​

text
忽略大小写匹配 é 与 É      Java false(加 UNICODE_CASE 后 true)   Python True   JS true
JS 带 g 的正则对 "1","2","3","4" 反复 test:[true,false,true,false]

Java 的 CASE_INSENSITIVE 默认只处理 ASCII,非拉丁字母要再加 UNICODE_CASE。

JavaScript 里带 g(或 y)标志的正则对象有状态:test 和 exec 成功后会把 lastIndex 设为匹配结束的位置,下一次从那里开始。把一个带 g 的正则存成常量反复用来校验,结果会交替成功和失败。校验用的正则不要加 g。

六、在多种语言之间共享规则 ​

  • 字符集写明范围:[0-9]、[A-Za-z0-9_],不依赖 \d、\w 的默认含义。
  • 整串校验使用各语言明确的整串 API 或 \z,不依赖 $。
  • 替换逻辑在每种语言里单独编写和测试,不复制替换模板。
  • 共享一组测试用例(包括全角数字、末尾换行、空字段),在每种语言的实现上都跑一遍。
  • 正则的性能边界也因引擎而异,见输入只多了 4 个字符,正则的工作量变成 16 倍。

配套实验

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权