简介一份基于2019年7月数据的全国IP地址库SQL数据包源于ip_ip.net定位为可供网络管理员、安全运维人员及数据分析师直接使用的离线IP情报数据源。资源包体积仅6.66MB内部包含1个SQL文件将IP地址、归属地、网络类型等关键信息按关系表结构组织可导入MySQL等数据库后通过标准SQL语句进行筛选、统计和关联减少对在线查询接口的依赖。利用该库网络管理员可批量识别恶意来源IP并完善访问控制规则网站运营者可分析访客地域分布以优化服务电信运营者能辅助完成路由规划执法或安全调查人员亦可用于定位网络行为的大致区域范围。目前已有236人学习下载是搭建本地IP地址库、开展日志分析或安全审计时值得收藏的基础工具。考虑到IP地址动态分配及隐私匿名处理的影响实际工程中建议结合业务场景和最新数据进行校验避免误判。1. 一份 2019 年的 IP 地址库快照为什么现在还有人找做日志分析、风控策略和数据治理的人手里没几个“最新ip地址库”心里是不踏实的。但“最新”是个动态概念——IP 段和运营商归属每个月都在变真正有价值的不是“某个时刻的最新”而是“可追溯的快照某个时间点的完整数据”。这份ip_ip.net 201907.zip本质就是某 IP 库在 2019 年 7 月的全量离线快照包含 IPv4 地址段、国家、省份、城市、运营商等维度适合做历史回溯、样本对比、离线批量解析。适合三类人手头日志量大、不想每次查询都走在线接口的运维需要给历史数据补 IP 归属维度的分析师以及想研究 IP 库格式和解析逻辑的开发者。2. 拆包与格式认知这 2019 年的 zip 里到底装了什么拿到 zip 的第一件事不是急着解压而是先看清单和校验值。压缩包的名字既然带了201907说明内部数据空间基准是 2019 年 7 月但压缩包的修改时间可能因为转手变过所以不要靠文件系统时间判断数据时间要看包内文档和 README 里写的 build 时间。2.1 解压与完整性校验mkdir ipdb_201907 cd ipdb_201907 unzip ../ip_ip.net\ 201907.zip ls -lh md5sum *dat* *.txt 2/dev/null || md5 ./*解压后对着文件清单核对文件数、字节大小和扩展名。MD5 这一步看起来多余但在团队协作或从网盘转存时能快速发现文件是否被截断。zip 里的文件如果缺失索引块或数据块解析时不会报“文件损坏”而是静默返回错误位置和乱码那才是最难排查的。2.2 文件格式是 dat / datx不是常见的 mmdb 或 qqwry.datipip.net 的常用离线格式有几种经典.dat、带 IPv6 支持的.datx、以及纯文本的.txt或.csv导出。这个包里大概率是.dat或.datx二选一两者结构并不兼容。.dat的记录是定长结构每条记录固定字段国家、省份、城市、ISP、备注按\t分隔后补齐固定字节.datx则在文件头多了一个版本段和索引段记录区支持变长字段和 IPv6 段。不管哪种格式IP 库的本质都是“段表”每一条记录不是一个 IP而是一个 IP 段start_ip ~ end_ip解析的时候要把目标 IP 转成整数然后二分查找落在哪个段里再取该段的属性字段。这个“段表 二分”认知能解决后面 80% 的坑。2.3 文件头探测先看清格式再写解析器with open(ipip.datx, rb) as f: head f.read(16) print(head.hex())如果前 4 字节是\x00\x01\x00\x00之类的小版本号后面跟着 4 字节 build 时间和 4 字节索引区偏移那就是 datx如果前几字节直接是 ASCII 可见字符串或固定长度字段区多半是经典 dat。我一般会先用hexdump -C看前 64 字节确认索引偏移值是否为 4 或 8 倍数再决定写哪种解析器。这一步判断错误后面所有查询结果都会错位。2.4 数据字典与字段顺序先建模再写查询无论 dat 还是 datx字段顺序一般是国家 / 省份 / 城市 / 区县 / 运营商 / 备注。但 2019 年的版本不一定带区县有的只有 5 个字段最后一个是空的。建议先用脚本读第一条记录按\t分割数一下字段个数确认是 5 字段还是 6 字段。我通常会把这个信息记下来作为解析器的参数避免后续在批量查询时因为字段数不一致而丢掉整行数据。3. 离线解析实战从 0 写一个能跑通的 IP 查询脚本很多人一上来就找“现成的解析库”但 2019 年的数据格式用现在官方最新 SDK 解析可能不兼容。更稳妥的做法是自己写一个最小解析器理解透之后再去封装。3.1 经典 dat 格式的二分查找解析import struct def load_dat_index(path): with open(path, rb) as f: # 前 4 字节记录数4 字节开始偏移后面就是索引 head f.read(8) count, start struct.unpack(II, head) f.seek(8) index [] for _ in range(count): s, e, off struct.unpack(III, f.read(12)) index.append((s, e, off)) return start, index def ip2long(ip: str) - int: parts ip.split(.) return (int(parts[0]) 24) (int(parts[1]) 16) (int(parts[2]) 8) int(parts[3]) def find_location(long_ip, start, index, data_path): lo, hi 0, len(index) - 1 while lo hi: mid (lo hi) // 2 s, e, off index[mid] if s long_ip e: with open(data_path, rb) as f: f.seek(start off) raw f.read(128) # 按 \t 分割定长记录遇到 \x00 截断 fields raw.split(b\t) return [x.decode(utf-8, errorsreplace).strip(\x00) for x in fields[:5]] elif long_ip s: hi mid - 1 else: lo mid 1 return None逻辑说明先把整个索引区读入内存每条索引占 12 字节包含段起始 IP、段结束 IP 和该段在数据区的偏移。查询目标 IP 转成整数后走二分查找命中段后去数据区按偏移读一条记录。read(128)是保守读取实际记录定长可能更短但要确保多读不越界字段按 Tab 分隔后做 UTF-8 解码容忍坏字节。参数说明II/III是 Python struct 模块的小端无符号整数写法。这里的偏移off是相对数据区起始位置的偏移不是文件绝对偏移所以读取时要加上start。如果你的库是 5 字段fields[:5]就够如果是 6 字段记得改成[:6]否则备注列会漏掉。3.2 datx 格式的解析多一个版本段和索引段import struct def load_datx(path): with open(path, rb) as f: magic, version, build, index_offset struct.unpack(IIII, f.read(16)) f.seek(index_offset) count struct.unpack(I, f.read(4))[0] index [] for _ in range(count): s, e, off struct.unpack(III, f.read(12)) index.append((s, e, off)) return build, index def query_datx(long_ip, build, index, path): lo, hi 0, len(index) - 1 while lo hi: mid (lo hi) // 2 s, e, off index[mid] if s long_ip e: with open(path, rb) as f: f.seek(off) raw f.read(256) return raw.split(b\t) elif long_ip s: hi mid - 1 else: lo mid 1逻辑说明datx 的头部通常固定 16 字节读出索引偏移后跳到索引区第一条是记录总数后面是每条 12 字节的段索引。数据区的“偏移”这里就是文件绝对偏移和经典 dat 相对偏移不同别混用。查询命中后直接读 256 字节原因同前宁可多读别少读字段用 Tab 切分最稳。参数说明build是数据版本号可以打印出来做日志溯源。raw.split(b\t)返回的是字节数组后续需要逐字段 decode。如果记录里包含\x00填充要在 decode 之前先rstrip(b\x00)否则字段尾部会出现不可见字符写 CSV 时会带进\x00干扰后续处理。3.3 最小可用的命令行查询工具import sys def main(): path sys.argv[1] ip sys.argv[2] build, index load_datx(path) result query_datx(ip2long(ip), build, index, path) if result: print(|.join([x.decode(utf-8, errorsreplace) for x in result[:6] if x.strip(b\x00)])) else: print(not found) if __name__ __main__: main()这个工具只做一件事输入一个 IP输出归属字段。生产环境我一般不会直接用它而是把它拆成函数导入到批处理脚本里。这里专门写出来是为了让你先验证数据文件本身有没有问题——如果随便查一个本机 IP 都返回乱码问题多半出在文件头解析或偏移计算而不是查询逻辑。4. 批量查询与多维应用把 IP 字段变成分析资产单条查询可以手动验证但真正有价值的是批量跑日志、跑订单表、跑用户表把 IP 字段变成“地域维度”和“出口类型维度”。这一步要处理的是吞吐量和异常容忍问题。4.1 用线程池做批量查询from concurrent.futures import ThreadPoolExecutor, as_completed import csv, ipaddress def batch_query(ip_list, build, index, path, workers8): results {} def one(ip): try: long_ip int(ipaddress.ip_address(ip)) r query_datx(long_ip, build, index, path) return ip, r except Exception as e: return ip, ferror: {e} with ThreadPoolExecutor(max_workersworkers) as pool: future_map {pool.submit(one, ip): ip for ip in ip_list} for future in as_completed(future_map): ip, r future.result() results[ip] r return results逻辑说明ThreadPoolExecutor一次性提交全部 IPas_completed边完成边收结果避免等所有任务结束才一次性取回。int(ipaddress.ip_address(ip))比手写ip2long更稳健它支持 IPv4 和 IPv6 的多种书写格式遇到畸形输入会抛异常正好被try-except捕获。参数说明workers8是经验值。如果数据文件在机械盘上线程多了反而会因为磁盘寻道变慢如果是 SSD可以提到 16。2019 年的库数据量不大单条查询读文件时走的是随机读线程数太高容易把磁盘 IO 打满。rows 输出时建议用errorsreplace容忍非法 UTF-8 字节而不是直接中断任务。4.2 把查询结果落成 CSV 分析表with open(ip_region_result.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([ip, country, province, city, isp, raw]) for ip, r in results.items(): fields_dict [ip] if isinstance(r, list): fields_dict [x.decode(utf-8, errorsreplace) for x in r[:5]] else: fields_dict.append(r) writer.writerow(fields_dict)这里要把查询异常的数据也写进 CSV不能静默丢弃。如果某一天查询结果集中出现大量error不是库坏了就是 IP 格式脏了。CSV 用newline防止空行编码用utf-8保存后给 Excel 或 BI 工具读取都不会乱。4.3 做一张“出口类型分布”透视表import collections counter collections.Counter() for ip, r in results.items(): if isinstance(r, list) and len(r) 5: isp r[4].decode(utf-8, errorsreplace) counter[isp] 1 print(counter.most_common(20))这一步能快速看出你的用户或日志来源里哪些是家庭宽带、哪些是数据中心出口、哪些是政企专线。比如异常流量排查时如果某时段突然出现了大量数据中心类型的 IP说明大概率是爬虫或脚本批量操作而不是真实用户。这比光看 IP 数量有意义多了。5. 避坑指南2019 年 IP 库的六个典型翻车现场这一类“老库 新需求”的场景里坑几乎是确定性的只是时间早晚问题。5.1 数据新鲜度快照是 2019 年的不代表现在还能精准定位现象用这份库解析某个 2024 年新申请的云厂商 IP返回的归属地和实际不符甚至省份都对不上。原因IP 段是动态分配的运营商、云厂商会持续回收和重新规划地址空间。2019 年的段表里很多段到了 2024 年已经换主人了。解决明确这是历史快照只用于回溯性分析不做实时风控拦截。如果要支撑现状业务必须另买或另找更接近当前时间的更新库。我一般会把这类历史库标记为historical单独存一个目录避免和当前库混淆。5.2 dat 和 datx 解析器用反了现象查询结果里省份字段出现大段二进制乱码或每一条的返回内容都一样。原因用经典 dat 的偏移逻辑去解析 datx索引偏移错位后加载的 index 全部指向了错误位置。解决写一个小脚本先探测文件头确认格式后再进入解析流程。前面第 2.3 节的head.hex()就是干这个用的。生产环境里我通常加一个配置项db_format: datx不让代码自动猜格式猜错了会很难查。5.3 字节序和prefix位数搞反现象同一个 IP 用两个解析函数查结果一个返回北京一个返回广东肉眼都看得出不对。原因IP 段索引的起始值和结束值在小端序与大端序之间切换后数值会完全错位另外有些库记录的是“起始 IP 段长度”不是“起始 IP 结束 IP”。两种结构之间只差一个减法但结果天差地别。解决先拿三个已知地址验证本机出口 IP、一个海外 IP、一个保留地址。如果三个全对基本可以排除字节序和索引计算问题。验证脚本单独存成一个文件改一次解析器就重跑一次这是血泪教训换来的习惯。5.4 定长记录里有\x00填充现象字段末尾出现不可见空字符导出的 CSV 在 Excel 里看是正常在 MySQL 里LOAD DATA后字段尾巴多了空格或\0导致GROUP BY时对不上。原因定长记录为了补齐字节会在字段尾填充\x00解析时没做 trim。解决字段 decode 前加一步rstrip(b\x00)稳妥起见再strip()一次去掉空格。我发现这个坑在所有定长 IP 库解析里几乎都会遇到建议封装成公共函数。5.5 字段数不一致永远不要写死 5 个字段现象一条记录返回 6 个字段另一条返回 5 个字段直接按下标取第 6 个字段时抛 IndexError。原因IP 库的字段是“随着版本不断加列”的2019 年 7 月的版本可能是 5 字段但如果打包时混入了更新版本的导出文件就会漂移。解决解析后先判断len(fields)少于目标字段数就补空字符串多于就截断到目标字段数。这样在批量处理时即使字段漂移也不会中断任务日志里打出field_count_mismatch告警就行。5.6 IPv6 是盲区别硬刚现象输入 IPv6 地址查询结果为空或报错。原因2019 年的快照即使叫 datxIPv6 段的数量也比 IPv4 少一个数量级很多国家的 IPv6 段根本没有覆盖。解决业务上先区分“IPv4 查询”和“IPv6 查询”IPv6 走单独通道。如果当前业务必须支持 IPv6就把这份库当作“查不到即未知”的兜底另外准备一份近期的 IPv6 库做交叉。不要自己尝试在旧库上改代码强行支持那不是解析问题是数据缺失。6. 进阶技巧把老旧快照变成数据对账的标尺前面都在讲怎么“查”最后讲讲怎么把这份历史快照用出新价值。6.1 做“IP 段漂移对账”# 把两份库的 IP 段各自导成段表 # 一份是 2019 快照一份是当前库任选其他源 # JOIN 区间段找出 start_ip 相同但 end_ip 或归属字段不同的段对比 2019 年与当前数据后凡是start_ip能对上但end_ip变了的说明该段发生过拆分或合并归属字段变了的说明该段被重新分配过。这份 diff 列表可以用来做“IP 变化速率分析”变化越剧烈的区域数据时效性越短在风控模型里给这些区域的 IP 线索降权。6.2 把索引加载进内存做高速镜像解析库每次查询都要打开文件读数据虽然对单条查询无所谓但千万级日志处理时磁盘 IO 会成为瓶颈。常见做法是启动时把索引段和数据段一次性加载到内存里构建一个list 二分查找的镜像常驻进程里查询。class IpMirror: def __init__(self, path): build, index load_datx(path) with open(path, rb) as f: self.data f.read() # 一次性读入文件字节 self.build build self.index index def query(self, long_ip): lo, hi 0, len(self.index) - 1 while lo hi: mid (lo hi) // 2 s, e, off self.index[mid] if s long_ip e: raw self.data[off:off 256] return raw.split(b\t) elif long_ip s: hi mid - 1 else: lo mid 1这段代码把 datx 的整个文件读进内存查询时直接切字节切片完全绕开磁盘 IO。内存占用方面2019 年的 IPv4 库一般在 100MB 到 200MB 之间可以接受。生产进程启动时加载一次之后每次查询都是纯内存操作吞吐量能提升一个量级。6.3 给快照建立“数据血统”自述文件我建议在解压目录里放一个新README.md记录数据时点、原始 zip 名称、MD5、解压时间、解析器版本、已验证过的样本 IP 列表。这样将来任何人拿到这份数据都能知道它是谁、从哪来、验证过没有。这些内容应当落成文件随数据一起留存而不是事后靠记忆。经过这次拆包我养成了一个习惯任何 IP 离线库拿到手都会先做三件事——数文件头、跑已知 IP 验证、写血统文件从那以后基本没在 IP 解析上翻过车。历史快照确实过期但分析“它对应哪个时间点”这件事永远不过期。希望帮到你。本文还有配套的精品资源点击获取