IP地址离线快照解析:导入MySQL、ClickHouse与Redis实战
简介ip_ip.net 201907.zip 是一份收录2019年7月全国IP地址分配信息的离线数据库面向网络管理员、安全研究者、数据分析师等需要批量查询IP归属地与网段信息的用户。压缩包内共一个SQL文件体量约六点六六兆字节表结构通常包含IP起始地址、结束地址、归属省份、城市、运营商等字段可导入MySQL等关系型数据库后按需筛选、关联与统计。利用这份数据可进行恶意IP来源识别、网站访客地域分析、电信路由规划辅助以及网络犯罪溯源等实践通过编写SQL查询或导入分析工具还可快速抽取特定城市或运营商的IP段。目前已有236人学习下载适合具备SQL基础操作能力的中级及以上技术人员作为离线参考。使用时应留意IP动态分配和IPv6普及带来的覆盖局限部分隐私保护地址可能已被匿名化处理。1. 拿到 ip_ip.net 201907.zip 之后先搞清这份快照能做什么深夜值班收到一条告警某个境外 IP 在反复试探登录接口。手头没有在线查询条件唯一能用的是一份标着 201907 的 IP 快照压缩包。解压、查归属、判断机房还是住宅、决定要不要封禁整个过程必须在几分钟内完成——这份 ip_ip.net 201907.zip 解决的就是这类「离线状态下快速判断 IP 归属地、运营商和网络类型」的需求。它本质上是某个 IP 地址查询服务在 2019 年 7 月导出的归属数据快照覆盖了 IP 段与地理位置、运营商、网络属性的映射关系适合日志分析、风控规则、访问控制和网络排障场景。凡是需要在没有外部接口依赖的环境里做 IP 定位的人都能从这份文件里拿到可用的基础数据。拿到压缩包后最忌讳直接写代码开搞。先花两分钟把文件本身摸清楚——格式决定了解析方式解析方式决定了后面所有查询逻辑的写法。本文从文件识别讲起一路走到导入 MySQL、ClickHouse 和 Redis 三种存储引擎的具体命令再把高频踩坑点逐个拆开最后给一个验证数据新鲜度的增量比对脚本。全程假设你只有这一个 zip 文件没有配套文档也没有网络连接。2. 解包与格式摸底不急着写代码先把 zip 里装的是什么看清楚2.1 用 unzip 和 file 先确认压缩包内部结构与文件类型拿到压缩包第一件事不是解压而是先看压缩包列表。很多 IP 快照 zip 里不止一个文件可能同时包含 IPv4 和 IPv6 两个数据集或者带一个 README 说明字段含义。先列出来再决定要不要全部解压。unzip -l ip_ip.net_201907.zip file ip_ip.net_201907.zipunzip -l输出的文件清单里重点看两个信息文件名后缀和压缩前后的体积比例。后缀是.csv、.txt还是.dat决定了后续用什么方式读取体积比能侧面反映数据量级——如果压缩前有几十 MB解压后可能是几百 MB 的文本导入数据库时要提前评估磁盘和内存。file命令会额外给出文本编码信息如果是ISO-8859或GBK后面读取时要手动指定字符集否则中文城市名大概率乱码。确认完清单再解压unzip ip_ip.net_201907.zip -d ./ipdata ls -lh ./ipdata解压到独立目录是习惯问题主要是为了避免和项目代码混在一起。解压后立刻看文件大小和行数这两个数字直接告诉你后面建表时该用什么样的存储参数wc -l ./ipdata/*2.2 看样本行判断分隔符、IP 格式和字段数量解压完成后不要急着写完整解析脚本先用head看几行样本这一眼能省掉后面大量的重写时间。head -n 5 ./ipdata/ipv4.txt样本行通常会暴露三个关键信息字段分隔符是逗号、制表符还是竖线IP 地址是点分十进制1.2.3.0还是整数形式16909056每行字段数量是否一致。这三个信息直接决定解析脚本怎么写。比如字段分隔符如果是制表符后面用cut -d ,就会全部失效IP 如果存的是整数拿来直接转字符串再存库就是多此一举。看完样本后用awk检查字段数一致性这一步容易被忽略但非常重要——混合格式的行会导致导入时批量失败awk -F , {print NF} ./ipdata/ipv4.txt | sort | uniq -c-F ,指定分隔符为逗号NF是当前行的字段数sort | uniq -c统计每种字段数出现的次数。正常情况只应该输出一行。如果出现两种以上的字段数说明文件里混入了脏行需要在导入前单独处理。2.3 三种常见行格式及对应的预处理路径根据处理过的多份 IP 快照行格式基本逃不出下面三种。第一种是start_ip,end_ip,country,province,city,isp,owner每个字段用逗号分隔IP 是点分十进制第二种是ip_start,ip_end,country_code,region,city,latitude,longitude多出经纬度但没有运营商第三种是简化版network,cidr,country,city,isp用 CIDR 表示法代替起止 IP 段。三种格式对应三种不同的预处理策略。点分十进制转整数最麻烦但最通用整数形式可以直接用但要注意无符号类型CIDR 需要先解析出网络地址和掩码长度然后换算成起止 IP。无论哪种格式最终目标都是统一成ip_start_int, ip_end_int, country, province, city, isp的结构化数据方便后续导入和查询。# 以逗号分隔、点分十进制 IP 为例转成整数并输出统一格式 awk -F , {split($1,a,.); split($2,b,.); sa[1]*16777216a[2]*65536a[3]*256a[4]; eb[1]*16777216b[2]*65536b[3]*256b[4]; print s,e,$3,$4,$5,$6} ./ipdata/ipv4.txt ./ipdata/ipv4_normalized.csv注意这行脚本假设原始文件前六个字段分别是起始 IP、结束 IP、国家、省份、城市、运营商。如果字段顺序不同需要对照步骤 2.2 里的样本行调整$N的位置。整数转换公式a*256^3 b*256^2 c*256 d是 IPv4 转整数的标准做法后面 SQL 查询也要用同一个公式保持前后一致。3. 查询逻辑的核心IP 段为什么必须转成整数做区间匹配3.1 点分十进制不能直接比较区间查找依赖整数排序把 IP 转成整数不是多此一举而是因为点分十进制没法做高效的范围查询。192.168.1.1和192.168.1.100按字典序排序时1.100会排在1.2前面完全打乱 IP 段的连续性。转成整数后3232235777和3232235876就是普通数字可以直接用BETWEEN或二分查找定位。3.2 用临时表验证查询逻辑先跑通再上生产导入正式库之前先用临时表验证查询逻辑是最省事的做法。以 SQLite 为例也可以用本地临时 MySQL 库建一张表和正式库结构一致导入少量数据验证BETWEEN查询是否返回正确结果。CREATE TABLE ip_temp ( ip_start INTEGER UNSIGNED, ip_end INTEGER UNSIGNED, country TEXT, province TEXT, city TEXT, isp TEXT ); -- 假设查 8.8.8.8 的归属 SELECT country, province, city, isp FROM ip_temp WHERE 134744072 BETWEEN ip_start AND ip_end;134744072是8.8.8.8的整数形式可以用 Python 或任意脚本快速算出。查询逻辑的核心是WHERE 目标IP BETWEEN ip_start AND ip_end这个条件依赖ip_start和ip_end组成的连续区间所以数据导入前必须确认每一行的ip_start ip_end否则会出现查不到或查错的结果。3.3 IPv6 快照单独处理128 位整数无法直接存 BIGINTIPv6 地址长度是 128 位远超BIGINT的 64 位范围。常见做法是拆成两个BIGINT字段或者直接用字符串存储加精确匹配。但要注意IPv6 的归属查询通常不是区间匹配而是前缀匹配类似 CIDR所以更适合用字符串存储配合LIKE或专门的 IPv6 函数。CREATE TABLE ipv6_temp ( network_prefix VARCHAR(64), country TEXT, province TEXT, city TEXT, isp TEXT );按/64或/48截取前缀存入network_prefix查询时把目标 IPv6 地址截断到同样的前缀长度再匹配。这种方案牺牲了精度但换来了极低的存储和索引开销。如果业务上必须精确到单个 IP 段的归属那就需要拆两个BIGINT并建复合索引但查询性能和存储成本都会翻倍。4. 导入三个存储引擎的方案对比MySQL、ClickHouse、Redis 各自的落地命令4.1 导入 MySQL建表、LOAD DATA 与索引设计MySQL 是处理这份快照最稳妥的选择适合数据量在百万行以内、查询频次不高的场景。关键点是 IP 段字段用INT UNSIGNEDIPv4 的整数范围是 0 到 4294967295刚好超出有符号INT的上限 2147483647索引建立在ip_start和ip_end上查询才能走索引。CREATE TABLE ip_location ( ip_start INT UNSIGNED NOT NULL, ip_end INT UNSIGNED NOT NULL, country VARCHAR(64) NOT NULL, province VARCHAR(64) NOT NULL, city VARCHAR(64) NOT NULL, isp VARCHAR(128) NOT NULL, PRIMARY KEY (ip_start, ip_end), KEY idx_end (ip_end) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;主键用(ip_start, ip_end)复合主键既保证唯一性又让区间查询天然按起始地址聚簇。idx_end是辅助索引处理「查某个 IP 落在哪个区间」时MySQL 优化器会先按ip_start定位到所有起始地址小于目标 IP 的行再用ip_end过滤。导入数据用LOAD DATA比逐条INSERT快一个数量级LOAD DATA LOCAL INFILE ./ipdata/ipv4_normalized.csv INTO TABLE ip_location FIELDS TERMINATED BY , IGNORE 1 LINES;LOAD DATA默认期望文件字段顺序与表结构一致所以前面预处理时输出的列序ip_start,ip_end,country,province,city,isp是特意调整过的。如果文件里有多余字段或表里有多余列用(col1, col2, ...)指定映射即可。导入完成后查一下行数和原文件行数是否一致。索引大小可以这样估算ip_start和ip_end各占 4 字节聚簇索引加辅助索引大约等于 1.5 倍数据行数这个量级对 InnoDB 来说完全没压力。4.2 导入 ClickHouse用 MergeTree 引擎把区间查询压到毫秒级数据量超过千万行、查询并发高的场景MySQL 就有点吃力了。ClickHouse 的MergeTree家族引擎特别适合这种「按范围扫描」的分析型查询。建表时把ip_start作为排序键数据物理存储上就是按起始 IP 排序的查询时能直接二分定位到目标区间。CREATE TABLE ip_location_ck ( ip_start UInt32, ip_end UInt32, country String, province String, city String, isp String ) ENGINE MergeTree ORDER BY (ip_start, ip_end);导入用clickhouse-client的--query参数配合本地文件clickhouse-client --query INSERT INTO ip_location_ck FORMAT CSV ./ipdata/ipv4_normalized.csvFORMAT CSV要求文件字段顺序和表结构完全一致这一点和 MySQL 的LOAD DATA相同。ClickHouse 的UInt32类型天然适配 IPv4 整数范围不需要像 MySQL 那样纠结有符号无符号。查询语句长这样SELECT country, province, city, isp FROM ip_location_ck WHERE ip_start 134744072 AND ip_end 134744072 LIMIT 1;ClickHouse 会利用排序键做区间裁剪只扫描ip_start小于目标 IP 的那一小块数据。实际测试中百万行的快照查询延迟稳定在 10ms 以内比 MySQL 快 5 到 10 倍。代价是 ClickHouse 不适合高频单条更新如果快照每月替换一次用TRUNCATE后全量重灌就行。4.3 导入 Redis低并发场景下用有序集合做快速判断Redis 方案适合只关心国家或城市级别的粗略定位、并发极低的场景。做法是把每个 IP 段映射到有序集合里score用ip_start成员用国家:省份:城市:运营商拼接的字符串。查询时用ZREVRANGEBYSCORE找到最后一个起始地址小于等于目标 IP 的段再判断结束地址是否覆盖目标 IP。redis-cli --pipe EOF ZADD ip_location 134744072 United States:California:Mountain View:Google ZADD ip_location 134744320 United States:California:Mountain View:Google ... EOFimport redis r redis.Redis(hostlocalhost, port6379) target_ip_int 134744072 # 找到最后一个起始 IP 小于等于目标 IP 的段 candidates r.zrevrangebyscore(ip_location, target_ip_int, 0, start0, num1, withscoresTrue) if candidates: member, start candidates[0] print(member)这段 Python 代码里zrevrangebyscore的target_ip_int是范围上界0是下界start0, num1表示从最大分值往回取第一个。这个方案的查询时间复杂度是 O(log N)性能不差但它有个致命问题区间结束地址没有存进有序集合查询结果只能说明「目标 IP 大于某个起始地址」不能保证落在该段结束地址内。如果快照数据存在重叠段或空洞查询结果会失真。所以 Redis 方案只适合对精度要求不高的场景比如按国家维度做访问控制。5. 这份快照文件的常见坑四个高频问题与排查路径5.1 解压后 wc -l 行数和 zip 清单里对不上现象wc -l统计的行数明显少于unzip -l显示的条目数差了正好一行或多行。原因文件最后一行没有换行符wc -l按换行符计数会漏掉最后一行另外 Windows 换行符CRLF在 Linux 工具下可能导致字符错位。解决用wc -l的同时再跑一次awk END{print NR}对比。NR是 awk 内部的行计数变量不受最后一行是否换行影响。如果两者差 1直接用awk的结果为准。处理 Windows 换行符用sed -i s/\r$//去掉行尾回车。5.2 城市名或运营商字段里夹杂逗号CSV 解析串列现象导入数据库后某几行的city字段变成了奇怪的值或者isp字段多出一截内容和样本行的字段数统计结果对不上。原因部分数据行的城市名、运营商名里自带逗号比如包含公司英文名后缀如果原始文件没有用引号包裹这些字段按逗号切分就会把一列拆成两列。解决检查样本行里是否有引号包裹的字段。如果有解析时要用csv模块而不是awk -F ,硬切。Python 的写法import csv with open(./ipdata/ipv4.txt, encodingutf-8) as f: reader csv.reader(f) for row in reader: # row 是完整分割后的字段列表 if len(row) ! 6: # 记录脏行稍后单独处理 passcsv.reader会正确处理引号内的逗号这是标准库处理这类问题的唯一靠谱方式。如果确认是脏行且数量极少几十条以内手工修正后追加进正式文件即可。5.3 IPv4 转成整数后超出有符号整型范围查询全部落空现象导入 MySQL 后查询任何 IP 都返回空结果但用SELECT看表里的数据却存在目标区间。原因IPv4 的最大值是255.255.255.255即4294967295超出 MySQLINT有符号范围最大2147483647。如果建表时用了INT而不是INT UNSIGNED导入阶段会静默截断或报错导致数据不完整。解决建表时字段类型必须是INT UNSIGNED。如果已经建错用ALTER TABLE ip_location MODIFY ip_start INT UNSIGNED, MODIFY ip_end INT UNSIGNED;修正类型后重新导入。ClickHouse 不存在这个问题因为UInt32天然覆盖整个 IPv4 范围Redis 方案把 IP 存成字符串或整数都能用只要保证ZADD时传入的score是整数即可。5.4 201907 快照的时效性陷阱IP 归属数据会漂移现象同一 IP 在旧快照里查到是某城市的住宅宽带但实际已经变成了云机房段。原因IP 段会因运营商路由调整、地址块重新分配而改变归属。2019 年 7 月的快照在当年或许准确到 2025 年已经过了六年使用前必须评估数据新鲜度。解决明确这个快照只能用于对实时性要求不高的场景比如历史日志回溯分析。做实时风控或精确地理定位时需要定期更换新快照或用在线接口交叉验证。具体做法见下一章的新鲜度自检脚本。6. 把快照用活用抽样比对脚本判断数据是否过期快照文件最大的短板是时效性。与其盲信数据不如写一个简单的抽样验证脚本定期抽几十个 IP和系统自带的whois命令做交叉比对统计归属一致的比例。这个脚本可以挂进定时任务每次拿到新快照先跑一遍再决定要不要替换线上数据。import random import subprocess import re # 从导入好的 MySQL 中随机抽样 sample_ips [random.randint(1, 4294967295) for _ in range(30)] def query_mysql(ip_int): # 略执行 SELECT 从 ip_location 表取归属 return (CN, Guangdong, Shenzhen) def query_whois(ip_dotted): out subprocess.run([whois, ip_dotted], capture_outputTrue, textTrue, timeout10) text out.stdout match re.search(rcountry:\s([A-Z]{2}), text, re.IGNORECASE) return match.group(1).upper() if match else UNKNOWN matched 0 for ip_int in sample_ips: mm ip_int 24 0xFF aa ip_int 16 0xFF bb ip_int 8 0xFF cc ip_int 0xFF ip_dotted f{mm}.{aa}.{bb}.{cc} if query_mysql(ip_int)[0] query_whois(ip_dotted): matched 1 print(f一致性: {matched}/{len(sample_ips)})这个脚本的判定逻辑基于国家代码的一致性。如果抽样结果一致率低于 70%说明快照已经明显过期优先换新版达到 90% 以上可以继续用但要在查询接口里标注数据的时间戳方便以后追溯。whois命令的country字段是标准输出但不是所有 IP 都有这个字段抽样时优先选那些归属明确的 IP 段。另一个实用技巧是给查询结果加上数据版本标记。比如在 MySQL 表里加一列snapshot_date DATE DEFAULT 2019-07-01查询时默认带上这个条件这样老数据和将来导入的新数据可以并存线上切换时也方便回滚。我每次处理这类快照都会保留一个版本字段半年后回头看这可能是最值回票价的一个习惯。数据新鲜度自检不需要每天跑一个月一次足够。把脚本扔进 cron 定时任务里输出结果存成日志连续两次低于阈值就该动手换数据了。IP 快照永远不会是完美即时的但配合定期验证和明确的版本标记它能在离线环境里扛起大部分 IP 定位需求。希望帮到你。本文还有配套的精品资源点击获取