资讯动态

全国手机号码段归属地数据库:离线查询与风控应用实战

发布时间:2026/10/18 7:44:32 来源:尧图企业网站定制
简介这是一份面向开发者、数据分析人员及反欺诈风控从业者的全国手机号码段归属地数据资源用于快速查询号码对应的运营商、省份、城市、区号与邮编等信息可支撑地域分析、客户画像与本地化服务等场景。压缩包共4个文件以2个txt、1个sql和1个csv为主SQL脚本便于建表与批量导入关系型数据库CSV与文本文件则方便直接读取或二次清洗整体约7.48MB涵盖360569条记录数据量较为完整。目前已有5056人学习下载说明其在号码归属地查询类需求中具备一定参考价值。使用者可借助这些数据完成号码段匹配、运营商分布统计与地域维度聚合也能将其作为基础字典表嵌入现有系统减少自行采集与整理的成本适合需要快速落地号码归属地查询功能或开展相关数据分析的读者参考使用。1. 全国手机号码段归属地数据库36万条记录到底能拿来干什么手里有一份 360569 条记录的全国手机号码段归属地数据库Excel 格式第一反应往往是“这不就是个号段对照表吗”。真把它丢进业务里你会发现它能干的事比想象中多注册风控要判断手机号归属地是否与常用登录地一致电商下单要按收货区域做路由客服系统要按号码归属地分配对应语系的坐席数据清洗时要批量补全用户表里缺失的城市字段。这些场景的共同点是——你不需要调用任何在线接口本地一张表就能完成毫秒级查询。适合谁用做后端风控的、做数据仓库 ETL 的、做本地化 CRM 的以及任何被“手机号归属地查询接口限流”折磨过的开发者。这份数据的核心价值在于离线、可批量、无调用成本代价是你得自己处理号段前缀匹配和运营商字段的脏数据。2. 号段归属地数据的结构拆解与前缀匹配原理2.1 一条记录里到底存了什么拿到 Excel 后先别急着导入用 pandas 把表头和前几行打出来看。常见的字段包括号段前缀通常是 7 位如 138xxxx、省份、城市、运营商移动/联通/电信/虚拟运营商、区号、邮编。360569 条这个量级说明它覆盖了从早期 13x 到较新 19x、16x 的绝大多数已分配号段甚至包含部分虚拟运营商号段。字段命名各家不同有的叫prefix有的叫号段有的把省市合并成一列。先统一列名后面所有代码都基于统一 schema 写否则每换一份数据就要改一次逻辑。import pandas as pd # 读取 Excel注意 engine 选择大文件用 openpyxl 更稳 df pd.read_excel(phone_prefix.xlsx, engineopenpyxl, dtypestr) # 打印列名和前 5 行确认字段结构 print(df.columns.tolist()) print(df.head()) # 统一列名映射按实际列名调整 rename_map { 号段: prefix, 省份: province, 城市: city, 运营商: carrier, 区号: area_code, 邮编: zip_code } df df.rename(columnsrename_map) # 去重同一号段可能因数据合并出现重复 df df.drop_duplicates(subset[prefix]) print(f去重后记录数: {len(df)})这段代码做了三件事读取时强制dtypestr防止号段被 pandas 当成数字丢掉前导零或变成科学计数法打印列名和样本让你确认字段是否齐全重命名并去重。参数上engineopenpyxl对 xlsx 支持最好如果文件是 xls 则换xlrd。去重那一步很关键我见过同一号段出现三条记录、运营商字段还不一致的情况不去重会导致后续查询结果随机。2.2 为什么用前缀匹配而不是等值查询手机号是 11 位号段表存的是前 7 位。用户输入 13812345678你要拿前 7 位1381234去表里查。这里有个容易翻车的点不同号段的长度可能不一致早期数据有 6 位前缀新数据有 8 位前缀。稳妥做法是统一截取前 7 位如果查不到再尝试前 6 位或前 8 位。更工程化的做法是把号段表按前缀长度分层查询时从长到短匹配。下面是一个可复用的查询函数def lookup_phone(phone: str, prefix_df: pd.DataFrame) - dict: 输入 11 位手机号返回归属地信息 匹配策略优先 7 位前缀失败则尝试 6 位、8 位 phone str(phone).strip() if len(phone) ! 11 or not phone.isdigit(): return {error: invalid_phone} for length in (7, 8, 6): prefix phone[:length] row prefix_df[prefix_df[prefix] prefix] if not row.empty: r row.iloc[0] return { province: r.get(province, ), city: r.get(city, ), carrier: r.get(carrier, ), area_code: r.get(area_code, ), matched_prefix: prefix } return {error: not_found} # 测试 print(lookup_phone(13812345678, df))逻辑说明先做输入校验非 11 位纯数字直接返回错误避免脏数据进入匹配流程。匹配顺序是 7→8→6因为 7 位是主流8 位用于部分新号段6 位是兜底。每次匹配用prefix_df[prefix] prefix做布尔索引数据量 36 万时单次查询在毫秒级但如果要批量查几十万条这种写法会慢。批量场景见下一章。参数上matched_prefix字段返回实际命中的前缀长度方便排查“为什么这个号查出来是空”。如果业务对性能敏感可以把 DataFrame 的prefix列设为索引查询时用.loc速度提升明显。2.3 运营商字段的清洗与归一原始数据里运营商字段可能是“中国移动”“移动”“CMCC”“中国移动通信”等多种写法。不做归一后续按运营商分组统计就会得到一堆碎片化结果。常见做法是建一个映射字典把变体统一成三个标准值移动、联通、电信虚拟运营商单独归为“虚拟”。carrier_map { 中国移动: 移动, 移动: 移动, CMCC: 移动, 中国移动通信: 移动, 中国联通: 联通, 联通: 联通, CUCC: 联通, 中国电信: 电信, 电信: 电信, CTCC: 电信, 虚拟运营商: 虚拟, 虚拟: 虚拟 } df[carrier_std] df[carrier].map(carrier_map).fillna(其他) print(df[carrier_std].value_counts())map加fillna是标准操作没命中映射的归为“其他”然后打印分布。如果“其他”占比超过 5%说明映射字典不全需要回到原始数据看还有哪些写法。这一步做完数据才算真正可用。3. 把 Excel 变成可查询服务从导入到接口的完整链路3.1 导入 SQLite 并建索引Excel 适合看不适合查。36 万条记录用 pandas 全量加载到内存大约占几十 MB单机跑没问题但多进程或容器环境下内存会吃紧。更稳的做法是导入 SQLite建前缀索引查询走 B-Tree内存占用低且支持并发读。import sqlite3 conn sqlite3.connect(phone_location.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS phone_prefix ( prefix TEXT PRIMARY KEY, province TEXT, city TEXT, carrier TEXT, area_code TEXT, zip_code TEXT ) ) # 批量插入用 executemany 比逐条快一个数量级 records df[[prefix, province, city, carrier_std, area_code, zip_code]].values.tolist() cursor.executemany( INSERT OR REPLACE INTO phone_prefix VALUES (?, ?, ?, ?, ?, ?), records ) conn.commit() # 前缀本身是主键已有索引如果按省份查得多补一个省份索引 cursor.execute(CREATE INDEX IF NOT EXISTS idx_province ON phone_prefix(province)) conn.commit() print(f已导入 {cursor.rowcount} 条)INSERT OR REPLACE保证重复导入不会报错适合数据更新场景。executemany是批量插入的关键逐条execute在 36 万条时可能要几分钟批量后几秒完成。索引方面prefix作为主键自带索引等值查询走索引如果业务经常按省份筛选补idx_province。注意索引不是越多越好每个索引都会增加写入开销这份数据基本只读多建一两个无妨。3.2 用 Python 封装查询接口数据库有了接下来封装一个查询函数供脚本或 Web 服务调用。核心是参数化查询防止 SQL 注入同时利用索引。def query_by_phone(phone: str) - dict: phone str(phone).strip() if len(phone) ! 11 or not phone.isdigit(): return {error: invalid_phone} conn sqlite3.connect(phone_location.db) cursor conn.cursor() for length in (7, 8, 6): prefix phone[:length] cursor.execute( SELECT province, city, carrier, area_code FROM phone_prefix WHERE prefix ?, (prefix,) ) row cursor.fetchone() if row: conn.close() return { province: row[0], city: row[1], carrier: row[2], area_code: row[3], matched_prefix: prefix } conn.close() return {error: not_found}参数化查询用?占位不要把 prefix 拼进 SQL 字符串。每次查询新建连接在低频场景没问题高频场景应该用连接池或长连接。如果做成 HTTP 接口Flask 或 FastAPI 包一层即可注意加缓存——同一号码重复查询很常见用functools.lru_cache或 Redis 都能显著降低数据库压力。3.3 批量查询的性能优化单条查询毫秒级但如果你要清洗一张 50 万行的用户表逐条查就是 50 万次数据库往返可能要十几分钟。优化思路是批量取前缀、一次性查回、内存匹配。def batch_lookup(phones: list) - pd.DataFrame: # 提取所有前缀去重后一次性查询 prefixes set() for p in phones: p str(p).strip() if len(p) 11 and p.isdigit(): prefixes.add(p[:7]) conn sqlite3.connect(phone_location.db) placeholders ,.join([?] * len(prefixes)) query fSELECT prefix, province, city, carrier FROM phone_prefix WHERE prefix IN ({placeholders}) result pd.read_sql_query(query, conn, paramslist(prefixes)) conn.close() # 构建前缀到归属地的映射内存匹配 prefix_map result.set_index(prefix).to_dict(index) rows [] for p in phones: p str(p).strip() info prefix_map.get(p[:7], {}) rows.append({ phone: p, province: info.get(province, ), city: info.get(city, ), carrier: info.get(carrier, ) }) return pd.DataFrame(rows)这里的关键是IN查询把 N 次往返压成 1 次然后用字典做内存匹配。注意IN的参数数量有上限SQLite 默认 999如果去重后前缀超过 999 个要分批查询。实际业务中 50 万手机号去重后前缀通常只有几万个分批处理即可。这个方案比逐条查快 10 倍以上是我在数据清洗场景里的默认做法。4. 避坑与排查号段数据落地时最容易翻车的 5 个点4.1 号段查不到但号码明明是合法的现象用户反馈某个 19x 开头的号码查不到归属地但号码本身能打通。原因号段表更新滞后新分配的号段没收录。解决在查询逻辑里加兜底——7 位查不到查 6 位再查不到返回“未知”而不是报错同时记录未命中前缀定期汇总后手动补录。不要因为查不到就抛异常线上环境一个异常可能阻断整个下单流程。4.2 Excel 读取后号段变成科学计数法现象1381234读进来变成1381234.0或1.381234e06。原因pandas 默认按数值类型推断列。解决读取时加dtypestr并且对已经变成浮点的列做astype(str).str.replace(.0, , regexFalse)修复。更彻底的做法是导入前在 Excel 里把号段列设为文本格式但从工程角度代码里强制字符串类型更可靠。4.3 同一号段对应多个城市现象查询1381234返回了三个不同城市。原因数据合并时不同来源的记录冲突或者该号段确实跨区域分配。解决去重时保留第一条或者按运营商优先级排序后取第一条。如果业务对准确性要求极高可以标记为“多归属”由上层业务决定。我一般会在导入阶段就做去重避免把问题带到查询层。4.4 批量查询时 SQL 参数超限现象batch_lookup处理大列表时报too many SQL variables。原因SQLite 对IN子句参数数量有限制。解决把前缀集合按 500 个一组分批查询结果合并。代码里加一个chunk函数即可不要试图调高 SQLite 限制分批更可控。4.5 运营商字段大小写和空格导致映射失败现象映射后“其他”占比很高。原因原始数据里“中国移动 ”带尾随空格或者“cmcc”小写。解决映射前先做str.strip().str.upper()归一化再查映射字典。这个坑很隐蔽因为肉眼看数据没问题但字符串比较是精确匹配。养成清洗前先strip的习惯能省很多排查时间。5. 进阶用法把号段数据变成风控特征与区域路由依据号段数据本身只是静态对照表但把它和业务数据 join 之后能衍生出不少有价值的特征。比如在注册风控里计算“手机号归属地”与“IP 归属地”是否一致不一致的注册请求风险分上调在电商场景里按手机号归属地预判收货区域提前分配最近的仓库在客服系统里根据归属地路由到对应语系的坐席减少转接次数。这些用法的前提是你得先把号段数据做成一个稳定的查询服务而不是每次现读 Excel。一个具体的进阶技巧是构建“号段-省份”聚合表用于快速统计。比如你想知道某次营销活动覆盖了多少个省份的用户不需要逐条查直接按前缀前 3 位聚合即可。# 构建省份维度的聚合视图 province_stats df.groupby(province).agg( prefix_count(prefix, count), carriers(carrier_std, lambda x: ,.join(sorted(set(x)))) ).reset_index() print(province_stats.sort_values(prefix_count, ascendingFalse).head(10))这个聚合表可以物化成一张单独的 SQLite 表或缓存到 Redis查询省份分布时直接读聚合结果避免全表扫描。carriers字段用lambda聚合出该省份涉及的运营商列表方便快速判断某省是否只有单一运营商覆盖。另一个实用技巧是校验数据新鲜度。号段数据会随运营商分配而变化建议在表里加一列updated_at每次导入时打时间戳。查询服务启动时检查最新时间戳如果超过半年就告警。我自己的习惯是每季度重新导入一次并在导入脚本里加一行print(f数据日期: {df[updated_at].max()})让每次运行都能看到数据新旧。这个习惯帮我避免过好几次“用两年前的数据做风控”的尴尬。最后说一个验证方法随机抽 100 个号码用三大运营商官方渠道查归属地和本地库对比命中率低于 95% 就说明数据需要更新或清洗逻辑有问题。这个抽检脚本很简单但能给你一个量化的信心。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