Appearance
ip2region、qqzeng-ip、GeoIP2 和在线接口怎么选?离线 IP 库实测对比(2026)
搜「IP 归属地」相关的库,几乎每个 README 都写着同样的三句话:准确率 99.9%、查询 0.0x 毫秒、体积只有几 MB。
这些数字不算假,但每一句都带着前提。这篇文章把四类数据源(ip2region、qqzeng-ip、GeoIP2/GeoLite2、在线接口)的机制讲清楚,附上今天刚跑的一份实测:文件多大、加载多久、单次查询多快,以及同一批 IP 在两个数据源上给出的结论到底差在哪。
一、先搞清楚「准确率 99.9%」在说什么
这个词有三种可能的含义,混在一起就变成了营销话术:
- 能返回结果的 IP 占比。也就是「不是空的」。库越大越全,这个数字越高,但不代表答案对。
- 中国区地级市粒度的正确率。这是国内离线库最常引用的口径,通常基于自家或第三方抽样。
- 全球所有 IP 的正确率。如果拿这个口径去衡量,任何库的数字都会掉一大截——海外 IP 的广播和回收比国内自由得多,IP 段换城市是常态。
比宣传语更值得看的,是下面这四个问题:
- 谁在维护、多久更新一次?离线库最大风险不是「不准」,而是「不更新了」。
- IPv6 覆盖如何?IPv6 地址池还在快速扩张,几年前的 v6 库基本等于没有。
- 海外 IP 的质量?做跨境业务的话,国内库的海外字段经常只能给到国家。
- 区县、运营商这两个字段能不能信?字段存在和字段准确是两件事。
二、四类数据源,机制上的差别
| 方案 | 数据形态 | 典型体积 | 查询耗时 | 更新方式 | 运维成本 |
|---|---|---|---|---|---|
| ip2region(lionsoul2014) | xdb 二进制文件,B 树/二分查找,可选向量索引;v3 起拆成 v4/v6 两个文件 | v4 约 11MB,v6 约 36MB | 微秒级 | 社区维护,更新不定期 | 自己下载、自己替换、自己写好热更新 |
| qqzeng-ip | 商业增强的 xdb,IPv4+IPv6 双栈 | 与 ip2region 同量级 | 微秒级 | 官方称每月更新 | 商业授权,免费版有使用限制 |
| GeoIP2 / GeoLite2(MaxMind) | mmdb 文件,行业事实标准 | City 库几十 MB 到上百 MB | 微秒级 | GeoLite2 每周更新 | 需注册账号/授权,国内区县级数据一般 |
| 在线接口(如 IP9) | HTTP JSON | 无 | 百毫秒级(含网络) | 服务端自己迭代,你不用管 | 零运维,按请求量受额度约束 |
一句话总结:**离线库拿磁盘和运维成本换延迟,在线接口拿延迟和额度换运维成本。**两者不是替代关系,不同量级的业务适合不同的那一个。
三、实测数据(2026-09-17)
用官方 Python 绑定 py-ip2region 加载 ip2region_v4.xdb / ip2region_v6.xdb,在普通云服务器上跑的结果:
| 指标 | 结果 |
|---|---|
| v4 xdb 文件大小 | 11MB |
| v6 xdb 文件大小 | 36MB |
| 只加载向量索引(VectorIndex) | 0.4ms |
| 整个文件读进内存 | 5.2ms |
| 单次查询(12000 次采样) | 中位数 6.0µs,P95 14.2µs |
| 单次查询的磁盘 IO 次数 | 命中向量索引后 2~7 次 |
同一个进程里再调一次在线接口对比:单个 IP 含网络往返约 250ms(响应体的 qt 字段是 0.001,服务端处理只用了 1 毫秒,剩下全是网络)。
微秒 vs 百毫秒,差了四个数量级。 这就是离线库唯一无法被替代的优势:当你需要在几分钟内处理几千万条历史日志,或者服务本身跑在没有外网的内网环境里,在线接口不是慢一点的问题,是根本用不了。
同一批 IP,两个数据源的结论对照
| IP | 离线库(ip2region v4/v6) | 在线接口(IP9) |
|---|---|---|
| 114.114.114.114 | 中国|江苏省|南京市|0|CN | 中国 / 江苏 / 南京,isp=114DNS |
| 223.5.5.5 | 中国|浙江省|杭州市|阿里|CN | 中国 / 浙江 / 杭州,isp=AliDNS/DoH/DoT/阿里云 |
| 39.156.66.10 | 中国|北京市|北京市|移动|CN | 中国 / 北京 / 北京,isp=中国移动 |
| 185.199.108.153 | United States|California|San Francisco|0|US | 美国 / 加利福尼亚州 /(城市为空),isp=GitHub |
| 8.8.8.8 | United States|California|0|Google LLC|US | 美国 /(省市均为空),经纬度 -97.82,37.75 |
| 1.2.4.8 | 中国|北京市|北京市|0|CN | 中国 /(省市均为空),isp=SDNS/CNNIC |
| 240e:ff:e02c:1:0:ff:b0e4:20f | 中国|北京市|北京市|电信|CN | 中国 / 广东 / 广州,isp=中国电信 |
| 2001:4860:4860::8888 | United States|Florida|Miami|Google LLC|US | — |
四行一致、两行「离线有答案、在线留空」、一行 IPv6 直接打架。这里面有几条能直接改变你做技术决策的结论:
结论一:留空不等于错了。 1.2.4.8 这类地址(CNNIC 的 DNS 服务、大型 anycast 段)本来就没有一个「唯一归属地」——它在全国甚至全球多点广播。离线库必须给一个答案,于是它按地址注册信息填了「北京市」;在线接口的数据集更保守,省市字段直接留空。**业务上要接受「有些 IP 就是没有可靠归属地」这件事。**如果你的风控规则把「拿不到省份」等同于「可疑」,这批 IP 会天天误伤。
结论二:海外 IP 的经纬度是国家中心点。 8.8.8.8 查出来是「美国」,经纬度 -97.82, 37.75——这是美国的地理中心,不是 Google 的数据中心。**拿着这种坐标往地图上打点,会在地图中间冒出一堆毫无意义的点。**做地理可视化时,先按 country 拆分,海外数据要么只做国家级统计,要么单独标注精度。
结论三:IPv6 是数据源打架最厉害的地方。 同一个电信 IPv6 地址,离线库说北京,在线接口说广东广州。IPv6 地址段近两年在大量重新分配,谁的库更新得慢,谁就更容易说错。做大范围 IPv6 业务之前,最好拿自己的真实日志抽样对比一下两个数据源,别直接信任何一个的宣传页。
结论四:一些小细节能省很多调试时间。 比如用 Python 的 urllib 默认 UA 请求在线接口,会被前置防护直接拦成 HTTP 403(换成普通 UA 立刻正常);再比如带上端口的 IP 字符串(8.8.8.8:80)会被判非法返回 400 而不是猜一个结果——这些都不是 bug,是设计,但不知道就会以为「接口挂了」。
四、Python:一套代码同时跑两个数据源
下面的脚本把「离线库查询」和「在线接口查询」封装成同样的返回结构,跑同一批 IP,输出一份带差异判定的 CSV。这是上面那张对照表的生成方式,可以直接拿去跑你自己的 IP 样本。
python
#!/usr/bin/env python3
"""离线 IP 库 vs 在线接口:同一批 IP 跑两个数据源,输出差异 CSV
准备:
1) pip install py-ip2region
2) 下载 xdb 数据文件放到 ./data 下(ip2region 仓库的 data 目录或 Release 包)
data/ip2region_v4.xdb data/ip2region_v6.xdb
3) 待测 IP 写进 ips.txt,一行一个
"""
import csv
import json
import os
import sys
import time
import urllib.parse
import urllib.request
API = "https://ip9.com.cn/get?ip="
UA = {"User-Agent": "ip-compare/1.0"} # 别用默认 UA,容易被前置防护拦成 403
DATA_DIR = "data"
try:
import ip2region.util as util
import ip2region.searcher as xdb
HAS_LIB = True
except ImportError:
HAS_LIB = False
def norm_prov(name: str) -> str:
"""归一化省名,方便比对:江苏省 / 江苏 / 江苏 都算同一个"""
if not name:
return ""
for suf in ("省", "市", "自治区", "维吾尔", "回族", "壮族"):
name = name.replace(suf, "")
return name.strip()
class OfflineDB:
"""离线库封装:v4/v6 各一份,向量索引只加载一次"""
def __init__(self, data_dir: str = DATA_DIR):
self.ready = False
if not HAS_LIB:
print("[离线] 未安装 py-ip2region,跳过离线对比:pip install py-ip2region")
return
try:
v4 = os.path.join(data_dir, "ip2region_v4.xdb")
idx4 = util.load_vector_index_from_file(v4) # 只读向量索引,快
self.db4 = xdb.new_with_vector_index(util.IPv4, v4, idx4)
v6 = os.path.join(data_dir, "ip2region_v6.xdb")
idx6 = util.load_vector_index_from_file(v6)
self.db6 = xdb.new_with_vector_index(util.IPv6, v6, idx6)
self.ready = True
except Exception as e: # 文件缺失、版本不匹配
print(f"[离线] 初始化失败,跳过离线对比:{e}")
def lookup(self, ip: str) -> str:
if not self.ready:
return ""
db = self.db6 if ":" in ip else self.db4
try:
# 返回形如:中国|江苏省|南京市|0|CN(国家|省|市|运营商|国家代码)
return db.search(ip) or ""
except Exception:
return "" # v4 库里查 IPv6 会直接报错
def online_lookup(ip: str, timeout: float = 3.0) -> dict:
"""在线接口查询;失败返回空 dict,绝不抛异常打断批处理"""
url = API + urllib.parse.quote(ip)
req = urllib.request.Request(url, headers=UA)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = json.loads(resp.read().decode("utf-8"))
except Exception as e:
return {"error": str(e)}
data = body.get("data") or {}
return {
"prov": data.get("prov") or "",
"city": data.get("city") or "",
"isp": data.get("isp") or "",
"country": data.get("country") or "",
}
def verdict(offline: str, online: dict) -> str:
off_prov = ""
if offline:
parts = offline.split("|")
off_prov = norm_prov(parts[1]) if len(parts) > 1 else ""
on_prov = norm_prov(online.get("prov", ""))
if not online and offline:
return "online_fail" # 在线接口异常
if not offline and not on_prov:
return "both_empty"
if off_prov and not on_prov:
return "online_conservative" # 离线有答案,在线留空:多为 anycast / 海外 IP
if off_prov and on_prov and off_prov == on_prov:
return "agree"
return "differ" # 需要人工看的那批
def main(ip_file="ips.txt", out="compare.csv"):
ips = [l.strip() for l in open(ip_file, encoding="utf-8") if l.strip()]
db = OfflineDB()
stats, rows = {}, []
for i, ip in enumerate(ips, 1):
offline = db.lookup(ip)
online = online_lookup(ip)
v = verdict(offline, online)
stats[v] = stats.get(v, 0) + 1
rows.append({
"ip": ip,
"offline": offline,
"online_country": online.get("country", ""),
"online_prov": online.get("prov", ""),
"online_city": online.get("city", ""),
"online_isp": online.get("isp", ""),
"verdict": v,
})
print(f"[{i}/{len(ips)}] {ip:32s} {v}")
# 在线接口免费版是 60 次/分钟的量级,批量跑必须限速
time.sleep(1.1)
with open(out, "w", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
w.writeheader()
w.writerows(rows)
print("\n差异统计:", json.dumps(stats, ensure_ascii=False))
print(f"明细已写入 {out}(用 Excel 打开按 verdict 排序,differ 那批就是你要人工看的)")
if __name__ == "__main__":
main(*(sys.argv[1:] or []))两个细节值得说明:离线库的 search() 在 IP 不在库里时返回空字符串,在美国库查 IPv6 会直接抛异常——批处理里必须把这两种情况都吃掉,否则一个坏 IP 就能让整个任务中断;在线接口那侧把 sleep(1.1) 写死在循环里,是因为免费接口 60 次/分钟的量级,跑几千个 IP 不限速的话,前几十个成功后面全是 429,脚本会白跑。
五、怎么选:几条能直接用的判断
这几类项目直接用在线接口更划算:
- 团队小、没有专门的人维护数据更新。
- 业务要求「新 IP 查得到」。新分配的 IP 段、云厂商新机房的 IP,离线库要等下次更新,接口是服务端的事。
- 要海外和 IPv6 的省市字段。
- 请求量不大(每天几万到几十万次),免费额度加上缓存就够用。
这几类项目绕不开离线库:
- 历史日志回溯,量级在每天千万条以上。250ms × 3000 万条是个不可能完成的任务,6µs 可以。
- 服务跑在内网、专网、离线环境里,出不了外网。
- 单机网关、边缘节点这类对延迟极敏感的位置,本地内存查一次比发一次 HTTP 稳。
- 想彻底控制成本和依赖,且能接受数据不是最新的。
**混合是大多数团队最后的选择:**在线接口做主力(数据新、维护成本为零),把离线库作为兜底——接口抖动、限速、超时的时候走本地库,宁可答案旧一点也别让业务空着。反过来也有企业用离线库做粗筛、关键决策再调接口核验。
最后提醒一句容易被忽略的事:**别把任何一个数据源的「区县」字段当成硬依据。**国内区县级的整体精度本来就不高,离线库的区县字段更是靠推算填的。拿它做风控的判定条件、或者做「用户精确位置」的产品功能,翻车是迟早的事。
IP 归属地在线的方案,我们用的一直是 IP9 的免费接口 https://ip9.com.cn/get?ip=<IP>:不传 ip 参数返回调用方自己的归属地,IPv4/IPv6 都能查,返回国家、省市、邮编、区号、运营商、经纬度(城市中心点)、大区这些字段,免费版 60 次/分钟;需要区县、IP 类型(ISP/企业/IDC 机房)、AS 号这类字段的话是 VIP 版能力,字段和额度在官网 https://www.ip9.com.cn 上写得很清楚。