ARTICLE DETAIL

资讯详情

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

PHP实现手机号段归属地数据抓取与更新全攻略

PHP实现手机号段归属地数据抓取与更新全攻略 简介中国移动、联通、电信手机号段归属地数据抓取与更新项目是一套面向开发者、金融机构及市场调研人员的PHP数据解决方案。它的核心价值在于通过自动抓取运营商公开发布的号段信息生成并持续更新可用于业务查询的归属地数据库适合需要精准用户画像、区域分析或CRM优化的场景。压缩包共11个文件大小1.02MB包含5个PHP抓取程序、4个TXT号段数据文件、1个README说明文档和1个附赠资源docx其中PHP脚本覆盖三大运营商的数据采集与整合逻辑TXT文件提供可直接导入的号段归属地基础数据说明文档则介绍安装部署与定期更新流程附赠资源中还包括API接口、处理脚本或用户指南类材料便于非专业用户快速上手。目前已有91人学习浏览。通过这套资源使用者能获得一套完整可落地的号段抓取与更新方案节省自行采集清洗数据的时间并灵活接入金融风控、精准营销、CRM等业务系统实现按地区维度的数据支撑与持续化更新。1. 手机号段归属地数据为什么值得用 PHP 自己抓一套做业务系统的后端迟早会碰上一次“信任崩塌”用户填了个 199 开头的手机号系统查归属地返回值是“未知”拿 170 开头的号码去问老号段库结果显示虚拟运营商再拿 148 开头去查直接空白。原因只有一个——号段数据远比想象中过期得快。运营商每年都在放新号第三方接口要么限流要么收费而网上流传的“最新号段文件”往往是改了个日期就发出来的旧数据。这个用 PHP 写成的号段归属地数据抓取与更新项目核心就是两件事把分散在公开渠道的移动、联通、电信号段数据定时抓下来持续更新和校验之后再导出一份干净、可交付的号段归属地数据文件供业务查询和开发环境直接使用。它不是高深算法而是一套能自己维护、不被外部接口绑架的数据保鲜方案。适合有 PHP 基础、需要做离线或半离线归属地查询的开发者。2. 先弄懂号段数据的来源与更新规律再动手抓取2.1 手机号的分段结构前三位只是运营商入口前四位才是归属地精度关键一个 11 位手机号在归属地层面拆成两段看前三位是号段中间四位是地区码。前三位决定运营商归属例如 130-132 联通、134-139 移动、133 电信中间四位在早期代表号码开户时的地区例如 1390 对应北京、1381 对应上海。但这里有一个很多人忽略的细节号段文件里如果只保留前三位查询精度只能到省级只有把前四位一起存下来才能做到城市粒度。举一个实际例子一个 1390 开头的号码和一个 1391 开头的号码前三位都是 139但地区码完全不同归属地可能是北京和重庆的区别。再往下说号段与地区码的对应关系不是不变的。早年运营商把大量地区码段投放给了用户部分中间四位不再严格对应初始地区加上携号转网落地后号码的当前运营商和开户运营商可以不同。因此数据库里存的内容本质上是“开户登记地”这一点从字段命名开始就要向业务方说清楚否则后患无穷。抓取程序在生成号段前缀时也必须清楚各运营商的号段区间大致分布否则连“运营商”这个字段都会被赋值错。下表是目前的号段总体分布概况具体以各运营商最新公示为准号段区间移动联通电信13x1340-1348、135-139130-13213315x150-152、157-159155-15615317x178176177、17318x182-184、187-188185-186180-18119x195、198-199196190-191、193这里最容易被抓取程序误判的是 170、171 号段。它们属于虚拟运营商租用基础运营商网络后放出的号段既不能简单归到移动或联通也不能归到电信需要在数据里单独打标。我的做法是给 operator 字段留一个额外值例如 0 表示虚拟运营商查询接口返回时附带说明避免业务方把虚拟运营商用户当成基础运营商用户去走错误流程。2.2 抓取来源的三大梯队官网公示页、第三方查询站、历史号段文件号段归属地数据没有官方统一的动态查询接口这是整个项目第一道认知门槛。运营商会定期公布号段启用公告但不会把全省市数据打包成一个 API 让你每天拉。所以抓取来源要做梯队设计不能盯死一个源。第一梯队是运营商官网的号段公示页。移动、联通、电信各自有公开的号段分配文档或公告权威性最高新号段启用的第一时间会更新。缺点是三家格式不统一移动可能只给号段范围电信会附带区号和邮编联通页面结构经常变直接抓非常费劲。这个梯队适合做增量发现不适合做全量拉取。第二梯队是第三方手机归属地查询网站。这类网站把全国号段聚合在一起查询响应快数据粒度可以到城市而且大部分支持简单的 GET 请求。用 curl 就可以拿到 JSON 或者 HTML。缺点是频率限制严格验证码和 UA 校验是常态部分站点还要求带签名参数。抓这里的数据必须提前想好限速策略否则跑 5 分钟就 403。第三梯队是历史号段数据文件也就是标题里提到的那类“最新号段数据文件”。这类文件通常是 CSV、Excel 或 SQL 导出字段齐、导入方便适合做整个数据库的初始化种子。但它们最大的问题是新鲜度不可控——文件作者不更新你拿到的就是一年前的旧数据。比较稳妥的用法是先把历史文件导入作为全量基线再靠第一、第二梯队做日常增量修正。优先级原则我固定为官网做新增发现第三方查询站做批量交叉验证历史文件做初始化种子三者轮流上岗缺一个都会出问题。2.3 数据更新的时间轴运营商发号节奏与最小更新闭环运营商发新号段是有时间规律的。常见的是年初大批量放号年中补充年底配合新套餐再放一批但具体日期没有承诺。因此更新脚本不能按“想起来才跑”的节奏设计我的最小闭环是日增、周全量、月归档三层。日增任务是每天凌晨跑目标是从运营商官网增量页抓当天新增号段插入增量表周全量任务在每周日凌晨跑把增量表和全量基线做合并重新生成一份完整的号段表月归档是每月 1 号把上一版全量文件按日期改名存档例如mobile_location_20250115.csv防止某次全量重建后数据反而丢失能快速回滚。这个节奏真正解决的问题是“静默过期”。如果某天抓取脚本因为页面改版挂了当天增量没抓到不用慌——周日的全量重建会把遗漏补回来。而如果没有月归档回滚就没有后悔药。我见过不少团队把更新脚本跑起来之后就再也不看结果三个月后数据文件里全是空的连项目带头人都没发现这比抓不到数据更可怕。3. 用 PHP 落地抓取与更新程序从 curl 到数据文件3.1 抓取函数与两个关键超时参数抓取号段的核心在 PHP 里就是 curl。我先写一个通用抓取函数输入手机号或号段前缀输出解析后的数组失败返回 false。这个函数在所有批量任务里复用避免业务代码里到处堆 curl 逻辑。?php /** * 抓取单个号段的归属地信息 * param string $mobile 手机号或号段前缀 * return array|false */ function fetchMobileLocation(string $mobile) { $url https://example.com/api/mobile?phone{$mobile}; $ch curl_init(); curl_setopt_array($ch, [ CURLOPT_URL $url, CURLOPT_RETURNTRANSFER true, CURLOPT_TIMEOUT 5, CURLOPT_CONNECTTIMEOUT 3, CURLOPT_USERAGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36, CURLOPT_HTTPHEADER [Accept: application/json], ]); $response curl_exec($ch); $err curl_error($ch); $status curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($err || $status ! 200) { return false; } return json_decode($response, true); }两个超时参数要分开理解CURLOPT_CONNECTTIMEOUT是连接超时CURLOPT_TIMEOUT是整个请求的总超时。实际抓号段时最容易遇到的不是连不上而是源站连接正常但响应特别慢如果只设总超时不设连接超时脚本会一直干等批量任务卡死在某个号段上。我本地调通的经验值是连接超时 3 秒、总超时 5 秒如果抓包时大量出现Timeout was reached优先加大连接超时而不是总超时。这段代码里没有放 cookie 和代理池因为大部分号段查询接口只靠 UA 加频控不需要登录会话。代理池在这个规模的项目里是最后手段——号段查询请求量最多几千次用令牌桶摊开跑大多数源站根本不会触发封禁一旦引入代理反而多出了代理 IP 质量这个变量脏代理返回超时的概率比源站限流更让人头疼。3.2 号段前缀生成不是抓手机号是抓前四位很多新手第一次写号段抓取会拿着完整手机号去查询接口结果源站返回空。实际上批量抓号段的正确姿势是拿前四位当最小单位因为接口只要看到前四位就能返回城市粒度归属地。整个 199 号段从 1990 到 1999 分别去查覆盖面就够了。?php /** * 生成号段前缀列表 * param string $segment 号段前三位例如 199 * param array $regionCodes 地区码白名单例如 [010,021] * return array */ function generatePrefixes(string $segment, array $regionCodes []) { $prefixes []; for ($i 0; $i 9; $i) { $prefixes[] $segment . $i; } if ($regionCodes) { $filtered []; foreach ($prefixes as $p) { foreach ($regionCodes as $code) { $filtered[] $p . $code; } } return $filtered; } return $prefixes; }generatePrefixes(199)会得到 10 个前四位前缀这是默认的最小粒度。如果业务只关注北上广深的号码可以传入$regionCodes白名单请求量能直接降一个量级。但要注意一个反直觉的点有些放号是全国同时进行的锁定地区码反而会漏掉外地号段所以白名单只适合业务方明确只要某些城市的情况。号段前缀总量在国内所有正式号段中大约几千条单机用普通数组循环就够不需要引入 PHP 队列。只有当扫描粒度降到第五位甚至第六位时请求量才会膨胀到几十万级别那种场景才值得用 Redis 或 RabbitMQ 拆任务。多数项目没必要队列装上去只会增加运维负担。3.3 MySQL 表设计与批量 upsert字段类型是性能分水岭抓回来的数据最终要落库。我把表结构固定成下面这套字段不多但每个都踩过坑。CREATE TABLE mobile_location ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, prefix VARCHAR(4) NOT NULL COMMENT 号段前4位, province VARCHAR(32) NOT NULL COMMENT 省份, city VARCHAR(32) NOT NULL COMMENT 城市, operator TINYINT NOT NULL DEFAULT 0 COMMENT 1移动, 2联通, 3电信, 0虚拟运营商, areacode VARCHAR(4) NOT NULL DEFAULT COMMENT 区号, postcode VARCHAR(6) NOT NULL DEFAULT COMMENT 邮编, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_prefix_operator (prefix, operator), KEY idx_city (city) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT手机号段归属地表;prefix字段必须用varchar(4)而不是int。号码和号段带前导零是常态例如 0130 这种旧格式如果用整型前导零会被自动去掉查询时永远匹配不上。uk_prefix_operator唯一索引加在 prefix 和 operator 的组合上允许同一个号段下存在不同运营商或虚拟运营商的多条记录又防止同一来源的重复数据反复插入。批量写入用预处理语句加ON DUPLICATE KEY UPDATE做 upsert 而不是先删后插?php /** * 批量写入号段归属地冲突则更新 * return int 受影响行数 */ function batchInsertLocations(PDO $pdo, array $rows): int { $sql INSERT INTO mobile_location (prefix, province, city, operator, areacode, postcode) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE province VALUES(province), city VALUES(city), operator VALUES(operator), updated_at NOW(); $stmt $pdo-prepare($sql); $count 0; foreach ($rows as $row) { $count $stmt-execute([ $row[prefix], $row[province], $row[city], $row[operator], $row[areacode], $row[postcode], ]); } return $count; }用预处理语句不是形式主义抓取来源站点多谁能保证数据字段里不会被塞一段恶意内容直接拼接 SQL 会把整个归属地服务变成注入入口。upsert 解决的问题是幂等同一批号段连续跑两次不会产生重复数据。第一次全量入库时可以用这个函数一条一条写入数据量在几十万条时性能和可读性都能接受真到了百万级再把多条记录拼成一条INSERT INTO ... VALUES (...), (...)每 1000 条提交一次写入时间能降到原来的五分之一。提示updated_at字段别删。数据更新任务挂掉但没人发现的情况全靠比对updated_at的最近时间来判断。3.4 导出业务可用的号段数据文件带 BOM 的 CSV 和版本命名数据入库只是第一步业务方要的是“一份能直接拿去用的文件”。我每次全量更新后都会导出一份 CSV专供离线查询和环境初始化。?php /** * 导出全量号段数据为 CSV 文件 * return int 导出条数 */ function exportDataFile(PDO $pdo, string $filepath): int { $sql SELECT prefix, province, city, operator FROM mobile_location ORDER BY prefix, operator; $stmt $pdo-query($sql); $fp fopen($filepath, w); // 写入 BOM 头防止 Excel 打开 UTF-8 中文乱码 fwrite($fp, \xEF\xBB\xBF); fputcsv($fp, [prefix, province, city, operator]); $count 0; while ($row $stmt-fetch(PDO::FETCH_ASSOC)) { fputcsv($fp, $row); $count; } fclose($fp); return $count; }fputcsv默认逗号分隔适合大多数导入场景如果业务方要导进 Redis 或者做流式读取可以改成 Tab 分隔第三个参数传\t。BOM 头必须保留删掉之后 Windows 下的 Excel 打开中文全是乱码又得跟业务方解释一遍。文件名的版本命名我固定在代码里按日期走例如mobile_location_20250115.csv同时导出一个校验文件记录总条数和导出时间。业务方拿到的文件如果条数和校验文件对不上第一时间就能发现是传输损坏而不是数据错了。整个导出流程没有用复杂的压缩格式CSV 对绝大多数 PHP 项目是最不折腾的交付方式。4. 号段抓取与更新避坑指南5 个踩过的坑4.1 现象IP 被封请求全部返回 403 → 原因没做频率控制 → 解决令牌桶限速抓第三方查询站时最容易触发的是 IP 级封禁。早年我写过一个抓取循环里面没有任何 sleep结果 5 分钟内 80% 请求返回 403排查源码发现是查询频率给源站造成了压力源站对单个 IP 做了限制。解决方法是令牌桶限速?php class RateLimiter { private int $tokens; private int $capacity; private int $refillPerSecond; private float $lastRefill; public function __construct(int $capacity, int $refillPerSecond) { $this-capacity $capacity; $this-refillPerSecond $refillPerSecond; $this-tokens $capacity; $this-lastRefill microtime(true); } public function acquire(): bool { $now microtime(true); $this-tokens min( $this-capacity, $this-tokens ($now - $this-lastRefill) * $this-refillPerSecond ); $this-lastRefill $now; if ($this-tokens 1) { $this-tokens--; return true; } return false; } }容量和补充速率是直接影响抓取时间总量的参数。抓 199 号段大约 10 个前缀容量设 10、每秒补充 2 个令牌几秒钟就完成了如果扫全量几千个前缀同样参数要跑半小时但源站不会 403。acquire()返回 false 时脚本应等待 1 秒再重试。这里的等待时间就是最有效的频率控制。4.2 现象抓回来的数据跟源站页面不一致 → 原因接口参数被改 → 解决解析层容错加告警第三方站点调整接口是家常便饭最典型的是把phone参数改成mobile或者要求加一个签名参数。脚本会突然大面积返回空数据或报错。解决分两步第一步把 JSON 解析抽成独立类字段名变化只改映射关系核心抓取逻辑不动第二步每次批量任务完成后统计成功率低于 95% 就报警。解析时用isset()判断字段是否真的存在不要直接$data[result]索引拿不到 key 也不致于 fatal error。4.3 现象导出的号段文件里新号段缺失 → 原因只抓了单一来源 → 解决双源交叉验证某次更新后发现 170 号段在源站里只有几条记录而该号段早已放号一年多说明源站自己也没更新它只是拿旧表响应查询。此后我把抓取逻辑改成双源交叉验证两个独立来源的结果做交集合并两个源都没有的新号段用运营商官网公告补。这个过程无法完全自动化我定期跑一次“号段全集差异比对”脚本把两个源的数据 diff 一下新增号段差异表出来后人再确认一次。数据源污染的问题单靠代码是治不完的得有这个比对机制兜底。4.4 现象数据导入线上后查询变慢 → 原因索引没建或者字段类型不对 → 解决前缀索引最常见的慢查询是SELECT * FROM mobile_location WHERE prefix 1990。如果 prefix 字段没索引百万条数据全表扫描一次要上百毫秒。给 prefix 建前缀索引就够了ALTER TABLE mobile_location ADD INDEX idx_prefix (prefix(4));前缀索引只对等值匹配和LIKE prefix%有效不能用于排序和范围查询。另一个细节是业务方查询时经常传完整手机号需要主动写成WHERE prefix LEFT(13812345678, 4)否则 WHERE 里面是完整 11 位手机号索引直接失效。4.5 现象归属地跟用户实际所在地不一致 → 原因携号转网 → 解决明确“归属地开户地”携号转网全面铺开后一个 139 开头的号码可能现在属于联通用户但号段归属地依然是移动。业务上如果拿这个字段判断当前运营商必然出错。解决方式是数据字典和接口文档里明确标注“归属地 开户地不等于当前实际运营商”数据库字段也建议命名成类似province_open这样的语义。判断当前运营商只有实时查询运营商侧接口一条路号段文件不解决这个问题它超出了这个项目的边界。5. 进阶把抓取程序变成自动更新服务并验证数据质量5.1 cron 定时更新一行命令解决“忘了更新”的毛病脚本写完后要部署成服务。我的做法是在服务器上用 cron 驱动5 3 * * * /usr/local/php/bin/php /data/www/mobile_location/update_full.php /data/logs/mobile_update.log 215 3 * * *表示凌晨 3 点 5 分执行避开业务查询高峰。PHP 二进制路径要写实际环境的路径多版本 PHP 混用的情况下尤其要检查否则可能加载不到 curl 等扩展。日志单独存文件更新任务异常时翻这一个日志就能排查不用去系统日志里大海捞针。5.2 更新后的三层数据校验抽样、边界、全量自检更新完不校验等于白更新。我做三层校验第一层随机抽 50 个号段在源站人工比对第二层固定测 134、170、199 三个边界号段这三处是历史问题高发区最容易在这几条翻车第三层是全量自检脚本?php // 自检找出记录数异常的号段和空省份记录 $dup $pdo-query( SELECT prefix, COUNT(*) c FROM mobile_location GROUP BY prefix HAVING c 5 )-fetchAll(); $empty $pdo-query( SELECT COUNT(*) FROM mobile_location WHERE province )-fetchColumn(); if ($dup || $empty 0) { // 写日志并通知维护人 }重复前缀用c 5而不是c 1因为同一号段在不同地区码下有多条记录是正常的只有超过 5 条才大概率是重复抓取。自检脚本顺带统计总条数和上周对比如果一周内突然多了几百个号段优先怀疑数据源是否异常。5.3 给业务方一个归属地查询接口最后是业务方最关心的怎么把号段数据接入现有系统。一个最小可用的 PHP 查询接口?php // query_location.php require_once db.php; header(Content-Type: application/json; charsetutf-8); $phone preg_replace(/[^\d]/, , $_GET[phone] ?? ); if (strlen($phone) ! 11) { http_response_code(400); echo json_encode([error invalid phone]); exit; } $stmt $pdo-prepare( SELECT province, city, operator FROM mobile_location WHERE prefix ? LIMIT 1 ); $stmt-execute([substr($phone, 0, 4)]); $row $stmt-fetch(PDO::FETCH_ASSOC); if (!$row) { http_response_code(404); echo json_encode([error prefix not found]); exit; } echo json_encode([ phone $phone, province $row[province], city $row[city], operator $row[operator], location $row[province] . $row[city], ]);先用preg_replace过滤非数字字符是为了处理业务方传过来带空格、带横杠的号码。substr($phone, 0, 4)取前四位而不是前三位前四位才能识别城市粒度。LIMIT 1保证即使同一个前缀存在多条记录也只返回最新一条避免接口偶发返回数组的诡异 bug。这个接口上线后一定要记录查询日志日志至少包含时间、号码前缀、返回结果业务反馈某个号段查不到时能直接定位是数据缺失还是接口问题。最后说一个我的教训每次更新号段数据文件都保留上一个版本不要直接覆盖。某次新号段入网后全量导入发现数据反而少了想回滚却发现旧版本已经被覆盖只能重新抓全量白白浪费一个下午。从那以后所有文件一律按日期存档不图省事。这个习惯希望帮到你别等踩了坑再养成。本文还有配套的精品资源点击获取
返回列表