ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PHP手机号码归属地批量查询系统设计与实现

PHP手机号码归属地批量查询系统设计与实现 简介一套基于PHP的手机号码归属地批量查询系统面向PHP开发者和需要批量校验号码归属地的运营人员内置50.1万条公开号段数据供学习与轻量级应用参考。资源包仅404KB共3个文件一个PHP入口脚本、一个Db数据库文件以及一个Json索引配置代码与数据完整可快速部署。系统在数据压缩与查询优化上做了不少工作将59MB的SQL压缩至不足2.5MB采用7位号段转4位36进制、省市区邮编与运营商映射为短字母/数字等策略并将50.1万行号段分存至1296行通过后两位索引定位查询时先对号段去重再查询保持原始顺序输出。实测单条查询约0.001秒30条批量查询约0.014秒。资源同时附有部署建议如修改数据库名称、限制Json和Db文件直链下载等方便二次开发。已有84人学习适合想掌握数据压缩思路、提升PHP查询性能的开发者参考。1. 为什么需要一套 PHP 手机号码归属地批量查询系统运营人员手里攒了三万条用户手机号要按省份做活动投放催收和风控团队要清洗名单把无效号码和异地号码分开处理电商客服想统计订单里的手机号都分布在哪几个城市。逐个去网页工具里查一次只能查一个三万个号码要查到天黑而且网页工具基本都有查询上限查多了还要验证码。手机号码归属地批量查询系统解决的正是这个问题写一段 PHP 脚本或搭一个小服务把号码列表丢进去程序自动完成清洗、匹配、结果导出。这类系统在 2025 年的今天依然有实用价值因为归属地信息本身是低频变动的数据。号码段由运营商分配新增号段和携号转网会影响少量数据但大部分号码段在数月甚至数年内保持稳定所以离线号段库配合 PHP 脚本做批量查询比逐条请求在线 API 更经济、更快、更可控。适合的场景包括名单清洗、营销地域分析、数据补全、接口服务封装。下文按从数据准备到系统落地的路径把一套可复现的批量查询方案完整讲清楚。2. 归属地数据的三种来源与数据模型设计2.1 在线 API 虽省事但批量场景多数不合算目前市面上有大量手机归属地查询 API常见的有聚合数据、阿里云市场、极速数据等单次查询价格从几分到几毛不等。它们的优点是数据由服务方维护无需关心号段更新缺点是批量查询按条计费几万条号码跑一遍成本可观而且大多数 API 都有 QPS 限制频繁请求会被限流。如果是一次性的清洗任务用 API 可以接受如果系统要长期运行、周期性处理新导入的号码文件离线本地库几乎是必然选择。2.2 离线号段库的本质前 7 位映射手机号码归属地的核心规律是号码的前 3 位是运营商网号如 138、186、199加上第 4 到 7 位组成号段运营商按号段投放给特定地区的用户。因此归属地查询本质上是拿号码的前 7 位做字典查找命中号段记录后返回对应的省份、城市、运营商。一个常见的号段数据格式是每行一条记录包含号段、省份、城市、运营商、区号、邮编等字段。号段数据来源有几种渠道开源仓库的定期数据快照、运营商官网公示的号段分配文件、爬虫抓取的公开页面。无论哪种来源都需要转换成一份干净的、可增量更新的数据文件。项目下载后zip 包里通常自带一份数据文件但拿到手后第一步不是写查询代码而是先校验数据质量。常见的做法是检查号段是否 7 位、是否有重复号段、省份名称是否统一这三类脏数据在真实项目里出现频率很高。2.3 号段表结构一表定乾坤还是双表更稳多数查询系统只用一张号段表字段设计如下。后续的查询、统计、更新都围绕这张表展开字段不过度设计避免关联查询拖慢批量场景的响应速度。字段名类型说明prefixCHAR(7)号码前 7 位主键provinceVARCHAR(32)省份名称cityVARCHAR(32)城市名称carrierVARCHAR(16)运营商名称area_codeVARCHAR(8)区号zip_codeVARCHAR(8)邮编updated_atDATETIME数据更新时间把 prefix 作为主键是因为查询只走前缀匹配不需要额外索引。如果数据量达到几十万条全国号段大约 45 万条左右用 InnoDB 主键查找的耗时在毫秒级批量处理一万条号码也只需要几秒钟。这里不建议拆表存省份维度和号段维度查询路径多一次连接性能收益为零。2.4 号码清洗查询之前的必修课号码清洗排在查询之前是因为文件导入的号码几乎不可能全部符合标准格式。带 86 前缀的、中间有空格或横线的、混入座机号的、长度不对的都要在进入查询流程前过滤或归一化。function normalizePhone($raw) { $phone preg_replace(/[\s\-\(\)]/, , $raw); if (strpos($phone, 86) 0) { $phone substr($phone, 3); } elseif (strpos($phone, 86) 0 strlen($phone) 13) { $phone substr($phone, 2); } return $phone; } function isValidMobile($phone) { return preg_match(/^1[3-9]\d{9}$/, $phone) 1; }清洗函数处理三种常见脏格式去掉空格、短横线和括号去掉 86 或 86 的国际区号前缀最终用正则校验号码是否以 1 开头、第二位在 3 到 9 之间、总长度 11 位。注意86开头的处理必须判断长度否则可能误伤正常号码。清洗环节的返回值应该分两类格式合法的号码进入查询队列不合法的号码单独记录原因便于后续人工处理这是批量系统里容易被忽略但实际很影响用户体验的设计。3. PHP 查询引擎核心实现与性能优化3.1 内存字典查询比数据库查询更快的路径批量查询个数以万计的时候每条号码都走一次 SQL 查询也能跑完但连接开销和查询开销加在一起耗时会比内存查询高一个数量级。常见做法是启动时把号段表一次性加载到 PHP 数组中后续查询直接操作内存字典。PHP 数组底层的 HashTable 实现使得字符串键的查找复杂度接近 O(1)四十五万条记录加载到内存大约占用 100 到 150 MB在 CLI 模式下完全可接受。class PhoneLocationService { private array $prefixMap []; public function loadFromCsv(string $csvPath): int { $handle fopen($csvPath, r); $header fgetcsv($handle); $count 0; while (($row fgetcsv($handle)) ! false) { if (count($row) 5) continue; $this-prefixMap[$row[0]] [ province $row[1], city $row[2], carrier $row[3], area_code $row[4] ?? , ]; $count; } fclose($handle); return $count; } public function lookup(string $phone): ?array { $phone normalizePhone($phone); if (!isValidMobile($phone)) { return null; } $prefix substr($phone, 0, 7); return $this-prefixMap[$prefix] ?? null; } }加载函数逐行读取 CSV忽略表头将前 7 位作为关联数组的键后面的省份、城市等作为值。查询函数先做归一化和格式校验然后截取前 7 位直接索引数组。注意?? null的兜底逻辑号段库缺少新号段时返回空不会抛错。这个类的设计把数据加载和查询路径分离后续要做接口封装或 CLI 工具都可以复用同一套逻辑。3.2 批量处理控制内存峰值的分批策略一次性把十万条号码全部读入数组再做循环内存会随号码数量线性增长。按批处理是更稳的做法每批读 500 到 1000 条处理后立即释放处理结果累加到统计数组。这样内存占用主要由号段字典决定与号码总量无关系统能处理的数据量就没有上限了。function batchLookupCsv(string $inputPath, string $outputPath, int $batchSize 500): array { $service new PhoneLocationService(); $service-loadFromCsv(__DIR__ . /phone_prefix.csv); $in fopen($inputPath, r); $out fopen($outputPath, w); fputcsv($out, [手机号, 省份, 城市, 运营商, 查询状态]); $batch []; $stats [total 0, hit 0, invalid 0, miss 0]; while (($row fgetcsv($in)) ! false) { $batch[] $row[0] ?? ; if (count($batch) $batchSize) { processBatch($service, $batch, $out, $stats); $batch []; } } if ($batch) processBatch($service, $batch, $out, $stats); fclose($in); fclose($out); return $stats; } function processBatch(PhoneLocationService $service, array $batch, $out, array $stats): void { foreach ($batch as $phone) { $stats[total]; $clean normalizePhone($phone); if (!isValidMobile($clean)) { $stats[invalid]; fputcsv($out, [$phone, , , , 无效号码]); continue; } $info $service-lookup($clean); if ($info null) { $stats[miss]; fputcsv($out, [$phone, , , , 号段未收录]); } else { $stats[hit]; fputcsv($out, [$clean, $info[province], $info[city], $info[carrier], 成功]); } } }批量函数每凑满 500 条就调用一次 processBatch处理函数在循环内部按状态写入结果文件最后释放 batch 数组。统计数组使用引用传递避免每次循环复制一份数据。这里把无效号码、号段未收录、成功三类状态分开不只是为了统计更重要的是让下游拿到结果后可以直接执行不同的后续动作无效号码剔除、未收录号码送人工补录、成功号码进入分发名单。3.3 优先用字符串前缀而不是正则提取号段有些实现会在循环内部用substr($phone, 0, 7)提取前缀这个没问题但见过有人用正则preg_match(/^(\d{7})/, $phone, $m)来取性能差了三到五倍。批量场景下每个号码哪怕只多花 20 微秒一万条就是 200 毫秒可以接受但十万条就是两秒完全没必要。字符串函数能做的事正则尽量少用。另外提取前缀前一定先做长度校验否则号码长度不足 7 位时substr 会返回截断后的短字符串命中不了任何号段白白增加 miss 数量。3.4 虚拟运营商号段的特殊处理170、171、162、165、167 开头的号码属于虚拟运营商数据文件里可能没有覆盖全或者覆盖了但在运营商字段上有多种写法。建议在查询引擎里做一次映射如果号段前三位匹配虚拟运营商字段就返回“虚拟运营商”而不用号码归属地的省市级信息——因为虚拟运营商的号码绑定的是转售企业并不严格对应传统的地域归属。做名单清洗时这类号码通常需要单独分流因为它们的地域信息没有参考价值。4. 批处理任务的 Web 化文件上传、进度反馈与结果下载4.1 用 Web 界面跑批量任务的两个核心问题命令行脚本自己用没问题但要把这套系统交付给运营或客服同事就得包一层 Web 界面。界面本身不复杂文件上传加结果下载。两个核心问题要提前规划第一个是执行超时PHP 默认max_execution_time是 30 秒哪怕在 php.ini 里调大Nginx 和代理层也有自己的超时配置第二个是大文件上传限制upload_max_filesize 和 post_max_size 的默认值都偏小几 MB 的 CSV 文件可能都传不上去。同步执行模式只适合几百条的小文件几万条以上建议改成异步文件上传后写入待处理队列后台常驻进程消费队列。这里不需要引入重量级队列中间件用 MySQL 表模拟任务队列就够。任务表设计为 status 字段标识排队中、处理中、已完成、失败worker 脚本定时拉取排队中的任务并更新状态。CREATE TABLE lookup_task ( id INT AUTO_INCREMENT PRIMARY KEY, file_name VARCHAR(255) NOT NULL, file_path VARCHAR(500) NOT NULL, result_path VARCHAR(500) DEFAULT NULL, status TINYINT DEFAULT 0 COMMENT 0排队 1处理中 2完成 3失败 4取消, total_count INT DEFAULT 0, success_count INT DEFAULT 0, fail_count INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;任务表记录了任务状态和统计信息前端轮询接口查看 status处理完成就显示下载链接。文件路径建议存相对路径根目录统一配置避免用户上传文件名包含路径跳转符号造成安全问题。前端轮询间隔建议 2 到 3 秒不需要上 WebSocketPHP 接口返回 JSON 格式的数组对象给前端解析实用且省开发量。4.2 后台 worker 脚本命令行模式下的 PHP 执行worker 脚本用 PHP CLI 模式运行不依赖 Web 服务器超时限制。一个常见的实现是脚本内死循环每次迭代从任务表拉取一条排队任务处理完标记完成然后继续下一轮。为了适配 crontab 场景比如每 5 分钟触发一次脚本可以加一个--once参数执行一次就退出避免常驻内存时的内存泄漏风险。// worker.php $pdo new PDO(mysql:host127.0.0.1;dbnamephone_db, user, pass); $stmt $pdo-query(SELECT * FROM lookup_task WHERE status 0 ORDER BY id ASC LIMIT 1); $task $stmt-fetch(PDO::FETCH_ASSOC); if (!$task) exit(no task\n); $pdo-exec(UPDATE lookup_task SET status 1, updated_at NOW() WHERE id {$task[id]}); $result batchLookupCsv($task[file_path], $task[result_path], 1000); // 统计结果回写 $update $pdo-prepare(UPDATE lookup_task SET status 2, total_count ?, success_count ?, fail_count ?, updated_at NOW() WHERE id ?); $update-execute([$result[total], $result[hit], $result[total] - $result[hit], $task[id]]); echo task {$task[id]} done\n;这段逻辑的关键是把任务状态在开始处理前就改为处理中避免多个 worker 并发时重复消费同一条任务。悲观锁在这里比 SELECT FOR UPDATE 简单因为单个任务处理时间不长冲突概率低。统计字段拆成 total 和 fail页面展示时可以直接算出命中率不用二次查询明细文件。CLI 脚本里的错误处理用 try-catch 包住主流程失败时把任务状态改为失败并记录错误日志到单独的文件。4.3 用 Redis 队列替换 MySQL 简单轮询的可选方案如果系统里已经引入了 Redis把任务 ID 直接丢到 Redis 的 List 结构里worker 用阻塞读取命令BRPOP拉取任务响应速度和代码简洁度都好于 MySQL 轮询。但代价是任务状态需要单独表维护Redis 里存的是只是待处理信号消费后仍要回写 MySQL 的 status。从运维角度讲引入 Redis 的时机应该是已有其他业务共用该 Redis而不是单独为这个查询系统安装一个。Nginx 上传大小配置也需要同步修改常见的配置是 client_max_body_size 20m超出后后端拿不到文件浏览器看到的是 413 错误排查时要从 Nginx 错误日志入手。4.4 结果文件按任务隔离避免并发下载串号多用户同时使用时结果文件路径如果直接拼用户 ID可能被其他用户猜测遍历。建议结果文件名为任务 ID 加随机串存储在 Web 根目录之外通过 download 接口读取文件流输出。下载接口用 PHP 的 readfile 配合 headers 强制触发下载文件名根据任务 id 从库里查出原始文件名拼上前缀再输出。$filePath $config[upload_dir] . / . $task[result_path]; if (!file_exists($filePath)) { http_response_code(404); echo json_encode([error 文件不存在或已被清理]); exit; } header(Content-Type: application/csv; charsetutf-8); header(Content-Disposition: attachment; filenameresult_ . $task[id] . .csv); header(Content-Length: . filesize($filePath)); readfile($filePath);readfile 会自动把文件内容输出到缓冲区文件略大也不会占用额外 PHP 内存因为它是流式读取。文件名直接引用原始任务 ID对运营人员来说便于和上传时的文件对应。接口统一返回 JSON 结构出错时不输出 HTML方便前端 fetch 处理统一弹窗提示这是 PHP 接口数组对象的标准做法前后端解耦明确。5. 归属地库的增量更新与乱码处理5.1 号段库更新策略全量替换优先于逐条补丁号段数据的变化是低频事件每月甚至每季度更新一次就够。主流的数据源网站 — 手机吧、查号吧、ip33 等 — 会不定期更新号段文件。获取到新版数据后直接全量替换本地 CSV 是最省事的方案不涉及 diff 计算和脏数据合并。需要注意的是新文件里可能有旧文件没有的省份写法比如「内蒙古」和「内蒙古自治区」不一致导出前要做一次省份枚举的映射否则统计省份分布时会发现同一个省份被拆成了多个值。$provinceAlias [ 内蒙古 内蒙古, 内蒙古自治区 内蒙古, 广西 广西, 广西壮族自治区 广西, 新疆维吾尔自治区 新疆, 新疆 新疆, ];5.2 坑CSV 文件里的 UTF-8 BOM 和 GBK 编码从 Windows 上下载的号段文件大概率是 GBK 编码用 Excel 另存的 CSV 文件则可能带 UTF-8 BOM 头。直接加载进 PHP 数组时第一个号段的键会带着 BOM导致首条记录查不到。处理方式是在读取文件时先检测 BOM有则移除检测到非 UTF-8 编码时用 mb_convert_encoding 转为 UTF-8。转换后重新写入一份标准格式的文件后续加载不再重复转换。打开 Excel 导出的 CSV 乱码是同样的原因输出结果的 PHP 文件或接口返回 JSON 未指定 UTF-8 编码浏览器或 Excel 默认按 GBK 解析。统一在接口响应头加header(Content-Type: application/json; charsetutf-8)导出 CSV 时先输出 BOM 再把内容写文件。5.3 用命中率评估库的完整度号段库覆盖是否全面不能只靠数据提供方说要在系统里加一个命中率统计。批量处理完成后统计 hit 数除以总合法号码数。正常情况下2025 年新号段192、193、195、196、197、198、199 等如果能查询到命中率应该在 95% 以上。如果发现命中率低于 90%优先检查号段文件是否缺少了最近两年新增的号段而不是怀疑代码有 Bug。下面是一个简单的自动健康检查脚本片段public function healthCheck(): array { $samples [13800138000, 19912345678, 19212345678, 17012345678]; $result []; foreach ($samples as $phone) { $info $this-lookup($phone); $result[$phone] $info ? $info[province] . $info[city] : 未命中; } return $result; }5.4 号码状态统计分析批量查询只是第一步查询结果除了下载平台方通常还希望直接在界面看到汇总图表。借助 PHP 实现一个聚合统计接口从批次结果文件或明细表里按省份和运营商做分组计数。接口返回数组给前端前端用图表库渲染。统计口径注意区分按号码归属地统计和按用户当前所在地统计是两回事方案是对数据库里的归属地字段做 GROUP BY与号码解析逻辑保持一致。5.5 清理任务文件避免磁盘写满批量系统长期运行后上传目录会积累大量源文件和结果文件。按单文件 1 MB、每天一百个任务估算一个月磁盘占用约 3 GB对一台普通服务器还能接受但对小磁盘 VPS 来说就是隐患。建议在 worker 任务完成、结果文件被下载或超过 7 天后由清理脚本删除源文件和结果文件。MySQL 里任务记录保留但把文件路径置空防止下载接口读到不存在的文件时报 500。如果是 CLI 文件上传还要设置上传目录不可执行 PHP 脚本禁止 .htaccess 与伪静态规则之外还加 Nginx 或 Apache 的 location 禁止规则。这样处理查询系统长期运转不需要人肉盯磁盘。本文还有配套的精品资源点击获取
返回列表