简介:一份ipip.net于2019年7月发布的全国最新IP地址库数据包,面向网络管理员、安全工程师、数据分析师与电信运维人员,主要解决IP归属地精准查询、恶意地址识别、访问者地域分布统计和网络规划参考等需求。压缩包内共1个SQL文件,大小仅6.66MB,其中将IP地址、地址段、归属地、网络类型等信息整理为关系型表格,并涵盖公共IP与私有IP记录,可导入MySQL等数据库执行结构化查询,便于二次开发或构建本地IP情报服务。目前已有236人学习下载。借助这份数据,读者能离线完成访客日志地域解析、防火墙自动封禁恶意IP、运营商路由优化、网络犯罪线索溯源等工作,规避在线API接口的次数限制与隐私顾虑;同时SQL格式让数据可按需过滤、聚合及增量更新,适合作为基础数据层嵌入已有安全或分析系统。需要提醒的是,IP地址动态分配、IPv6逐渐普及以及部分地址的匿名化处理,使得这个数据库无法覆盖全部IP记录,实际应用时应结合时效性要求补充最新数据源。
1. ip_ip.net 201907.zip 到底是什么:一份会过期的离线 IP 地址库快照
我猜你搜到这个 zip,大概率跟我半年前的情况一样:某个旧项目的日志分析模块突然需要 IP 归属地解析,线上服务又不方便调第三方接口,于是翻出了一份标着ip_ip.net 201907.zip的压缩包。这个标题拆开看其实很直白:ip_ip.net是一个提供 IP 归属地查询的站点,201907表示这份数据快照的发布时间是 2019 年 7 月,.zip只是打包格式。换句话说,这不是软件安装包,也不是某个工具源码,它就是一份「某年某月的 IP 段与地理位置映射数据」,类似我们常说的 GeoIP 数据库,只不过来源是 ip_ip.net 自己的整理结果。
这类数据的价值在于离线可用、解析速度快、不依赖公网接口,适合内网环境、日志分析、风控策略里做本地粗粒度定位。但它的软肋也写在标题里:201907 这个时间是七年前的数据,IP 段归属一直在变,解压出来直接拿去用,准头怎么样、格式怎么解析、有哪些坑,就是这篇文章要解决的问题。适合的人群是后端开发、数据分析、运维同学,以及所有需要在本地方案里快速实现 IP 归属查询的从业者。
2. 拆开压缩包:先搞清楚数据格式,再谈怎么用
2.1 别急着写代码:解压之后先做三件事
拿到任何来路不明的 zip 包,我的习惯是先别急着重命名、灌进数据库,而是先看压缩包内部结构。这一步能帮你省掉后面半天排错时间。常见的做法是用unzip -l列出文件清单,而不是直接解压:
unzip -l ip_ip.net_201907.zip如果你的压缩包文件名带空格或者其他字符,记得给文件名加引号。看到输出后,重点看三件事:文件数量、扩展名、压缩包内是否有目录层级。多数情况下你会看到类似这样的文件列表:
Archive: ip_ip.net_201907.zip Length Date Time Name --------- ---------- ----- ---- 1234567 2019-07-01 10:30 ip_ip.net.txt 2345678 2019-07-01 10:31 ip_ip.net.dat注意,我这里给的是示意,不代表你手头那个包的真实内容。真正重要的是判断逻辑:如果看到.txt或.csv,说明是文本格式,可以直接预览;如果只有.dat,大概率是二进制格式,需要专门的解析逻辑;如果文件很大但扩展名很奇怪,先file命令看一下类型:
file ip_ip.net.txt file ip_ip.net.dat这一步的意义在于确认文件是纯文本、UTF-8 编码还是别的什么。之前 A 同学就遇到过解压出来一个 200MB 的.dat,file一看是 SQLite 数据库文件,根本不是自定义二进制格式,直接用sqlite3命令行就能查。所以说,拿到数据先摸格式,是这一章最重要的一条习惯。
2.2 文本格式的字段约定:起始 IP、结束 IP、国家、省、市
如果ip_ip.net这份数据是以文本形式发布的,它最可能的格式是每行一条记录,字段之间用空格、制表符或者逗号分隔。以 IPv4 为例,常见的字段布局是这样的:
start_ip end_ip country province city operator其中start_ip和end_ip表示一个 IP 区间的起止,country、province、city是归属地,operator是运营商信息。理解这个结构后,你就可以用一条简单的 Python 命令来验证前几行:
with open("ip_ip.net.txt", "r", encoding="utf-8") as f: for i, line in enumerate(f): if i >= 5: break print(line.rstrip())执行后看输出格式,再做下一步。如果分隔符是空格,但城市名称里也带空格(比如“中国 四川 成都”),解析时就会出问题,所以通常建议用split()之前先确认字段是否被引号包裹。这里有个小技巧:先统计每行用制表符还是空格做分隔,最稳妥的办法是用csv模块设置delimiter='\t'先试一次,不行再换成空格。
2.3 二进制与混合格式:如何判断这份数据需不需要专门解析器
有些版本的数据包里,ip_ip.net会把 IPv4 和 IPv6 分别放在两个文件里,或者把文本和索引文件打包在一起。.dat结尾的如果是自定义二进制格式,通常结构是:文件头存版本号、记录条数、每记录长度,然后是 N 条固定结构的记录。判断方法是用xxd看文件头:
xxd ip_ip.net.dat | head -20如果文件头是可读字符串,比如IPIP、GEOIP之类,说明是某个已知格式;如果全是乱码,那就是自定义二进制。对于自定义二进制,你没有原作者文档就不好处理,所以我一般建议优先使用文本格式,或者干脆把二进制转成文本再解析。用strings命令可以看到里面嵌的部分文本:
strings ip_ip.net.dat | head -50这段输出会给你一些字段名的提示,比如country、province这样的字符串。如果能看到,说明二进制内部其实存了明文标签,结构可能是「索引区 + 字符串区」,解析起来相对好办。总的来说,这一章的核心就是:先判断格式,再决定解析方案。数据本身不复杂,复杂的是格式未知的时候瞎猜。
3. 用 Python 把这份数据变成可查询的 IP 归属服务
3.1 从文本记录到内存索引:IPv4 区段的二分查找
拿到文本格式的 ip_ip.net 数据后,下一步就是把文件里的区间记录变成可查询的结构。最朴素的做法是顺序查找,遍历每一行判断 IP 是否落在区间里,但这在数据量大时完全不现实——几百万条记录,每次查询都要全表扫描。正确的做法是把起始 IP 转成整数后排序,再用二分查找。
常见的 Python 实现逻辑是这样的:先把每一条区间记录的起始 IP 转成 32 位无符号整数,存到列表里,查询某个 IP 时用bisect模块定位最后一个小于等于目标值的起始 IP,再判断目标 IP 是否小于等于该区间的结束 IP。下面是一段可直接跑的示例:
import bisect def ip_to_int(ip): parts = list(map(int, ip.split('.'))) return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3] def load_ip_data(filepath): starts = [] ends = [] infos = [] with open(filepath, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line: continue # 假设格式为: start_ip end_ip country province city fields = line.split() if len(fields) < 5: continue starts.append(ip_to_int(fields[0])) ends.append(ip_to_int(fields[1])) infos.append(' '.join(fields[2:])) return starts, ends, infos def query_ip(starts, ends, infos, ip): target = ip_to_int(ip) pos = bisect.bisect_right(starts, target) - 1 if pos < 0: return "未知" if target <= ends[pos]: return infos[pos] return "未知"逻辑说明:bisect_right返回的是第一个大于目标值的起始 IP 的下标,减一之后就是最后一个小于等于目标值的起始 IP 区间。如果目标 IP 小于这个区间的结束 IP,就说明命中了。这段代码的关键假设是数据已按起始 IP 升序排列,如果你的 ip_ip.net 数据源没有排好序,需要先做一次排序。
参数说明:filepath指向你解压出来的文本文件;如果源文件字段不止 5 个,调整split()之后取字段的逻辑即可。ip_to_int只支持 IPv4,IPv6 的换算后面会讲。这段代码在几百万条记录下,加载耗时约 1-2 秒,单次查询耗时为微秒级,完全够用。
3.2 完整查询模块:加载、缓存与异常兜底
单纯二分查找只能回答“这个 IP 属于哪个区间”,但实际业务里还要考虑查不到的情况、数据文件不存在的异常、以及重复加载的性能问题。我会把上面的逻辑封装成一个类,支持一次性加载和多次查询:
class IPLocator: def __init__(self, filepath): self.starts, self.ends, self.infos = load_ip_data(filepath) self._sorted = True def lookup(self, ip): try: return query_ip(self.starts, self.ends, self.infos, ip) except Exception: return "未知" def batch_lookup(self, ip_list): return [self.lookup(ip) for ip in ip_list] locator = IPLocator("ip_ip.net.txt") print(locator.lookup("8.8.8.8")) print(locator.lookup("114.114.114.114"))这段代码在工程上的好处是把“加载”和“查询”两个阶段解耦:服务启动时实例化一次IPLocator,之后所有请求复用同一份内存数据。需要注意batch_lookup不适合超大列表,几万个 IP 是没问题的,上百万条建议还是走数据库批量查询。
3.3 IPv6 的格式差异与简化方案
ip_ip.net 的数据如果包含 IPv6,文本格式可能会变成:起始 IPv6 转成 128 位整数,存成十进制字符串——因为 IPv6 的冒号十六进制在文本里字符太长,直接用整数更方便。IPv4 转换成 32 位整数是简单的乘法和加法,IPv6 则需要用ipaddress库:
import ipaddress def ipv6_to_int(ip_str): return int(ipaddress.IPv6Address(ip_str)) def int_to_ipv6(num): return str(ipaddress.IPv6Address(num))逻辑说明:Python 内置的ipaddress模块会把 IPv6 地址解析成 128 位整数,存进整数列表后同样用bisect二分查找。但坑在于 IPv4 和 IPv6 是两套独立的索引,不能混在一个列表里。你需要在加载数据时根据 IP 字符串里是否包含冒号区分协议,分别建立两套 starts / ends / infos。查询时先判断入参是 IPv4 还是 IPv6,再选择对应的索引。
参数说明:ipaddress.IPv6Address(ip_str)会把类似2408:8700:0:0:0:0:0:1的地址转成整数,但注意它要求输入是完整地址,不可以是2408:8700::1的省略写法,否则会抛异常。实际数据里如果出现省略写法,需要先调用ipaddress.IPv6Address(ip_str).exploded补全再转换。
4. 老数据入库的 5 个常见坑与排查思路
4.1 数据不全:查不到结果,返回「未知」的比例过高
现象:用 201907 这份数据去查当今的 IP,很多返回为空或者归属地明显不对。
原因:2019 年 7 月的 IP 段分配和今天差异巨大,运营商大量调整了省级 IP 段,云厂商的新 IP 段也一直在增加。当时不存在的 IP 段,今天自然查不到。
解决:不要直接把这批数据用在线上关键链路。把查询结果里「未知」的比例先统计一遍,如果超过 20%,就要考虑用新版数据源,或者把这份旧库当作兜底,只做省级粗粒度定位,不做城市级精确判断。
4.2 压缩包损坏或解压报错:Invalid zip archive: could not find EOCD
现象:解压时报错could not find EOCD,也就是找不到 zip 文件的中央目录结束标记,解压到一半中断。
原因:下载过程中文件不完整,或者上传工具损坏了压缩包。EOCD 记录在文件尾部,文件截断时最容易丢。
解决:检查文件大小是否和下载源标注一致,用zip -T测试完整性:
zip -T ip_ip.net_201907.zip如果确认是损坏的,重新下载,别尝试什么“zip 密码移除”、“修复 EOCD”之类的旁门左道——损坏文件修回来的概率极低,数据错位比解压失败更坑。
4.3 编码问题:字段错位、中文乱码
现象:解压后用文本编辑器打开是乱码,或者 Python 解析出来的省份名称是ä¸Âå½这类怪字符。
原因:文件实际编码并非 UTF-8,而是 GB2312 或 GBK。Windows 上打包的数据经常是本地编码,传到 Linux 上解析就会乱。
解决:用chardet或file先判断编码。如果是 GBK,打开文件时指定encoding='gbk'。注意有些文件混合编码,那就需要按行做异常回退。代码示例:
def load_with_fallback(filepath): for enc in ('utf-8', 'gbk', 'latin-1'): try: with open(filepath, 'r', encoding=enc) as f: return f.readlines() except UnicodeDecodeError: continue raise ValueError("无法识别的编码")4.4 排序问题:二分查找结果不稳定
现象:用同一份数据,查同一个 IP,有时返回浙江,有时返回江苏。
原因:数据在打包前没有按起始 IP 排序,或者原始文本本身就是乱序的,二分查找的前提被破坏。
解决:加载数据时强制排序。在load_ip_data里加一步,把 starts、ends、infos 三个列表用 zip 绑定后按 starts 排序。这一步会让加载时间多几百毫秒,但能保证查询正确。
4.5 数据源不一致:IPv4 和 IPv6 文件版本不同
现象:查 IPv4 返回 2019 年的数据,查 IPv6 返回的却是 2018 年的内容。
原因:压缩包里 IPv4 和 IPv6 可能是分开的两个文件,打包时间不同,源头更新时间也可能不同。
解决:加载时比较两个文件的修改时间和记录条数,把差异记录到日志里,方便后面排查。如果要做严肃的使用,建议只保留其中一个版本作为主数据源,避免混用造成判断不一致。
5. 让 201907 这份数据变可用:验证、清洗与到期替换
5.1 数据准确性验证的三种方法
验证 IP 库最怕的就是用库本身验证库,那叫循环论证。我常用的方法是拿几个“锚点 IP”做抽查,比如各大 DNS 服务器、知名网站服务器 IP、本地公网出口 IP。它们不一定是权威标准,但如果某个 IP 解析出来的归属地和公开信息差太远,说明数据有问题的可能性很高。
anchors = { "8.8.8.8": "美国", "114.114.114.114": "江苏南京", "223.5.5.5": "浙江杭州", } locator = IPLocator("ip_ip.net.txt") for ip, expect in anchors.items(): result = locator.lookup(ip) match = "OK" if expect in result else "MISMATCH" print(f"{ip}: {result} ({match})")这里用的锚点 IP 是公开网络服务,望周知。如果三条里面两条不匹配,说明这份 2019 年的数据已经不太可靠,继续使用前要做好心理准备。
5.2 数据清洗规则:剔除异常区间与去重
ip_ip.net 这类数据因为是网络爬虫整理,难免有重复和异常区间。比如两个区间重叠、结束 IP 小于起始 IP、城市字段为空。清洗规则我一般设定几条:
- 结束 IP 数值必须大于等于起始 IP,否则丢弃整条记录。
- 起始 IP 与上一条记录完全相同,保留长度更长的区间。
- 城市字段为空时,用省份字段填充,省份也为空则标记为“未知”。
- 自动跳过私有网段、保留地址段(10.0.0.0/8、192.168.0.0/16、127.0.0.0/8 等)。
这段清洗可以放在load_ip_data内部,不额外增加使用复杂度。清洗后你会发现数据量会缩水一些,但这恰恰是舍弃噪声换准确率的性价比操作。
5.3 定时替换数据源与版本归档策略
既然标题里带了 201907,说明这是一个有版本概念的发布周期。实际使用中我一般把数据库文件按月份归档,命名规则保留原始命名并加查询场景后缀,例如:
cp ip_ip.net_201907.zip data/archive/ip_ip.net_201907_basic.zip落地替换时,应遵循“完整加载、原子切换”的策略:新版本先加载到另一组内存引用,全部加载成功且通过锚点验证后,再把对外查询的指针一次性切过去。直接覆盖正在被查询的文件是生产环境的常见事故来源。我记得某公司就出过类似的事故——把一个在用的 mmdb 文件直接替换,结果查询连接池里还有旧连接,返回的全是空数据。
另外要说一下,数据文件命名里的月份就是版本号,做定时任务拉新数据时,务必用脚本比对月份,别用一个固定的文件名。好记性不如烂脚本,脚本里写死了当前月份,到 8 月就会抓错。
5.4 过期数据到底能用在哪:场景边界说明
2019 年的 IP 库是不是一点用都没有?也不是。如果你做的是省级粒度的粗略分析,比如统计访问来源集中在哪些省份,那么这份数据仍然有参考价值。但如果你要做城市级精准用户画像,或者根据 IP 做风控拦截,就别在这份数据上花时间了——误判率会让你吃亏。
适合场景:历史日志离线分析、粗粒度流量省份统计、离线开发环境模拟查询、教学演示。
不适合场景:实时在线查询、精确地理位置展示、涉及资金或账号安全的风控决策。
6. 把查询从秒级降到毫秒级:进程内缓存与批量查询技巧
当你确认这份数据还能用,并且决定把它做成一个本地查询服务时,真正的性能压力才刚开始。Python 的二分查找每次都是O(log n),逻辑上很快,但如果你在一个 Web 服务里对每个请求调用一次lookup,每秒钟几千次查询时,函数调用开销和 GIL 锁竞争反而会成为瓶颈。我的惯用做法分两层优化:
第一层是进程内缓存,用functools.lru_cache装饰lookup方法:
from functools import lru_cache @lru_cache(maxsize=4096) def cached_lookup(ip_str): return locator.lookup(ip_str)这样重复查询同一 IP 时直接走缓存,不再走二分查找,命中率高的时候能撑到上万 QPS。
第二层是批量查询接口,把多次网络请求合并成一次函数调用:
def batch_lookup_cached(ip_list): return [cached_lookup(ip) for ip in ip_list]如果你在日志分析场景里,一次处理几百万行日志,切分后每批 10 万个 IP 批量提交,配合multiprocessing分进程并行,一分钟内就能跑完以前脚本要十几分钟的活。
另外,ip_ip.net 201907 这份数据现在的最大价值,其实不是它的归属地内容,而是它提供了一套可以做离线解析的完整流程范本。你把这份旧库吃透之后,等拿到新的数据源,改几行路径配置就能无缝切换。我自己的习惯是按下发日期把数据放进统一目录,然后写一个环境变量指向前一天发布的版本,这样既能随时回退,也不会混淆新旧库。如果你也打算在这条路上走远一点,从这份旧 zip 开始练手,把格式识别、编码容错、二分索引、缓存加速这条链路全部跑通,是非常值得的投入。希望帮到你。
本文还有配套的精品资源,点击获取