纯真IP库+ip2region,把IP地址转换成省市县编码

最近发现项目使用的IP地址解析工具,准确率很低,然后解析出的地址信息是文本,匹配省市县信息还需要复杂的解析,效率效果都很差,于是决定重构这一模块。

查阅了大量网上的方案,商业API的效果很好,但我个人项目使用没有必要,并且要求也没有那么高,还是用开源的吧。

首先我找到了ip2region工具,选择了他的java实现,引入依赖。

<dependency>
    <groupId>org.lionsoul</groupId>
    <artifactId>ip2region</artifactId>
    <version>2.7.0</version>
</dependency>

直接使用他们的ip地址库,搓一个demo试一下。

public static void main(String[] args) throws Exception {
    byte[] buff = ResourceUtil.readBytes("ip2region.xdb");
    Searcher searcher = Searcher.newWithBuffer(buff);
    String region = searcher.search("1.0.32.1");
    System.out.println(region);
}

运行后得到结果

中国|0|广东省|广州市|电信

试了几个常用IP后发现数据不是很准确,工具文档上已经有说明:

并且ip2region.xdb这个文件有点大。简单分析了一下:

1、我不需要国外的IP信息。
2、我不需要运营商信息。
3、省市县信息依然是文字,没有快速获取到省市县编码

于是决定自己整理生成一个xdb数据。

