手机归属地查询:用 Python 给 Excel 号码表批量补归属地

约 14 分钟 进阶手机号码归属地查询批量查询Python 脚本Excel 处理
# 手机归属地查询:用 Python 给 Excel 号码表批量补归属地 接口:手机归属地查询(apiCode=6,接入点 6-1)|是否免费:是,注册后默认可调用,设使用档次限制|请求方式:POST / GET|返回格式:JSON|适用人群:需要给号码清单补地域标签的数据与运营同学|阅读时间:约 9 分钟|最后实测核对:2026-09-18 拿到一张号码表要补归属地,手工一条条查不现实,直接写个并发循环打接口又会撞上并发上限。先把两件事确认清楚:手机归属地查询接口没有官方批量能力,接入点 6-1 是单条同步请求;但归属地结果只跟前 7 位号段有关,先去重再查,请求数会明显下降。 ## 核心要点 - 接入点 6-1 是单条同步请求-响应,接口没有批量入口,批量靠客户端循环实现。 - 号码表先取前 7 位去重,实测后 4 位不影响归属地结果,几万个号码通常只剩几百个不同号段。 - 并发上限由档位决定,L0 档 1 QPS,写循环时不要盲目开多线程。 ## 两步预处理:去重和排序 号码表里同一个号段的号码往往很多,一个公司内部的号码通常集中在少数几个号段。实测确认 `13800130000` 和 `13800139999` 的返回完全一致,`13800240000` 才落到另一个城市,说明匹配键是前 7 位。 ```python import re from collections import Counter def to_segment(raw) -> str | None: """清理并取前 7 位号段,不合规返回 None。""" if raw is None: return None cleaned = re.sub(r"[-\s]", "", str(raw).strip()) if not re.fullmatch(r"\d{7,}", cleaned): return None if cleaned[0] == "0": return None return cleaned[:7] raw_numbers = [row[0] for row in rows] # 从表里读出来的原始号码列 segments = [s for s in (to_segment(n) for n in raw_numbers) if s] unique_segments = sorted(set(segments)) print(f"号码 {len(raw_numbers)} 个,其中格式不合规 {len(raw_numbers) - len(segments)} 个") print(f"去重后需要查询的号段:{len(unique_segments)} 个") ``` 先把这两个数字打出来再决定要不要跑。多数情况下这一步就能把请求数压下来一个数量级。 ## 逐条查询并写回 ```python import csv import time import requests API_URL = "https://route.showapi.com/6-1" APP_KEY = "YOUR_APPKEY" # 正式环境从配置或密钥管理服务读取 SEGMENT_FIELDS = ("prov", "city", "name", "type", "num", "areaCode", "postCode", "provCode", "cityCode") def locate(seg: str, retries: int = 3) -> dict: """查询单个号段,带指数退避重试。""" for i in range(retries): try: resp = requests.get( API_URL, params={"appKey": APP_KEY, "num": seg}, timeout=10, # 对齐官方 x-read-timeout ) data = resp.json() if data.get("showapi_res_code") != 0: raise RuntimeError(data.get("showapi_res_error")) body = data.get("showapi_res_body", {}) if body.get("ret_code") != 0: # 失败不扣点数,号段问题直接记录,不再重试 return {"ok": False, "ret_code": body.get("ret_code"), "remark": body.get("remark")} return {"ok": True, **{k: body.get(k) for k in SEGMENT_FIELDS}} except requests.RequestException as exc: if i == retries - 1: return {"ok": False, "ret_code": None, "remark": f"网络异常:{exc}"} time.sleep(2 ** i) # 1s / 2s / 4s 退避 return {"ok": False, "ret_code": None, "remark": "未知错误"} # 断点续跑:结果落盘,中断后重新执行会跳过已查过的号段 segment_map = {} try: with open("segment_result.jsonl", encoding="utf-8") as fh: for line in fh: rec = __import__("json").loads(line) segment_map[rec["seg"]] = rec except FileNotFoundError: pass with open("segment_result.jsonl", "a", encoding="utf-8") as fh: for idx, seg in enumerate(unique_segments, 1): if seg in segment_map: continue result = locate(seg) rec = {"seg": seg, **result} fh.write(__import__("json").dumps(rec, ensure_ascii=False) + "\n") fh.flush() # 立即落盘,中断不丢进度 segment_map[seg] = rec print(f"[{idx}/{len(unique_segments)}] {seg} -> {result.get('prov', '')}{result.get('city', '')}") time.sleep(1.0) # L0 档 1 QPS,逐条之间留 1 秒 ``` 写回表格: ```python import csv with open("numbers_in.csv", encoding="utf-8-sig", newline="") as fin, \ open("numbers_out.csv", "w", encoding="utf-8-sig", newline="") as fout: reader = csv.DictReader(fin) writer = csv.DictWriter(fout, fieldnames=reader.fieldnames + [ "prov", "city", "carrier", "carrier_type", "area_code", "post_code"]) writer.writeheader() for row in reader: seg = to_segment(row["phone"]) rec = segment_map.get(seg) or {} row.update({ "prov": rec.get("prov", ""), "city": rec.get("city", ""), "carrier": rec.get("name", ""), "carrier_type": rec.get("type", ""), "area_code": rec.get("areaCode", ""), "post_code": rec.get("postCode", ""), }) writer.writerow(row) ``` Excel 文件用 `openpyxl` 或 `pandas.read_excel` 读进来、`to_excel` 写回去,逻辑与上面完全一致,只是换掉读写层。 ## 并发节奏怎么定 免费档位的并发上限按等级划分,取自[免费接口与档位说明](https://www.showapi.com/free-api): | 等级 | 每日调用量 | 并发限制 | 所需积分 | |------|-----------|----------|----------| | L0 基础版 | 100 次/天 | 1 QPS | 免费 | | L1 进阶版 | 500 次/天 | 5 QPS | 5,000 积分/年 | | L2 高级版 | 2,000 次/天 | 10 QPS | 15,000 积分/年 | | L3 专业版 | 10,000 次/天 | 30 QPS | 50,000 积分/年 | | L4 旗舰版 | 50,000 次/天 | 50 QPS | 100,000 积分/年 | L0 档并发是 1 QPS,脚本里逐条之间留 1 秒就是合适的节奏,开多线程只会拿到限流错误。档位提高后可以把 `time.sleep` 换成令牌桶,桶容量设为当前档位的 QPS 值。 每日调用量也要对一下。去重后号段数如果超过当日上限,把任务拆成多天跑,脚本的断点续跑机制能直接支撑这种做法。 ## 号码列的常见清洗项 表里的号码列经常不干净,读进来之后先清一遍再取号段。 | 原始形态 | 处理 | 原因 | |----------|------|------| | `138 0013 8000` | 去掉空格 | 实测接口会清理空格,本地也一并清掉更稳 | | `138-0013-8000` | 去掉连字符 | 实测该形态能正常查询 | | `+8613800138000` | 剥离国际区号后再取前 7 位 | 带 `+` 的输入接口返回 `-2` | | `13800138000` | 全角转半角 | 全角数字接口返回 `-4` | | 空单元格 | 跳过并计数 | 避免拼出空请求 | | 首位为 0 | 跳过 | 不存在首位为 0 的号段 | 这些形态对应的错误码细节在[错误码逐条对照](https://www.showapi.com/guides/phone-attribution-error-codes-6)。 ## FAQ **Q1:接口有批量查询入口吗?** 没有。接入点 6-1 是单条同步请求-响应,没有订阅推送、没有回调。所谓批量是客户端循环调用实现的。 **Q2:几万个号码要调多少次接口?** 取决于不同号段的数量,不是号码总数。先取前 7 位去重,实测后 4 位不影响结果,所以同一个号段只需要查一次。 **Q3:能开多线程加速吗?** 取决于档位。L0 档并发限制是 1 QPS,多线程会触发限流。档位升到 L1 以上(5 QPS 起)再考虑并发,并且把并发数控制在档位 QPS 之内。 **Q4:脚本跑到一半中断了怎么办?** 结果按行落盘到 JSONL 文件,重新执行时会先读回已完成记录并跳过。这个设计不影响已消耗的额度,因为跳过的号段不会再发请求。 **Q5:查不到归属地的号码怎么处理?** `ret_code` 为 `-1`、`-5`、`-6` 的号段查不到归属地,失败不扣点数。建议单独输出一份清单,标记待人工核验,不要混在成功结果里。 **Q6:Excel 里写回时区号和邮编为什么会变形?** 区号 `"010"` 和邮编 `"100000"` 带前导零,写进 Excel 时会被当成数字。输出列的单元格格式设为文本,或者写成 CSV 再用 Excel 打开,前导零能保住。 ## 下一步阅读 - [手机归属地查询返回 -2、-3、-4 是什么意思?错误码逐条对照与排查](https://www.showapi.com/guides/phone-attribution-error-codes-6) —— 号码列清洗的依据 - [手机归属地查询按前 7 位号段做缓存:实测粒度与 Redis 实现](https://www.showapi.com/guides/phone-attribution-cache-6) —— 把去重结果沉淀成缓存 - [手机归属地查询返回字段全解:11 个字段的类型、取值与粒度](https://www.showapi.com/guides/phone-attribution-response-fields-6) —— 决定写回哪几列 - **本系列共 12 篇**:查看[手机归属地查询指南总目录](https://www.showapi.com/guides/phone-attribution-guides-6)
加载中...