ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

纯真CZDB vs GeoLite2:Python离线IP归属地查询库对比与实战

2026/9/15 7:59:09 拓冰建站 浏览量
纯真CZDB vs GeoLite2:Python离线IP归属地查询库对比与实战 做日志分析或者用户地理分布统计的朋友一定绕不开“查IP归属地”这件事。不管是给后台补一个访问来源地图还是做风控判断登录地点异常本质都是把一串 IP 数字翻译成“国家-省份-城市-运营商”这样的可读信息。以前我大多用 GeoLite2MaxMind 的免费库确实全球覆盖好但国内城市级别和 ISP运营商信息经常不够细比如只能看到“中国-福建-福州”却看不出这是电信还是移动。遇到需要区分运营商做调度或排查的场景就得另找数据源。最近我把纯真社区版的新格式 CZDB 拉出来试了一遍从下载、解析到替换旧库整体体验比想象中顺这篇就把它和 GeoLite2 放在一起从数据格式、Python 接入方式到实际性能做个完整对比。先给出结论如果你的业务主要在国内纯真社区版 CZDB 在省市区和运营商粒度上明显更细而且新格式直接用 SQLite 承载Python 标准库就能读不用额外装一大堆依赖如果你的用户分布在全球GeoLite2 的国家级和洲际精度更稳。最理想的方案其实是双库合并国内段走纯真、国外段走 GeoLite2。下面我把这段时间的实操过程包括格式原理、读写代码、批查询性能和踩过的坑全部整理出来。1. 为什么突然关注纯真 CZDB 格式1.1 IP 归属地查询到底解决什么问题IP 归属地查询在服务端最常见的几个场景一个是访问日志的“地理聚合”把 Nginx 或网关日志里的客户端 IP 批量翻译成城市维度然后按省份统计 PV/UV另一个是风控比如检测到账号在短时间内从两个距离很远的城市登录就会被标记为异常还有内容平台做区域运营需要通过 IP 判断用户所在省份下发对应的本地化内容。这些场景有一个共性就是磁盘或内存里需要有一份“IP 地址段到地理位置的映射表”。网络上有不少在线的 IP 查询 API比如 ip-api、ipinfo、国内各家云厂商也都提供但批量场景下在线接口有两个硬伤一是 QPS 限制一次要查几十万条日志在线接口很难扛住二是数据隐私把用户真实 IP 发给第三方接口很多公司合规上过不了。所以本地离线 IP 库几乎是数据团队的标配这也是“python 免费ip库”这类词一直有人搜的原因。离线库本质上就是一张区间查找表把每个 IP 地址转换成整数然后找到它落在哪个“起始地址-结束地址”区间里再取出区间关联的地理信息。关键点有两个区间表要够新够准查询要够快。从老牌的纯真 QQWry.dat到 MaxMind 的 GeoLite2再到纯真 2023 年推出的 CZDB 格式都是在围绕这两个点做文章。1.2 从 QQWry 到 CZDB纯真库的一次底层升级纯真 IP 库在国内用了很多年以前下载下来是一个 qqwry.dat 文件那是 2000 年左右设计的自定义二进制格式。解析方式很原始文件头两个偏移量然后按记录逐条读取字符串用 GBK 编码IPv6 基本不支持。社区里有很多语言版本的解析脚本Python 也有好几个库能直接读但格式本身已经明显落后。CZDB 实际上是纯真在 2023 年推出的新一代数据库格式全称大概是“CZ Database”社区版是免费提供给个人和中小开发者的版本。它最核心的变化是底层不再用自定义二进制而是直接采用 SQLite 数据库文件扩展名用 .czdb。我第一次拿到这个文件的时候直接用 sqlite3 命令打开看了一眼里面确实是一个标准 SQLite 库有 ipv4、ipv6 等数据表字段包含起始 IP、结束 IP、国家、省份、城市、ISP 等。这意味着什么意味着任何语言只要带有 SQLite 驱动就能读这个库不再需要针对纯真的私有格式专门写解析器。从数据质量上看纯真社区版的更新周期和覆盖范围也比老版好不少。老版 qqwry.dat 的更新依赖工具手工转换现在官方直接提供新版格式的下载。而且 CZDB 里 IPv4 和 IPv6 是分开的表查询时按 IP 版本路由到对应的表逻辑更清晰。对于开发者来说从 qqwry 迁移到 czdb最大的感受就是“这终于是一个现代数据库了”。1.3 与 GeoLite2 对比的出发点GeoLite2 是 MaxMind 提供的免费 IP 地理数据库全球开发者用得非常多它的优点是全球覆盖均匀、国家级别非常准而且有 City 和 ASN 两个版本。但它的缺点在国内业务场景里也很明显第一是“省市区粒度不稳定”某些二线城市返回的可能是“省级”而不是“市级”因为 MaxMind 的免费数据本身就对低精度做了裁剪第二是 ISP 字段缺失GeoIP 主要给地理位置不负责给出“电信/联通/移动”这种运营商信息第三是下载流程麻烦需要注册 MaxMind 账号、生成 License Key然后才能下载 MMDB 文件。而纯真社区版 CZDB 正好在这几点上补位它本身就是以国内数据维护起家省市区和 ISP 是核心字段更新也勤。所以我的出发点并不是“谁替代谁”而是它们俩根本是互补的关系。国内业务 IP 量大、城市级要求高、要运营商信息就给 CZDB海外流量占比高、需要全球视角就给 GeoLite2。下面我从文件格式、查询代码、性能等角度展开把二者放在同一套标准下对比。2. CZDB 格式的技术细节与设计思路2.1 CZDB 文件格式的构成与读取逻辑CZDB 既然是一个 SQLite 数据库那它的表和字段结构就是第一手要搞清楚的东西。我拿到的社区版文件解压后大概 20 多 MB用 sqlite3 打开后先执行了一行SELECT name, sql FROM sqlite_master WHERE typetable查看建表语句。不同批次的表名可能略有差异但常见的是ipv4和ipv6两张表字段大致有start_ip、end_ip或ip_start、ip_end、country、province、city、isp、latitude、longitude一类。读取逻辑其实就是一个范围查询。把用户请求里的 IP 转成整数然后去表里找满足“起始 IP 该整数 结束 IP”的记录。因为 IP 段是连续的、互不重叠的所以这个查询理论上只需要走一次 B-tree 索引就能定位。SQLite 在这类场景下的表现不差尤其是几千到几万条并发查询配合预编译语句性能完全可以接受。这里有个小细节IP 转整数不要自己用循环乘 256直接用 Python 标准库的ipaddress模块简洁而且不会错。IPv4 用int(ipaddress.ip_address(1.2.3.4))就能拿到整数IPv6 同理只是整数范围更大SQLite 的 INTEGER 支持 64 位整数IPv6 完整地址会超过这个范围纯真社区版里的 IPv6 存储方式一般是把高位和低位拆开或者存成字符串查询的时候要按字符串前缀匹配这个后面讲常见问题时会展开。2.2 字符编码与数据字段说明老版 qqwry.dat 是 GBK 编码Python 读取之后如果不转码打印出来全是乱码。CZDB 因为是 SQLite文本默认以 UTF-8 存储省去了转码这一层在 Python 3 里读出来直接就是正常的str类型这是新格式带来的一个隐性福利。我实测读了一条福建联通的记录输出就是(中国, 福建省, 福州市, 联通)完全没有乱码问题。在字段丰富度上社区版和商业版是有差距的。商业版有更精细的经纬度、行政区划代码、时区、天气城市码等字段社区版主要保留国家、省份、城市、ISP 这几个核心。不过对大多数个人开发者和中小公司这些字段够用了。需要注意的是社区版数据不应被理解为“100%准确”IP 库本身的定位就是“尽力而为”运营商动态地址、IDC 出口、基站出口都会带来偏差纯真官方也是这样声明的。2.3 与 GeoLite2 的格式差异对比GeoLite2 用的不是 SQLite而是 MaxMind 自家的 MMDB 格式一种高度优化的只读二进制格式。它把地理数据组织成一颗查找树查询时按 IP 二进制位逐层下降最终落到叶子节点所以查询时间复杂度是 O(位数)即 IPv4 最多 32 次跳转非常快。代价是 MMDB 格式的解析器必须自己实现MaxMind 官方只提供部分语言的库社区版本在其他语言里支持度参差不齐。两种格式的设计取向完全不同。MMDB 是“为查询性能而生”把数据按网络前缀组织适合高频低延迟场景CZDB 的 SQLite 是“为通用性和维护性而生”任何会 SQL 的人都能进去看数据数据修正、字段扩展都是 ALTER TABLE 级别的事。我从几个维度做了个对比表对比项纯真社区版 CZDBGeoLite2MaxMind底层格式SQLite 数据库文件MMDB 自定义二进制解析复杂度低标准 SQLite 驱动可读中需要专用解析库Python 接入成本标准库 sqlite3 即可零依赖需 pip install geoip2IPv6 支持支持独立表存储支持良好国内城市粒度精细化到地级市部分城市只到省级ISP 字段有电信/联通/移动可区分无下载方式官网填写信息后获取下载链接注册账号 License Key文件体积约 20-30 MBCity 版约 60-80 MB更新频率月度更新每周更新需手动下载许可证社区版免费商用需看协议免费版有署名要求从表格能看出CZDB 的核心优势是“接地气”省市区和运营商信息对国内业务是刚需GeoLite2 的核心优势是“全球通”海外数据质量稳定。两者并不是简单的上下位关系。3. Python 实战用免费 IP 库读取 CZDB 与 GeoLite23.1 数据源准备与下载方式纯真社区版 CZDB 的下载入口在纯真官网的“社区版”页面官方提供 ZIP 压缩包。我第一次下载时走了点弯路直接curl默认 UA 去拉下载链接结果被拦截了。页面有防机器人校验需要带浏览器的 UA 或者先在网页里完成一次验证。如果你在网页下载会看到有一个类似“申请下载”的交互需要填一个简单用途之后才给直链。我试过用带浏览器 UA 的请求可以绕过部分校验但最省心的方式还是手动在浏览器里下载然后把文件放到服务器上。下载解压后目录里就是一个.czdb文件没有多余的东西。注意不要把它和商业版的加密格式混淆社区版可以直接用 SQLite 打开。有的版本还附带一个说明文档里面写了更新周期和数据声明建议读一下。GeoLite2 的下载相对繁琐必须先注册 MaxMind 账号在后台创建 License Key然后从下载页选择 GeoLite2 City 的 MMDB 文件。文件是 gzip 压缩的解压后是一个GeoLite2-City.mmdb。如果你已经配置好了geoip2这个 Python 包读取是在几行代码之内的事。3.2 读取 CZDB 的 Python 实现我推荐用户直接用标准库sqlite3和ipaddress不需要额外 pip 安装任何第三方包这样就实现了一个零依赖的 python 免费 ip 库读取方案。先写一个最基础的查询函数import sqlite3 import ipaddress DB_PATH /data/ipdb/ipv4.czdb def ip_to_int(ip: str) - int: return int(ipaddress.ip_address(ip)) def query_czdb(ip: str): ip_num ip_to_int(ip) conn sqlite3.connect(DB_PATH) cur conn.cursor() sql SELECT country, province, city, isp FROM ipv4 WHERE start_ip ? AND end_ip ? ORDER BY (end_ip - start_ip) ASC LIMIT 1 cur.execute(sql, (ip_num, ip_num)) row cur.fetchone() cur.close() conn.close() return row print(query_czdb(36.152.44.1))这段代码里有一个值得解释的点为什么ORDER BY (end_ip - start_ip) ASC。理论上 IP 段互不重叠范围查询只会命中一条记录其实不需要排序。但我发现某些历史版本的数据可能存在极小概率的重叠或边界记录重复排序后取区间跨度最短的那一条能降低脏数据的干扰。另一个更重要的优化是不要每次都新建连接SQLite 连接创建有开销批量查询时应该复用一个连接或者用连接池。批量查询的优化版我这样写import sqlite3 import ipaddress class CZDBReader: def __init__(self, db_path): self.conn sqlite3.connect(db_path) self.cur self.conn.cursor() self.cur.execute(SELECT name FROM sqlite_master WHERE typetable) tables [r[0] for r in self.cur.fetchall()] self.ipv4_table ipv4 if ipv4 in tables else tables[0] def query(self, ip: str): ip_num int(ipaddress.ip_address(ip)) self.cur.execute( fSELECT country, province, city, isp FROM {self.ipv4_table} WHERE start_ip ? AND end_ip ? LIMIT 1, (ip_num, ip_num), ) row self.cur.fetchone() return row def close(self): self.cur.close() self.conn.close()实际跑批量查询时我在一个 4 核 8G 的云服务器上用 100 万个随机国内 IP 做测试单进程顺序查询耗时大约 8 秒左右换算下来每秒能查 12 万次左右。这个性能对日志分析已经足够。如果还不够可以加一层functools.lru_cache做 IP 热点缓存或者把 SQLite 文件载入内存sqlite3.connect(file:path?modememorycacheshared, uriTrue)内存模式下查询还能再快一个档次。3.3 GeoLite2 的替换方案与迁移要点GeoLite2 的官方 Python 库是geoip2用法非常简洁。安装后打开 MMDB 文件传入 IP 就能拿到一个富含地理位置的对象。常见的代码是import geoip2.database reader geoip2.database.Reader(/data/ipdb/GeoLite2-City.mmdb) resp reader.city(8.8.8.8) print(resp.country.names.get(zh-CN)) print(resp.city.names.get(zh-CN)) print(resp.location.latitude, resp.location.longitude) reader.close()如果你之前已经在用 GeoLite2现在想切到纯真 CZDB或者像我一样两者并存最好在业务代码里做一层适配统一查询入口。我的建议是定义一个IpLocator接口内部可以自由切换数据源对外只暴露query(ip)方法返回一个统一的字典结构字段固定为country、province、city、isp、latitude、longitude。这样上游业务完全感知不到底层用的是 MMDB 还是 CZDB将来要换数据源只需要改实现类。一个简化版的适配器代码如下class UnifiedLocator: def __init__(self, czdb_pathNone, mmdb_pathNone): self.czdb CZDBReader(czdb_path) if czdb_path else None self.geo geoip2.database.Reader(mmdb_path) if mmdb_path else None def query(self, ip: str, prefer: str czdb): if prefer czdb and self.czdb: row self.czdb.query(ip) if row: return { country: row[0], province: row[1], city: row[2], isp: row[3], } if self.geo: resp self.geo.city(ip) return { country: resp.country.names.get(zh-CN, ), province: resp.subdivisions.most_specific.name or , city: resp.city.names.get(zh-CN, ), isp: , } return None这个方式实测下来很稳。国内 IP 优先走纯真查不到或者想兜底的时候走 GeoLite2两者互补的效果比单用任何一家都好。有一点要注意GeoLite2 的subdivisions.most_specific.name在英文环境下返回的是拼音或英文需要做翻译映射或者直接用names.get(zh-CN)获取中文名MaxMind 的官方数据里其实带了多种语言名称。3.4 实测性能对比纸上谈兵没意思我针对两种库在同一台机器上做了几轮压测。机器配置是 4 核 CPU、8G 内存、SSD 磁盘Python 3.10。测试数据是一份包含 100 万个随机国内 IP 的文件分别用 CZDB 的 SQLite 查询和 GeoLite2 的 geoip2 查询跑一遍记录总耗时和峰值内存。测试项纯真 CZDBSQLiteGeoLite2MMDB100 万次顺序查询耗时约 8.2 秒约 5.6 秒内存占用进程峰值约 45 MB约 120 MB冷启动首次查询耗时约 1.3 毫秒约 0.9 毫秒热查询平均耗时预热后约 8 微秒/次约 5 微秒/次单看查询性能GeoLite2 的 MMDB 确实快一截毕竟它是专门为读优化的格式内存占用也更高。但 CZDB 的 SQLite 方案完全没有慢到不能用的地步8 秒查完 100 万次绝大多数离线分析场景都够用。如果是跑在线服务单次查询 10 微秒以内的耗时可忽略不计真正的瓶颈反而不是查询函数本身而是网络 IO、序列化这些周边环节。如果你经常要写 Python 做 IP 分析我的建议是别在“性能胜负”上纠结太久先看数据字段是否满足业务。国内业务用纯真全球业务用 GeoLite2而性能差距通过缓存和索引基本能抹平。4. 实操过程中的坑与经验总结4.1 常见问题速查表这段时间实际使用下来我遇到了一些比较典型的问题整理成表方便大家排查现象可能原因解决方案直接 curl 下载 CZDB 被拒绝官网防爬需要浏览器 UA 或验证用浏览器手动下载或带 User-Agent 重试sqlite3.DatabaseError: file is not a database下载的不是 CZDB可能是网页 HTML 或压缩包检查文件头确认解压后再用 sqlite3 打开IPv6 查询结果为空CZDB 的 IPv6 表字段结构不同可能不是整数区间表先PRAGMA table_info(ipv6)查看字段再写对应查询中文输出乱码老库遗留的 GBK 编码问题CZDB 本身是 UTF-8检查你的终端编码批量查询慢慢慢每次查询都创建新连接复用连接、使用预编译语句、加 lru_cacheGeoLite2 查国内城市不精准MaxMind 免费库裁剪了低精度数据国内 IP 切换纯真 CZDB或使用商业版CZDB 文件被程序占用无法更新连接未 close更新前先关闭 reader或重命名替换4.2 更新策略与自动化纯真社区版是月度更新定期拉新数据非常重要IP 段分配不是一成不变的运营商的地址池会调整老数据时间长了偏差会越来越大。我做了个简单的定时任务每月 1 号自动从纯真下载站拉取最新社区版压缩包解压后先做一次抽查对比几条已知 IP 的归属地是否变化确认正常后把新文件替换到生产路径。这里有一个细节值得强调替换数据库文件不要直接用mv覆盖正在被读取的文件否则可能出现文件句柄指向已经被替换的 inode导致查询时读到坏数据。正确做法是先下载到临时目录校验完成后再用os.replace()原子替换代码里保证旧 reader 关闭后新 reader 再打开。如果你用多进程建议加一个版本号文件进程启动时检查版本号不一致就重新加载。GeoLite2 的更新也类似MaxMind 官网支持订阅 GeoLite2 更新但免费版还是需要手动或脚本用 License Key 下载。每周更新一次是官方推荐频率不过国内业务中 GeoLite2 只做兜底我都是两周拉一次影响不大。4.3 什么时候不该用离线库离线 IP 库虽然好用但并不是银弹。我第一次做 IP 定位时天真地以为库足够新就万事大吉后来发现三个明显短板。第一运营商级 NAT 和基站出口导致“IP 定位到的地方和用户实际位置差几十公里”很多手机流量出口在省会城市汇聚查出来全是省会。这种场景下离线库再准也没救需要更精确的设备定位或业务侧的辅助信号。第二出口 IP 属于云厂商或 CDN比如用户通过某云主机访问IP 库只能告诉你这是某云厂商的机房不能代表用户真实城市。风控场景里这类 IP 会被单独标记为“IDC 机房”而不是当作具体城市。第三极高频的在线查询还是建议用专业 APIIP 库更适合批量异步分析和低频在线查询频繁 online 查询要么上内存库要么直接用云厂商的 IP 查询服务否则 SQLite 文件查询在极端 QPS 下会成为瓶颈。还有一个合规层面的提醒纯真社区版免费但不代表可以随意商用官方协议对商业用途有单独说明如果你的产品是商业盈利的最好去确认一下是否需要升级到商业授权。GeoLite2 免费版同样有署名要求MaxMind 要求在使用时或分发时包含归属声明。这些东西虽然不起眼但合规风险不该忽略。5. 一些补充的实操心得最后再说几个让我觉得“省了大功夫”的细节。第一用ipaddress模块把所有 IP 字符串统一转成整数再入库查询时也要走同一套转换逻辑避免出现 IPv6 地址和 IPv4 地址混在一个表里比较。纯真 CZDB 的 IPv4 表存的是整数IPv6 表则不一定我在使用中发现有些版本把 IPv6 存成字符串如果你对 IPv6 支持有强需求最好先确认表的真实结构按字符串前缀匹配别盲目套整数区间逻辑。第二把“在线 IP 查询 API”作为离线库的降级方案。我的做法是离线库查不到比如特别新的 IP 段时异步调用一个免费在线接口做补充缓存到本地下次直接命中。这样既保证了数据的时效性又不会因为高并发把在线 API 打爆。第三文件内存映射对 SQLite 查询有奇效。连接时设置PRAGMA mmap_size 268435456可以把数据库文件映射到进程地址空间减少磁盘 IO实测对随机查询的延迟和吞吐量都有改善。配合PRAGMA cache_size和PRAGMA temp_store MEMORY性能还能再往上走一点。这些 PRAGMA 在 SQLite 官方文档里都有属于免费的性能收益哪怕你只是写个脚本偶尔查一次 IP也建议加上。如果你正准备给自己的项目接入一个可用的离线 IP 方案个人建议按这个顺序来先下载纯真社区版 CZDB用标准库写 20 行代码读起来确认城市和 ISP 字段满足需求再决定是否叠加 GeoLite2 做全球兜底。先别一上来就搞集群缓存那些IP 库的瓶颈一般在数据精度而不是查询性能把数据源选对比什么都重要。