
做了十几年后台开发和数据分析IP归属地查询这个需求几乎在每个项目里都会遇上。用户登录日志要显示所在地风控系统要判断异地登录内容系统要做区域投放运营报表要看访问省份分布——这些场景背后几乎都绕不开同一个名字纯真IP数据库。这是个从十八年前就开始持续维护的离线IP归属库到今天依然被很多人称作中文网络中的首选方案。它做的事情非常聚焦给一个IP地址快速告诉你在哪个省、哪个市、哪个运营商。相比依赖在线API的做法纯真库最大的特点是离线、免费、可本地部署整个数据就是一个.dat文件十几MB大小单次查询在毫秒级完成。这篇文章我打算把这个库从头到尾讲透它为什么能活十八年还被广泛使用.dat文件的内部结构长什么样具体怎么下载、解析、接入现有系统以及我这些年实际使用中踩过的坑和排查思路。不管你是第一次接触IP归属查询的新手还是想替换现有方案的老手这篇内容应该都能直接参考。1. 纯真IP数据库是什么为什么能在中文互联网里存活十八年1.1 一个离线文件解决IP归属查询纯真IP数据库做的事情一句话就能说清把全球IP地址段切分成一个个区间每个区间对应一条位置信息统一打包进一个.dat文件。之所以要按“区间”而不是按“单个IP”存储是因为IP地址的分配天然是按段进行的。一个运营商拿到一个地址段会继续划分给不同城市、不同区县但落到数据库层面往往是一整段连续的IP共享同一条归属记录。我经常用一个类比来解释这个设计IP区间就像电话区号。010开头的基本是北京021开头的基本是上海数据库做的就是把“拿到一个号码查出它属于哪个区号范围”这件事做到极致。纯真库就是这个“区号表”只是它的粒度精细到了城市维度还能同时区分电信、联通、移动等运营商。“离线”是这个方案最突出的一点。下载完.dat文件之后所有查询都在本地进程内完成不发起任何网络请求。正因为这个特性它才能在没有云服务、没有在线API的年代就被广泛使用也在今天这类依赖外部服务的方案大行其道时依然保持竞争力。1.2 为什么离线查询在今天仍然有优势有人会问现在在线API那么多直接调接口不就行了确实行但代价也不少。第一每次查询都有网络往返。在批量处理历史日志或者高并发查询的场景下哪怕单次只要几十毫秒累积起来也会变成明显的吞吐瓶颈还需要考虑超时重试、限流降级一堆额外逻辑。第二第三方服务通常有配额限制。免费额度阶段可以随便用业务一上来QPS超了就得付费扩容成本从“零”变成“每个月固定支出”。第三也是最容易被忽视的一点把每一个待查询IP发到外部服务器等于把访客的一部分网络身份信息交给了第三方。这一点在企业内部做数据合规评审时经常被挑战——数据未出域这一条就是很多离线方案的核心卖点。纯真库把这三类问题一起解决掉了查询不依赖网络、没有配额概念、数据不出本地。对日志分析、安全风控这类动辄千万级查询量的场景这个优势非常实用。1.3 谁在用它解决什么问题我从实际项目里观察纯真库的用户群体远比想象中广主要分布在这几类场景场景具体用途关注点网站统计访问日志打地理标签、访客热力图查询量大、免费安全风控异地登录提醒、异常流量溯源低延迟、离线可靠内容运营按区域展示本地频道或内容国内信息准确运维排障分析Nginx日志、定位来源集中地可批量、可离线个人开发浏览器插件、命令行工具、爬虫标注接入简单、免费这些场景的共同特点是查询量大、不苛求绝对精确、但要求快速稳定且成本可控。纯真库恰好就踩在这条需求线上这也是它能持续存活十八年的市场逻辑。2. 核心原理拆解.dat文件里到底存了什么2.1 IP归属查询的本质区间匹配要理解.dat文件的组织方式先要明白IP地址的本质。IP地址是一个32位无符号整数点分十进制如114.114.114.114只是给人看的显示形式。判断一个IP属于哪条地理位置记录本质是把这个整数和一系列区间做匹配看它落在哪个区间里。举个例子如果数据库里有这么一条记录起始IP结束IP归属1.0.1.01.0.3.255福建省福州市电信那么1.0.1.55、1.0.2.128、1.0.3.254这些地址都会被归一为“福建省福州市电信”。整个IP空间就像一个巨大的书架每条记录就是一层隔板查IP就是在隔板之间找到正确的那一格。2.2 纯真.dat的数据组织方式纯真库沿用的经典格式通常以qqwry.dat为文件名在结构上分成三个部分文件头、索引区、记录区。文件头只有8个字节前4字节记录索引区在整个文件中的起始偏移后4字节记录索引区结束偏移。索引区才是查询的入口里面按IP从小到大的顺序排列着一条条索引项每条索引项记录了一个IP区间的起始IP以及这条区间对应的数据记录偏移量。记录区里放着真正的字符串内容也就是国家、省份、城市、运营商这些信息。这个布局放到今天看依然非常聪明索引区有序所以可以二分查找记录区按需读取所以加载时不用把全部字符串都扒出来。一把文件读入内存只需要保留索引区记录区在查询命中时才去读取内存占用被压得很低。2.3 查询为什么快二分查找与索引设计查询性能的核心是二分查找。索引区按起始IP排序后一次查询只需要做大约log2(N)次比较。纯真库的索引条目数大约在百万量级也就是说一次查询只需要二十来次比较就能定位到候选索引项再读取对应偏移处的记录字符串整个过程就是几次文件读取加一次字符串解码毫秒级返回毫无压力。实际工程中还有两个优化点。一是把整个索引区一次性读入内存查询时完全不做文件seek只在内层面比较速度可以提到微秒级二是用mmap把文件映射到进程地址空间操作系统按页加载既能享受内存级读取速度又不需要真的把文件全部读出来。这两个方案我都在项目里用过后者在文件较大、内存紧张时更稳。3. 实操指南下载、解析与接入全过程3.1 获取更新文件与版本核对纯真官网定期发布更新版本文件名一般就是qqwry.dat。把下载链接交给服务器端脚本直接拉取或者手动下载后上传两种方式都行。我自己的习惯是设置一个定期任务每个月去检查一次是否有新版本有就自动下载替换。替换前我一般做三件事。第一确认版本日期确实比当前版本新避免回退第二对比文件大小与官方标注是否一致防止下载截断或中途损坏第三下载后先抽样查询几个已知IP比如用几个自己所在城市的IP测试归属是否合理确认无误再正式替换线上文件。3.2 手写一个查询器从文件到结果的完整逻辑很多语言都有现成的库但了解底层逻辑能帮你排查问题。这里给一个Python参考实现核心流程就是读文件头 → 二分查索引 → 跳转读记录。import struct class QQWry: def __init__(self, path): self.f open(path, rb) self.index_start, self.index_end struct.unpack(II, self.f.read(8)) staticmethod def ip2long(ip: str) - int: parts list(map(int, ip.split(.))) return (parts[0] 24) | (parts[1] 16) | (parts[2] 8) | parts[3] def lookup(self, ip: str): ip_long self.ip2long(ip) count (self.index_end - self.index_start) // 7 low, high 0, count - 1 candidate None while low high: mid (low high) // 2 self.f.seek(self.index_start mid * 7) cur struct.unpack(I, self.f.read(4))[0] if cur ip_long: candidate mid low mid 1 else: high mid - 1 if candidate is None: return 未知, 未知 self.f.seek(self.index_start candidate * 7) self.f.read(4) # 起始IP raw self.f.read(3) # 3字节偏移小端 offset raw[0] | (raw[1] 8) | (raw[2] 16) self.f.seek(offset) # 实际记录区解析需处理GBK编码、重定向标记等 country self._read_string() region self._read_string() return country, region def _read_string(self) - str: raw b while True: b self.f.read(1) if not b or b b\x00: break raw b return raw.decode(gbk, errorsreplace) def close(self): self.f.close()注意这里为了展示核心逻辑省略了一些细节。真实生产环境里记录区会有重定向标记比如0x01表示跳转到另一处读取、分组标记等直接用这个代码处理所有情况会漏数据。这也是为什么我建议学习原理时手写跑线上最好用成熟库。3.3 常用语言接入方式与性能实测如果你只是想快速接入不需要从零实现各语言都有维护较好的方案。语言推荐方式说明Pythonqqwry或ipdb封装了加载和查询支持内存模式Javaip2region或自写读取器与纯真格式兼容常用于Spring服务Go开源纯真解析包编译后无依赖适合微服务PHP网上有不少现成类库注意PHP的二进制读取和字符串处理性能方面我实测过把索引区加载进内存后单次查询耗时普遍在0.01ms到0.1ms之间即使不加载内存直接做文件读取也基本稳定在1ms以内。这个量级意味着一个普通8核服务器单机每秒处理几十万次查询都没有压力。相比在线API动辄几十毫秒的RT离线库在性能上的优势是压倒性的。4. 常见问题与排查技巧实录4.1 查询结果不准的典型原因IP归属查询不准首先要搞清楚一个前提除非有精确到基站的数据库否则互联网IP的归属本身就是一种“统计推测”不能当成GPS定位来看待。以下三种情况最容易造成结果偏差。运营商NAT和移动基站接入排在第一位。现在很多用户上网走的是运营商的大网NAT或者手机基站的动态地址池一个公网IP背后可能挂着几十上百个用户归属地显示出来的往往是网关或者基站所在的城市而不是用户实际所在的城市。第二个原因是数据库更新滞后。互联网IP段的分配一直在变化像云厂商、宽带运营商经常新增或调整地址段。如果数据库版本比较旧新分配的IP段就查不出来最终落到上一级或干脆返回未知。第三个原因是部分IP段本身的注册信息就模糊。有一些地址段在IANA和运营商侧的记录是笼统的甚至跨省调剂数据库再怎么整理也只能给出“合理猜测”。理解这三个原因之后你就不会对“偶尔不准”这个事过度苛责而是知道它属于这类离线库的固有特性。4.2 文件损坏、乱码与加载异常处理下载过程中文件截断是最常见的问题。表象是程序加载时报错或者查询结果全是未知。处理办法很直接重新下载核对文件大小和校验值。另一个常见问题是中文乱码原因基本是编码没指定对。纯真库里的字符串早期是按GBK编码的读取的时候一定要用GBK解码用UTF-8直接解当然会乱码。这也是很多新手第一次接入时最容易卡住的地方。内存方面如果服务内存紧张建议用mmap方式加载而不是一次性把整个文件读进内存。另外线上替换数据库文件时不要直接覆盖正在被进程读取的文件否则可能出现读取到半个文件内容的情况。稳妥做法是先下载到临时文件再完成原子替换然后触发进程内的热加载逻辑。4.3 更新节奏与数据质量控制更新节奏取决于业务对准确度的容忍度。统计类用途三个月更新一次完全够用安全风控类用途建议至少每月更新一次。我自己的做法是写个脚本每月执行一次。脚本做的事情包括下载最新文件、对比版本日期、抽样校验若干IP、把校验结果写入日志。全自动跑完才通知业务方切流量中间任何一步失败都会发送告警而不是静默失败。这套流程跑下来我几乎没有再收到过“IP库太旧”之类的反馈。5. 一些个人实操体会最后分享几个我踩过坑之后沉淀下来的经验。第一个体会是别把纯真库当成唯一数据源。在安全风控这类严谨场景里我倾向于用离线库做初筛再用在线服务对少量命中规则的IP做二次校验两者结合才能兼顾性能与准确率。第二个体会如果业务规模到了多实例部署不要把每个实例都独立加载一份.dat文件然后各自维护更新。更好的做法是把它做成一个独立的小服务对内提供查询接口统一加载、统一更新、统一监控。这样数据版本一致排查问题也省心。第三个体会是关于成本的认识。纯真库对个人和大部分商业项目都是免费的但免费不等于没有成本——更新维护、人工抽查、接入开发都是隐性成本。你在评估在线API的时候别只看报价格式要把这些隐形成本一起算进去结论有时候和直觉相反。能在一个领域坚持十八年本身已经说明了很多问题。纯真IP数据库用最朴素的文件格式解决了中文互联网里一个永恒的小问题这种“把一件小事做到极致”的思路值得我们在做技术选型时反复琢磨。