简介:这是一份面向开发者、数据分析人员及反欺诈风控从业者的全国手机号码段归属地数据资源,用于快速查询号码对应的运营商、省份、城市、区号与邮编等信息,可支撑地域分析、客户画像与本地化服务等场景。压缩包共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", engine="openpyxl", dtype=str) # 打印列名和前 5 行,确认字段结构 print(df.columns.tolist()) print(df.head()) # 统一列名映射,按实际列名调整 rename_map = { "号段": "prefix", "省份": "province", "城市": "city", "运营商": "carrier", "区号": "area_code", "邮编": "zip_code" } df = df.rename(columns=rename_map) # 去重:同一号段可能因数据合并出现重复 df = df.drop_duplicates(subset=["prefix"]) print(f"去重后记录数: {len(df)}")这段代码做了三件事:读取时强制dtype=str,防止号段被 pandas 当成数字丢掉前导零或变成科学计数法;打印列名和样本,让你确认字段是否齐全;重命名并去重。参数上,engine="openpyxl"对 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 = f"SELECT prefix, province, city, carrier FROM phone_prefix WHERE prefix IN ({placeholders})" result = pd.read_sql_query(query, conn, params=list(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.381234e+06。原因:pandas 默认按数值类型推断列。解决:读取时加dtype=str,并且对已经变成浮点的列做astype(str).str.replace(".0", "", regex=False)修复。更彻底的做法是导入前在 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", ascending=False).head(10))这个聚合表可以物化成一张单独的 SQLite 表或缓存到 Redis,查询省份分布时直接读聚合结果,避免全表扫描。carriers字段用lambda聚合出该省份涉及的运营商列表,方便快速判断某省是否只有单一运营商覆盖。
另一个实用技巧是校验数据新鲜度。号段数据会随运营商分配而变化,建议在表里加一列updated_at,每次导入时打时间戳。查询服务启动时检查最新时间戳,如果超过半年就告警。我自己的习惯是每季度重新导入一次,并在导入脚本里加一行print(f"数据日期: {df['updated_at'].max()}"),让每次运行都能看到数据新旧。这个习惯帮我避免过好几次“用两年前的数据做风控”的尴尬。
最后说一个验证方法:随机抽 100 个号码,用三大运营商官方渠道查归属地,和本地库对比,命中率低于 95% 就说明数据需要更新或清洗逻辑有问题。这个抽检脚本很简单,但能给你一个量化的信心。希望帮到你。
本文还有配套的精品资源,点击获取