Skip to content

ip2region、qqzeng-ip、GeoIP2 和在线接口怎么选?离线 IP 库实测对比(2026) ​

搜「IP 归属地」相关的库,几乎每个 README 都写着同样的三句话:准确率 99.9%、查询 0.0x 毫秒、体积只有几 MB。

这些数字不算假,但每一句都带着前提。这篇文章把四类数据源(ip2region、qqzeng-ip、GeoIP2/GeoLite2、在线接口)的机制讲清楚,附上今天刚跑的一份实测:文件多大、加载多久、单次查询多快,以及同一批 IP 在两个数据源上给出的结论到底差在哪。

一、先搞清楚「准确率 99.9%」在说什么 ​

这个词有三种可能的含义,混在一起就变成了营销话术:

  1. 能返回结果的 IP 占比。也就是「不是空的」。库越大越全,这个数字越高,但不代表答案对。
  2. 中国区地级市粒度的正确率。这是国内离线库最常引用的口径,通常基于自家或第三方抽样。
  3. 全球所有 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.153United States|California|San Francisco|0|US美国 / 加利福尼亚州 /(城市为空),isp=GitHub
8.8.8.8United 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::8888United 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 上写得很清楚。