应使用白名单枚举合规号段:/^1(3[0-9]|4[5-9]|5[0-35-9]|7[2-8]|8[0-9]|9[0-9])\d{8}$/,明确排除170/171/162/165/167等虚拟号段,避免负向断言导致误判。

怎么用 PHP 正则排除 170/171/162/165/167 等虚拟运营商测试号段
PHP 的 preg_match() 本身不支持“排除某段”的语法,所谓“排除测试号段”,本质是**只允许匹配真实运营商的号段范围**。直接写“非 170 开头”这种逻辑在正则里容易漏判或误杀(比如 1700 要排除,但 170 本身是三位前缀,必须匹配完整 11 位才有效)。
正确做法是:明确列出所有合规号段的前三位(或前四位),用 | 枚举,再补上后 8 位数字校验。国内主流真实号段包括:13[0-9]、14[5-9]、15[0-35-9]、17[2-8]、18[0-9]、19[0-9],注意跳过 170、171、16[257] 等已知虚拟号段。
-
170、171、162、165、167、144、141这些必须从枚举中剔除,不能靠否定式(如(?!170))——它会在中间位置也触发,导致17012345678和13717012345都被误拒 - 工信部 2023 年新增了
19[2-3]、19[5-9],但194目前仍是空号段,暂不建议放开 - 开头必须是
1,第二位不能是0或1(10、11是电信服务号),所以1[3-9]是底线,但还不够细
推荐用的 PHP 正则模式(含号段白名单)
下面这个表达式覆盖截至 2024 年主流实名制要求,兼顾可读性和校验精度:
/^1(3[0-9]|4[5-9]|5[0-35-9]|7[2-8]|8[0-9]|9[0-9])\d{8}$/
说明:
-
3[0-9]→ 包含130–139,全部保留(不含测试号) -
4[5-9]→ 排除140–144,只留145–149(其中144是物联网卡,141已停用) -
5[0-35-9]→ 排除154(虚拟号段),其他都可用 -
7[2-8]→ 明确跳过170、171,只留172–178(含 176、177 等真实号段) -
8[0-9]和9[0-9]全部保留,目前无公开测试号段混入
实际使用时,建议加 strlen($phone) === 11 前置判断,避免正则处理超长字符串;若需兼容带 + 或 - 的国际格式,先用 preg_replace('/[^0-9]/', '', $phone) 清洗。
为什么不用 (?!170|171) 这类负向先行断言
看起来简洁,但实际会出问题。例如:
/^(?!170|171|162)\d{11}$/
这个表达式会把 17012345678 拒掉,但也可能让 13717012345(真实号码)因为中间出现 170 被误判——(?!...) 是从字符串开头逐位检查,不是只看前三位。
- 负向断言适合“禁止某前缀”,但必须配合锚点和精确长度,比如
^(?!170|171|16[257])1[3-9]\d{9}$才勉强可用,但可读性差、维护成本高 - 一旦号段规则更新(如新增
140实名号),白名单方式只需改一处,而负向断言要反复验证边界 - PHP 的 PCRE 引擎对复杂负向断言优化一般,大量并发校验时性能略低于纯枚举
上线前必须做的三件事
光写对正则不够,真实环境常因这些细节翻车:
- 用真实号段样本集测试:至少包含
13800138000(移动)、18912345678(电信)、15600000000(联通)、17612345678(真实 176)、以及17012345678、16512345678等应拒号码 - 注意 MySQL 存储时字段类型:别用
INT存手机号(会丢首位 1),必须用VARCHAR(11)或CHAR(11) - 前端 JS 校验建议复用同一套正则逻辑(去掉 PHP 的
/包裹),避免前后端不一致导致用户提交失败却不知原因
号段规则每年都在微调,硬编码白名单比动态查库更轻量,但记得每半年核对一次工信部最新分配表——上次漏掉 192 就是因为没同步 2022 年的公告。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











