唯一值得抄走的那条正则
如果你只想挡掉明显的垃圾输入,并且愿意接受"长得对"不等于"真的对",那就用这条:
^\+[1-9]\d{1,14}$
它实现的是 E.164 标准,也就是每家运营商和短信网关最终存储号码的格式。长成这样的号码,可以直接丢给短信接口或电话 API,不用再做二次转换。下面要讲的,是为什么这只是整件事里最简单的那一成。
这条 E.164 正则到底匹配了什么
从左到右拆开看:
^\+—— 字符串必须以一个加号开头。这个加号就是国际前缀,在 E.164 里没有商量余地。[1-9]—— 加号后面的第一位是国家码,而没有任何国家的国家码以 0 开头。前导零属于各国自己的拨号规则,不属于 E.164。\d{1,14}—— 后面再跟 1 到 14 位数字,包含地区码和用户号。ITU 规定整个号码(含国家码)最长 15 位。$—— 到此为止,不允许再有别的字符,没有空格、连字符、括号。
所以 +14155552671 通过,而 +0123456789 正确地被拒,因为国家码不能以 0 开头。
为什么各国格式永远凑不到一块
只要离开 E.164,世界就碎了。同样的七位数字,换个国度意思完全不一样:
- 美国(NANP):号码结构是三位区号、三位交换局号、四位用户号。区号和交换局号的首位都必须是 2–9,
212、415、312是真实存在的,而012、114不是。 - 英国:区号长度不固定(伦敦是
20,很多城镇用 4 或 5 位),手机号以07开头。用一条固定宽度的正则在英国基本会写成一坨没人看得懂的东西。 - 中国:手机号恰好 11 位,以
1开头、后面接3–9;座机则内嵌长度不一的区号。13912345678是手机号,01012345678是北京座机。
这些规则没有一条是稳定的。运营商会回收、重新分配号段,新号段不断开放,国家码也会变。今天写死在你代码里的正则,十八个月后就悄悄错了。
一份精确的北美(NANP)正则
如果你的产品只服务美国、加拿大和少数加勒比国家,NANP 这条规则值得记住:
^\(?([2-9]\d{2})\)?[-. ]?([2-9]\d{2})[-. ]?(\d{4})$
最关键的一点是区号和交换局号开头都是 [2-9]。NANP 禁止用 0 和 1 作为这两段的首位——0 留给总机,1 留作长途接入。可选的 \(? 和 \)? 让用户既可以写 (415) 555-2671,也可以写 415-555-2671。这条正则既正确又紧凑,而这也正是它的危险所在:它只对一个地区正确,放到别处就彻底翻车。
为什么你不该用正则校验电话号码
这是本页的核心。正则检查的是"形状",不是"合法性"。Google 的 libphonenumber 库之所以存在,正是因为形状检查既简单又基本没用。这个库带着每个国家完整且频繁更新的元数据:哪些号码长度是合法的、哪些号段被分配了、怎么把本地格式归一化成 E.164、怎么识别明显的手误。当某个国家重新编号时,你跑一次库升级就行,而不是发一个正则补丁、还指望没人发现。
用库来解析和格式化。如果你确实需要确认一个号码是活的,就发一条短信或打个语音电话做一次性验证——那才是唯一有意义的检查。
正则依然会搞砸的地方
- 把国家码写死:写成
^\+1\d{10}$等于把自己锁死在美国,悄悄拒绝所有海外用户。不带国家码假设、只用 E.164 反而更安全。 - 禁止分隔符:用户会输入
+44 20 8366 1177这种带空格的写法。测试前先去掉非数字字符,或者在正则里放行分隔符,否则你会拒掉合法输入。 - 把分机号吃掉:企业号码常带
x1234或ext. 1234。过窄的正则会把这部分丢掉,电话永远到不了该找的人。 - 轻信结果:形状完美的号码依然可能根本没被分配。正则永远无从得知这一点。
格式校验 vs 真实存在
把这两个问题分开,因为混为一谈就会出 bug:
- "这串字符长得像个电话号码吗?" —— 正则或 libphonenumber 都能回答。成本低,适合在提交表单时挡掉手误。
- "这个号码属于一个真实、能联系上的人吗?" —— 只有验证消息能回答。任何静态模式都做不到,因为"是否被分配"是运营商掌握的实时事实。
把精力花在第二个问题上,用验证消息来确认;形状检查只当作第一道门槛就好。
真例子:放行与拒绝
| 输入 | 结果 | 原因 |
|---|---|---|
| +14155552671 | 放行 | 合法 E.164,美国号码,加号后 11 位 |
| +442083661177 | 放行 | 合法 E.164,英国号码 |
| +8613912345678 | 放行 | 合法 E.164,中国手机号 |
| +0123456789 | 拒绝 | 国家码不能以 0 开头 |
| 14155552671 | 拒绝 | 缺少必需的加号前缀 |
| +1415555 | 拒绝 | 位数太少,不是任何真实 NANP 号码 |
该用什么来代替
把这个问题交给有人维护的库。Python 里这样写:
from phonenumbers import parse, is_valid_number
try:
n = parse("+14155552671", None)
if is_valid_number(n):
print("看起来是真的")
except Exception:
print("连号码都不是")
JavaScript 里用 libphonenumber-js 包,思路一样。先解析,再校验,最后用消息验证。你的正则只是块门垫,不是保安。