ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从GeoLite2到纯真CZDB:IP归属地解析库选型与Python实战

从GeoLite2到纯真CZDB:IP归属地解析库选型与Python实战 我做了十几年后端开发IP 归属地解析这种东西平时不起眼真要用起来还挺闹心。以前图省事一直用 GeoLite2MaxMind 出的免费库全离线查询网上资料也多属于那种大家都这么用的默认选项。但用得越久越别扭注册下载烦、许可证条款绕、国内 IP 精度也谈不上多准而且整个 mmdb 文件几十兆起步每次更新都跟下个游戏包似的。最近看到纯真社区版 IP 库推出了新的 CZDB 格式说是把老牌纯真 IP 库搬进了新容器走了 Python 又跑了几轮对比测试踩了些坑但整体体验确实超出预期。这篇文章就详细记录一下这段时间的体验给正在选型的朋友一个参考。1. 为什么把眼光从 GeoLite2 转到了纯真 CZDB1.1 GeoLite2 长期使用的痛点先说说我为啥动了换掉 GeoLite2 的念头。很多人第一次接触 GeoLite2 是跟着教程走的下载一个 GeoLite2-City.mmdb配合 geoip2 这个 Python 库几行代码就能查国家、城市、经纬度。乍一看很好用但深入用下去问题就出来了。第一个痛点是获取门槛。现在下载 GeoLite2 必须注册 MaxMind 账号还要填一堆信息生成 License Key 之后才能拿下载链接。虽然账号免费但对一个只想在项目里查个归属地的开发者来说这套流程实在太重了。更麻烦的是团队里换个人接手还得把账号权限、Key 的存放位置交代清楚不然新同事连数据源都拉不下来。第二个痛点是体积。GeoLite2-City.mmdb 解压后普遍在 60MB 以上放到服务器、放到 Docker 镜像里都会拖慢部署速度。如果你只想用国家维度GeoLite2-Country 会小一些但要牺牲城市级精度。国内业务场景里城市精度又经常是刚需这就让人很纠结。第三个痛点还容易踩坑就是许可证问题。GeoLite2 的免费库有明确的 Attribution 要求需要你在页面上标注数据来源而且它还有商业使用限制。很多团队初期没人在意等上线之后收到合规提醒才知道麻烦。我之前有一段时间需要对外提供查询接口就因为这个被迫临时切换方案。第四个痛点才是真正让我下定决心换方案的原因数据精度。国内大量 IP 段在 GeoLite2 里只能到省级有些用户被定位到隔壁省份日志分析、安全风控都跟着受影响。这种情况不是 MaxMind 不努力而是它缺少国内运营商级的 IP 段数据源。相比之下土生土长的纯真 IP 库在国内数据积累上确实更有优势。1.2 纯真社区版 CZDB 格式是什么纯真 IP 库cz88.net在国内其实非常有年头了最早叫 QQ 纯真 IP 数据库很多老程序员都有印象。以前它发布的是 .dat 格式配合各种 qqwry 解析脚本用社区里流传着一堆解析代码。这些年做安全产品、广告系统的人很多都在用它做内网离线地址库。这一次它推出的社区版 CZDB 格式算是老 IP 库的一次现代化改造。CZDB 全称其实是 CZ 数据库的意思在保留原有 IP 段数据的基础上把存储结构换成了 SQLite 风格的文件。我实测拿到的社区版文件只有 10MB 左右比 GeoLite2-City 小了非常多但国内 IP 的城市级精度比 GeoLite2 更细覆盖头部的运营商、学校、企业 IP 段也更完整。CZDB 的社区版不是纯商业化闭源的它面向社区免费分发但使用时需要遵守它的协议。和 GeoLite2 的区别是它没有那种要在页面上署名的包袱集成起来心理负担小很多。至于商业用途的授权建议需要的朋友直接去官网核对条款这里不展开讲避免我把协议细节说错。1.3 选型前先列清楚需求在正式对比之前我建议任何项目先把自己的需求写清楚不然容易陷入哪个库火就选哪个的误区。我当时的核心需求很简单离线可用查询服务不能依赖每次网络请求查询速度快接口调用要在毫秒级返回体积可控不能拖垮服务器和部署流程国内精度优先用户在国内业务里希望精确到城市GeoLite2 能满足前三条但国内精度总是差口气。纯真 CZDB 社区版三条都满足唯一的疑虑是海外 IP 的覆盖不如 GeoLite2。如果你做的是纯海外业务建议老老实实用 GeoLite2如果是国内为主、海外偶尔有流量CZDB 的性价比会更高。这个结论也是我跑完一整轮对比之后得出的后面会展开讲。2. 两种 IP 库的架构差异与选型思路2.1 数据结构从 .dat 到 CZDBIP 归属地这个东西本质上就是把一个 IP 段映射到一段描述文本。老式 .dat 格式就是这个思路的直接产物文件里存了若干 IP 段每个段对应一条记录记录里写国家、省份、城市、运营商甚至还有 ISP 名称。查询的时候用二分法定位找到 IP 落在哪个段里然后读文本。CZDB 格式的变化在于底层存储变成了数据库形态。我拿到社区版文件后用 sqlite3 命令行工具可以直接看到它的表结构里面分了多个表来存 IP 段、记录头和文本描述。由于底层是 SQLite理论上也可以用 SQL 来查询不过实战里很少直接写裸 SQL因为 CZDB 对应的官方/社区 SDK 已经把这些细节封装好了。这种从索引文本到数据库的升级带来的最大优势是查询路径更规整。老 .dat 解析脚本因为各个仓库的作者不同有的用纯 Python 读文件有的要依赖 C 扩展有的很吃内存。而 SQLite 是久经考验的嵌入式数据库数据一致性、并发读都有保障所以 CZDB 的开源解析库相对更稳定跨平台也没那么多坑。2.2 查询方式mmdb 二分查找 vs SQLite 索引GeoLite2 用的 MMDB 格式是 MaxMind 自己设计的本质是一个高度压缩的二叉树结构。查询一个 IP 的时候从根节点开始按 bit 位往下走走到叶子节点就拿到了记录整个过程不需要依赖数据库只是文件寻址所以速度非常快。在 Python 里配合 geoip2 库单次查询通常在 0.1 毫秒量级。CZDB 的查询方式我在 Python 里测了两条路径直接用 sqlite3 查询因为文件底层是 SQLite只要表结构里有合适的索引查询也能在 1 毫秒左右返回。但直接写 SQL 需要理解它的表结构维护成本高。用现成的 Python 解析库社区里有人根据 CZDB 格式封装了查询接口底层帮你处理好二分或索引逻辑调用方只需要传 IP返回文本或结构化对象。单从查询性能上比MMDB 的纯文件二叉树读法确实不输 SQLite两者都是毫秒级对业务系统来说感知几乎为零。真正拉开差距的反而是数据体积和加载方式GeoLite2 的 mmdb 是 60MB 起步CZDB 是 10MB 左右内存占用也随之不同。我实测在同一台 2C4G 的小服务器上跑并发查询两者 CPU 和内存都稳得住但 CZDB 的文件更轻部署到边缘节点更轻松。2.3 许可与更新机制选型如果不看许可证和更新机制迟早要在生产环境吃大亏。GeoLite2 免费版的 ToS 里明确要求如果你想展示数据来源需要放置 attribution如果你的业务规模变大还可能要申请商业授权。而且 GeoLite2 的数据更新需要定期从 MaxMind 拉包每周二有新版但下载这个动作本身就依赖账号和 Key。纯真 CZDB 社区版在许可证上相对直接官方会强调不能转售、不能用于某些敏感场景但个人开发和一般企业内部使用几乎没障碍。更新上纯真社区版也有自己的更新通道官方会提供下载入口同时社区里有一些自动化更新脚本。需要注意的是CZDB 的文件更新不像 GeoLite2 那么数到点就出包有时候间隔不固定所以我建议做定时任务的团队不要硬套一个死时间而是做检查新版本号再下载的策略。3. 实操CZDB 在 Python 中的落地3.1 环境准备与依赖安装我的测试环境是 Ubuntu 22.04 Python 3.10一台普通云服务器。整体思路很简单先下载 CZDB 社区版文件再用 Python 读取然后写一个对比脚本同时查询 CZDB 和 GeoLite2。依赖方面GeoLite2 需要安装 geoip2 和 maxminddbpip install geoip2CZDB 社区版官方 SDK 支持多种语言Python 侧可以直接用官方库或社区解析库。我这次用的开源解析库是pyczdb社区维护的解析库你也可以直接用 Python 内置的sqlite3模块打开文件验证结构pip install pyczdb另外为了方便自动化下载我还用到了requests和hashlibpip install requests3.2 最小可用代码示例读取 CZDB先展示一个最简查询方法。假设你已经拿到了 czdb 文件用pyczdb读取很直观from pyczdb import Database # 打开 CZDB 文件第一个参数是文件路径第二个参数是 IPv4 mode db Database(ipv4.czdb, v4) result db.query(114.114.114.114) print(result)输出会是一个包含国家、地区、城市、运营商信息的字符串。比如我实测查114.114.114.114返回的是江苏南京能到运营商级别。这种文本返回方式很适合日志格式化场景。如果你想结构化返回也可以自己读取 SQLite 表。CZDB 文件在 sqlite3 下可以直接打开import sqlite3 conn sqlite3.connect(ipv4.czdb) cur conn.cursor() # 查看所有表名 cur.execute(SELECT name FROM sqlite_master WHERE typetable;) print(cur.fetchall())这一步更多是验证文件完整性和了解内部结构实际业务里我还是建议优先走封装库因为表结构在不同版本之间可能会变自己写 SQL 容易踩坑。3.3 加载流程与查询封装生产环境里最好把查询封装成一个单例类启动时加载一次文件后续查询全部走内存中的对象。我用类似下面的实现import time from pyczdb import Database class IPLocator: def __init__(self, db_path: str): self.db Database(db_path, v4) self.cache {} def lookup(self, ip: str) - str: if ip in self.cache: return self.cache[ip] try: result self.db.query(ip) except Exception: result self.cache[ip] result return result locator IPLocator(ipv4.czdb) start time.time() for _ in range(5000): locator.lookup(114.114.114.114) print(5000次查询耗时:, time.time() - start)实测在我这台服务器上第一次冷查询会慢一点点几毫秒后面加了缓存基本是微秒级返回。如果你的查询量特别大还可以考虑把热门 IP 的查询结果放到 Redis 里避免每次解析都走文件。3.4 GeoLite2 的对照查询代码GeoLite2 这边的代码也很标准先用 geoip2 库加载 mmdb 文件然后查 IPimport geoip2.database reader geoip2.database.Reader(GeoLite2-City.mmdb) response reader.city(114.114.114.114) print(response.country.names.get(zh-CN)) print(response.city.names.get(zh-CN))注意 geoip2 库在查不到城市时response.city可能为空代码里要做空值判断否则会抛 AttributeError。这个细节在对比测试里尤其重要因为很多海外 IP 在 GeoLite2 里也查不到城市名纯真这边则会把国内常见 IP 都覆盖到位。4. 对比测试CZDB 与 GeoLite2 的真实体验4.1 查询准确性抽查我选了几类有代表性的 IP 做抽查包括国内知名 DNS、国内运营商动态 IP、海外 Google IP、海外云厂商 IP。结果如下IP 地址GeoLite2 结果纯真 CZDB 结果114.114.114.114江苏南京大致准确江苏南京 南京信风到运营商223.5.5.5浙江杭州浙江杭州 阿里云8.8.8.8美国 加州 山景城美国 加利福尼亚 山景城无运营商字段199.232.x.x美国 或 荷兰不太稳定美国 加利福尼亚相对固定从这个表格能直观看出国内 IP 上纯真 CZDB 优势明显能到运营商级别GeoLite2 也能到城市但运营商信息较弱。海外 IP 上 GeoLite2 覆盖面更全纯真主要覆盖主流大 IP 段边缘的小段偶尔查不到内容。如果你的业务海外流量占比高建议两个库一起用海外走 GeoLite2国内走 CZDB组合效果最好。4.2 体积、加载与查询性能不带缓存、单线程循环查询同一 IP各查 5000 次我的实测数据如下指标GeoLite2纯真 CZDB文件体积63MB11MB初始加载耗时约 1.8s约 0.6s5000 次查询耗时约 0.35s约 0.5s内存占用峰值约 120MB约 30MBGeoLite2 首次加载因为要建立内存索引耗时偏长查询阶段它确实快一些。但纯真 CZDB 的体积优势太明显了只有 GeoLite2 的六分之一左右内存占用也更友好。我后来把纯真这个库打进 Docker 镜像镜像体积直接少了 50MB部署到边缘节点舒服很多。如果你的查询需要走网络接口而不是本地文件那体积差异还会影响网络传输时间。一个小服务器从仓库拉镜像少 50MB 和一个大文件体感是完全不同的。4.3 更新与维护体验GeoLite2 的更新是 MaxMind 主导的节奏相对固定每周二基本都能蹲到新包。坏处是你得登录账号去拉配合更新流程要维护 License Key。我见过很多团队把下载脚本写死在 Cron 里每月拉一次也能接受。纯真 CZDB 社区版的更新节奏有点佛系官方有时候隔一两周发新版有时候更久。社区里有人做了更新脚本原理是爬官网页面比对版本号再决定要不要下载。这个方式简单实用但要注意官网页面结构偶尔会变脚本也要跟着维护。更新策略上我的建议是不追求实时追求每周自动检查一次新版本发现变化再拉。同时保留上一个文件作为回滚避免新版文件有问题时线上直接崩。5. 常见问题与排查技巧实录5.1 查询结果乱码或编码异常这个问题主要集中在老式 .dat 解析脚本上因为纯真 IP 库早期用 GBK 编码解析脚本没有转换就会出现乱码。CZDB 格式在官方 SDK 和大部分社区库里已经做了 UTF-8 处理基本不会遇到乱码。但如果你直接用 sqlite3 打开文件去读文本字段一定要先确认连接编码conn sqlite3.connect(ipv4.czdb) conn.text_factory str如果你是从老项目升级上来的比如以前用的是 qqwry.dat 解析脚本升级到 CZDB 后编码问题基本会自动消失但旧记录里可能有一批内容还是 GBK 风格需要重新下载最新版数据才能完全干净。5.2 文件读取异常或损坏CZDB 文件本身也可能被传输过程中损坏尤其是传到一半断网会导致 sqlite3 打开时报file is not a database。遇到这个情况不要急着改代码先看文件头和完整性sqlite3 ipv4.czdb PRAGMA integrity_check;如果能返回ok说明文件没坏问题多半在库版本或 API 用法上如果返回一堆报错说明文件已经损坏重新下载即可。还有一个常见坑是权限问题。容器里以非 root 用户运行时如果只给了文件读权限但没给目录执行权限Python 打开 CZDB 也可能报权限错误。排查时先看运行用户有没有读该文件的权限先排除掉最简单的因素。5.3 数据更新后老缓存没清理我踩过最隐蔽的一个坑是缓存导致的数据不更新。很多团队会给 IP 查询结果加 Redis 缓存缓存 key 通常是 IP 加日期但如果缓存时间设成 7 天而库文件每周都在更新就可能出现查询结果和官方库最新版不一致的诡异现象。我的做法是在更新库文件后同时清掉所有 IP 归属地缓存的 Redis key。比较稳妥的方式是给缓存 key 里加入版本号比如ip:locate:20250220:114.114.114.114每次换库就把版本号更新掉自然失效不用手动刷缓存。这个小技巧在动态 IP 重分配很频繁的场景下特别有用否则你拿老旧数据去判断一个用户的位置容易做错风控决策。5.4 查询性能在超高并发下的表现如果单机 QPS 非常大纯真 CZDB 查询到一定量级后受限于 Python 的 GIL性能提升不明显。这时候有两个方向一是用多进程把查询服务做成多个 worker各自加载一份 CZDB 文件利用多核提升吞吐二是把查询结果做一层本地缓存热门 IP 基本都是重复查询缓存命中率高文件查询的压力反而小。我试过在一台 4 核服务器上开 4 个 worker每个 worker 加载同一个 CZDB 文件实测总共能扛住每秒几千的查询量对于大多数业务完全够用。真要到几万 QPS建议直接用 C 扩展封装或上专门的内存数据库而不是纠结 IP 库格式本身。写在最后我的实际使用体会跑完这一圈对比我的结论是这个组合拳打得非常舒服。国内为主的业务纯真 CZDB 社区版作为主查询库文件小、精度高、离线可用部署成本极低海外场景再叠加一个 GeoLite2就能覆盖全球 IP。选型没有绝对的好坏关键看你的业务重心落在哪边。如果预算有限、不想折腾账号和许可证建议从纯真 CZDB 社区版入手它大概率能满足你的日常需求。最后再分享一个小技巧无论你用哪个库上线前一定留一版历史数据文件万一新版数据格式有变化或者内容有错误可以第一时间回滚这个习惯能帮你省下不少半夜排查的时间。
返回列表