原始数据选择了纯真社区版IP库(https://www.cz88.net/)

获得授权后,得到了dat数据。为了查看详细信息,我决定先把这个文件解析出来:

参考文章(https://www.iteye.com/blog/hzy3774-1851364)

#include <stdio.h>  
  
#define DAT_FILE "./qqwry20240731.dat"  
#define OUT_FILE "./out.txt"  
  
FILE *out = NULL;  
  
void showIp(unsigned int ip)  
{  
    char buffer[20] = {0};  
    sprintf(buffer, "%u.%u.%u.%u  ", (ip >> 24) & 0xff, (ip >> 16) & 0xff, (ip >> 8) & 0xff, ip & 0xff);  
    fputs(buffer, out);  
}  
  
void getString(FILE *fp)    //讀取字符串并写入文件,以0为结尾标志  
{  
    char temp[128] = {0};  
    int index = 0;  
    while ((temp[index] = fgetc(fp)) != 0)  
    {  
        index++;  
    }  
    fputs(temp, out);  
}  
  
void getInfo(FILE *fp, int flag)    //获取数据区字符串信息,flag为1时表示已经读了一半字符串  
{  
    unsigned char mod;  
    long int offset = 0;  
    long int preOffset = 0;  
  
    mod = fgetc(fp);            //获取模式  
    if (mod > 2)             //如果无重定向  
    {  
        fseek(fp, -1L, SEEK_CUR);   //返回  
        getString(fp);          //读取一个字符串  
        if (flag == 0)          //如果调用函数时还未读  
        {  
            getInfo(fp, 1);     //标志置1,递归调用  
        }  
        return;  
    }  
    fread(&offset, 3, 1, fp);       //如果有重定向,获取重定向偏移  
    if (mod == 1)                   //如果模式一  
    {  
        fseek(fp, offset, SEEK_SET);    //seek到目标地址  
        getInfo(fp, flag);              //递归读  
        return;  
    }  
    preOffset = ftell(fp);          //如果模式二,记住当前偏移  
    fseek(fp, offset, SEEK_SET);    //寻目标地址  
    getInfo(fp, 1);                 //读一个字符串  
    if (flag == 0)                  //如果调用函数时还未读  
    {  
        fseek(fp, preOffset, SEEK_SET);     //还要回到原来位置  
        getInfo(fp, 1);                     //再读一次  
    }  
}  
  
void parseRecord(FILE *fp, long int recordOffset)  
{  
    unsigned int endIp = 0;  
  
    fseek(fp, recordOffset, SEEK_SET);  //seek到数据区  
    fread(&endIp, 4, 1, fp);        //读取结束IP  
    showIp(endIp);                  //显示结束IP  
    getInfo(fp, 0);                 //获取信息  
    fputs("\n", out);  
}  
  
void parseDat(FILE *fp)  
{  
    long int startIndexOffset = 0;  
    long int endIndexOffset = 0;  
    long int indexOffset = 0;  
    long int recordOffset = 0;  
    unsigned int startIp = 0;  
  
    fseek(fp, 0L, SEEK_SET);            //seek文件头  
    fread(&startIndexOffset, 4, 1, fp); //读索引开始偏移量  
    fread(&endIndexOffset, 4, 1, fp);   //读索引结束偏移量  
    indexOffset = startIndexOffset;  
    while (indexOffset <= endIndexOffset)    //循环从根据索引读取数据  
    {  
        fseek(fp, indexOffset, SEEK_SET);   //seek到索引位置  
        fread(&startIp, 4, 1, fp);          //读取索引中的开始IP  
        fread(&recordOffset, 3, 1, fp);     //读取对应数据区的偏移量  
        indexOffset = ftell(fp);            //记住索引的偏移量  
        showIp(startIp);                    //显示开始IP  
        parseRecord(fp, recordOffset);      //读取数据区  
    }  
}  
  
int main(void)  
{  
    FILE *fp = NULL;  
  
    out = fopen(OUT_FILE, "wb");  
    fp = fopen(DAT_FILE, "rb");  
    if (fp == NULL )  
    {  
        printf("open file error!\n");  
        return 0;  
    }  
    parseDat(fp);  
    fclose(fp);  
    return 0;  
}  

于是得到了一个80+MB的超大文件

分析了一下结构,先对数据进行清理。

首先舍弃掉了国外的数据,然后移除/后面的精确地址,最终保留省市县名称和2位行政编码。如湖南省430000,只存储43

import json,re
# 加载省市县数据
with open("region.json", 'r', encoding='utf-8') as file:
    data = json.load(file)
print("---省市县加载完成---")

with open('ip2region.txt', 'w', encoding='utf-8') as outfile:
    with open('out.txt', 'r') as file:
        line = file.readline()
        while line:
            # 去掉国外的
            if "中国" not in line:
                line = file.readline()
                continue
            line = line.replace("中国–","")
            line = line.split("  ")
            # 解析省市县 移除不必要的数据
            # /后面的是精确地址,舍弃
            area = line[2].split("–")
            containProvince = None
            containCity = None
            containCounty = None
            # 匹配省
            for province in data:
                if re.search(province["short_name"], area[0]) is not None:
                    # 找到了省
                    containProvince = province
                    # 有第二级地区
                    if(len(area)>2):
                        for city in province["children"]:
                            if re.search(city["short_name"], area[1]) is not None:
                                # 找到了省
                                containCity = city
                                if(len(area)>3):
                                    for county in city["children"]:
                                        if re.search(county["short_name"], area[2]) is not None:
                                            # 找到了省
                                            containCounty = county

            if containProvince is None:
                #没有省的数据 跳过
                continue
            # 只保留省市县,其他信息丢弃
            result=line[0]+"|"+line[1]+"|"+containProvince["region_name"]+"|"+str(containProvince["id"])[:2]
            if containCity is not None:
                result = result +"|"+containCity["region_name"]+"|"+str(containCity["id"])[3:4]
            if containCounty is not None:
                result = result +"|"+containCounty["region_name"]+"|"+str(containCounty["id"])[5:6]
            result=result+"\n"
            outfile.write(result)

然后就得到了这样的数据,有15W多条。

然后利用ip2region提供的数据生成python实现(https://gitee.com/lionsoul/ip2region/tree/master/maker/python),转成XDB数据。

python genxdb.py gen --src=./ip2region.txt --dst=./ip2region.xdb

期间报了一个错误,make.py155-161行,上一行的结束IP和下一行的起始IP不连续,直接注释掉这段代码

然后就得到了新的xdb文件,只有2.6MB,小了很多,检查发现,还可以更加精简,由于去掉了详细地址和运营商等信息,很多地址信息是相同且连续的,可以合并:

于是我修改了maker.py中的load_segments函数,增加了合并连续IP相同地址的逻辑,合并后,数据减少到6W条,生成的XDB文件仅1MB:

            if last is not None:
                if last.end_ip + 1 == segment.start_ip:
                    if last.region == segment.region:
                        # 移除最后一个元素,修改当前元素的起始IP
                        self.segments.pop()
                        segment.start_ip = last.start_ip

最后,重新编写了解析工具类,就可以自动解析出行政编码了

public static IP2Region getIPAttribution(String ip) {
    IP2Region ipAttribution = new IP2Region();
    ipAttribution.setIp(ip);
    try {

        Searcher searcher = Searcher.newWithBuffer(ResourceUtil.readBytes("META-INF/ip2region.xdb"));

        String region = searcher.search(ip);

        searcher.close();

        String[] result = region.split("\\|");
        if (ObjectUtil.isNotNull(result[0])) {
            ipAttribution.setProvince(result[0]);
            ipAttribution.setProvinceId(StrUtil.padAfter(result[1], 6, "0"));
        }
        if (ObjectUtil.isNotNull(result[2])) {
            ipAttribution.setCity(result[2]);
            ipAttribution.setCityId(StrUtil.padAfter(result[1] + StrUtil.padPre(result[3], 2, "0"), 6, "0"));
        }
        if (ObjectUtil.isNotNull(result[4])) {
            ipAttribution.setCounty(result[4]);
            ipAttribution.setCountyId(result[1] + StrUtil.padPre(result[3], 2, "0") + StrUtil.padPre(result[5], 2, "0"));
        }
    } catch (Exception ignore) {
    }
    return ipAttribution;
}

7人评论了“纯真IP库+ip2region,把IP地址转换成省市县编码”

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部