简介:一份基于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.dat
ipip.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", errors="replace").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", errors="replace") 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, workers=8): 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, f"error: {e}" with ThreadPoolExecutor(max_workers=workers) 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一次性提交全部 IP,as_completed边完成边收结果,避免等所有任务结束才一次性取回。int(ipaddress.ip_address(ip))比手写ip2long更稳健,它支持 IPv4 和 IPv6 的多种书写格式,遇到畸形输入会抛异常,正好被try-except捕获。
参数说明:workers=8是经验值。如果数据文件在机械盘上,线程多了反而会因为磁盘寻道变慢;如果是 SSD,可以提到 16。2019 年的库数据量不大,单条查询读文件时走的是随机读,线程数太高容易把磁盘 IO 打满。rows 输出时建议用errors="replace"容忍非法 UTF-8 字节,而不是直接中断任务。
4.2 把查询结果落成 CSV 分析表
with open("ip_region_result.csv", "w", newline="", encoding="utf-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", errors="replace") 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", errors="replace") 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 年的快照即使叫 datx,IPv6 段的数量也比 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 解析上翻过车。历史快照确实过期,但分析“它对应哪个时间点”这件事永远不过期。希望帮到你。
本文还有配套的精品资源,点击获取