同一个正则,在 Java、Python、JavaScript 里结果不同
前端用 JavaScript 校验手机号,后端用 Java 再校验一次,数据分析脚本用 Python 清洗。三处的正则是从同一份文档里复制的,一字不差。某天一个用户填了全角数字,前端拒绝了,Python 脚本却把它当成了合法号码。
正则表达式看起来是一种通用语言,但每种编程语言的正则引擎都有自己的默认行为:\d 包不包括非 ASCII 数字,$ 会不会匹配换行前的位置,哪个 API 要求整串匹配,替换模板用 $1 还是 \1,split 会不会丢掉末尾的空串。同一个表达式在三个地方复制粘贴,得到的是三套略有不同的规则。
本文把同一组用例分别在 JDK 25、Python 3.14、Node 24 上运行,逐项对比结果。实验见文末。
一、字符类的范围:\d 和 \w 包括什么
^\d+$ 匹配「١٢٣」(阿拉伯-印度数字) Java false Python True JS false
^\d+$ 匹配「123」(全角数字) Java false Python True JS false
^\w+$ 匹配「中文」 Java false Python True JS falsePython 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。
二、锚点与整串匹配
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;加上 ^$ 后 falseJava 和 Python 的 $ 默认也能匹配末尾换行符之前的位置,JavaScript 的不能。用 ^…$ 校验输入时,Java 和 Python 会接受一个结尾带换行的字符串,这个换行可能一路被存进数据库。严格的整串校验在 Java 里用 \z,在 Python 里用 \Z 或 fullmatch。
各语言「是否匹配」的 API 名字相似、语义不同:
| 语言 | 整串匹配 | 只锚定开头 | 任意位置 |
|---|---|---|---|
| Java | matches() | lookingAt() | find() |
| Python | fullmatch() | match() | search() |
| JavaScript | 表达式里写 ^…$ | — | test()、exec() |
最容易出错的是 Python 的 match:它的名字听起来像「匹配整个字符串」,实际只要求开头匹配,re.match(r'\d{3}', '1234') 是成功的。
三、命名分组与替换模板
(?<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 的边界
"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
忽略大小写匹配 é 与 É 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 倍。
配套实验
- codesphere-labs/engineering/regex-across-languages:JDK 25.0.4.1、Python 3.14.8、Node 24.21.0:字符类范围、$ 与换行、整串匹配 API、命名分组与替换模板、split、忽略大小写、lastIndex(验证记录)
参考资料