php根据二分查找法从普通csv文件中获取ip的地理位置(效率比使用mysql提高近800倍)

最近要做一个全球的ip地理位置查询,并且要精确到城市一级,还要求是英文版的.

首先是要找ip库, 纯真ip 库只有中文的,而且国内的ip缺少国家这一级的分类,放弃

apnic 的倒是不错,英文而且更新也快,但是只有国家一级.

终于电驴上找到了相关的资源 IP地理位置数据库 世界城镇扩展版 130812 

数据来源于 MaxMind , 也算比较新的. 这个比较符合要求.

数据源是csv格式的. 格式如下:

 16777472  16778239  CN  CHN  China  

 16785408  16793599  CN  CHN  Guangzhou,  Guangdong, China

前两列是ip端 , 三四列 是国家英文名缩写, 第五列是 国家和地区,用逗号分割的.

看到这个数据,第一反应是导入到mysql数据库然后从数据库查询,但是看了下总数据大概有接近200w条.总大小130M 

预计mysql查询起来也不会太快.

于是自己弄了个简单版的二分查找法.这样可以直接在csv文件中查找,无需导入数据库,查询效率也不低.

总体思路是这样.

先用php读取csv文件的每行数据, 获取该行数据的大小.然后建立一个二进制的索引文件

索引文件的格式如下:

start_ip  end_ip   offset  length

start_ip表示起始ip  ,end_ip表示结束ip , offset表示数据在csv文件中的文件指针偏移位置,length表示该条数据的长度

然后然把数据打包成二进制数据存放到索引文件中.

除了length 外,其他的数据采用无符号长整型,length采用short就可以了. 每一行数据对应一条索引 ,索引长度是 4+4+4+2=14个字节.

200w 行的数据索引大小大概是 26.7M 左右,采用二分法查找时, 最坏的结果是需要循环 21次 才能得到最终的结果.不过效率依然比用数据库高出不少.

部分代码和测试结果如下:

首先把csv 导入数据库 . 

建立表 id_addr 

CREATE TABLE IF NOT EXISTS `ip_addr` (

  `start_ip` bigint(20) NOT NULL,

  `end_ip` bigint(20) NOT NULL,

  `short` varchar(5) COLLATE utf8_unicode_ci NOT NULL,

  `short1` varchar(5) COLLATE utf8_unicode_ci NOT NULL,

  `country` varchar(50) COLLATE utf8_unicode_ci NOT NULL,

) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

导入 csv  

mysql> load data infile 'E:/source/ip-to-country.csv' into table ip_addr fields terminated by ',' optionally encl

osed by '"' lines terminated by '\r\n';

测试查询 某IP 59.173.248.188 ,转换为整型 1001257148 

查询语句:SELECT * FROM `ip_addr` WHERE start_ip<=1001257148 and end_ip>=1001257148 ;

查询结果和时间

1001127936   1001390079   CN   CHN    Wuhan, Hubei, China

1 total, Query took 4.4871 sec

耗时近4.5秒

对start_ip和end_ip 建立索引

ALTER TABLE  `ip_addr` ADD INDEX (  `start_ip`,`end_ip` );

再次执行 该查询语句: SELECT * FROM `ip_addr` WHERE start_ip<=1001257148 and end_ip>=1001257148 ;

查询结果相同

1 total, Query took 1.7607 sec

耗时减少到1.76秒 

效率大概提高了60%左右..但是依旧不尽如人意.

再来看看我们的粗略二分查找法的效率如何.

首先根据csv生成索引,php代码如下:

<?php
$fp=fopen('E:/source/ip-to-country.csv','r');
$fp1=fopen('E:/source/ip-to-country.csv','r');
$fp2=fopen('E:/source/index.dat','w');
$i=0;
$offset=0;
$length=0;
while(($line=fgets($fp))&&($line1=fgetcsv($fp1))){
    $length=strlen($line);
    $start_ip=$line1[0];
    $end_ip=$line1[1];
    $index=pack('L',(float)$start_ip).pack('L',(float)$end_ip).pack('L',(float)$offset).pack('S',$length);
    $offset+=$length;
    fwrite($fp2,$index);
}
fclose($fp);
fclose($fp1);
fclose($fp2);

然后同样查询 这个IP :59.173.248.188  代码如下:

<?php
$ip='59.173.248.188';
$ip=sprintf('%u',ip2long($ip));
$time=microtime(1);
$fp=fopen('E:/source/index.dat','r');
$begin = 0;
$end = filesize('E:/source/index.dat');
$start_ip = sprintf('%u',implode('', unpack('L', fread($fp, 4))));
fseek($fp,$end-10);
$end_ip = sprintf('%u',implode('', unpack('L', fread($fp, 4))));
do{
    if($end - $begin <=14){
        fseek($fp,$begin);
        $s=sprintf('%u',implode('', unpack('L', fread($fp, 4))));
        $e=sprintf('%u',implode('', unpack('L', fread($fp, 4))));
        if($ip<$s||$ip>$e) break;
        $offset = sprintf('%u',implode('', unpack('L', fread($fp, 4))));
        $length = implode('', unpack('S', fread($fp, 2)));
        break;
    }
    $middle_offset = ceil((($end - $begin) / 14) / 2) * 14 + $begin;
    fseek($fp, $middle_offset);
    $middle_ip = sprintf('%u',implode('', unpack('L', fread($fp, 4))));
    if ($ip >= $middle_ip) {
        $begin = $middle_offset;
    } else {
        $end = $middle_offset;
    }
}while(1);
 
if($length>0){
    fclose($fp);
    $fp=fopen('E:/source/ip-to-country.csv','r');
    fseek($fp,$offset);
    $line=fgetcsv($fp,$length);
}
echo implode(',',$line).'<br />';
echo microtime(1)-$time;

输出

1001127936,1001390079,CN,CHN,Wuhan, Hubei, China
0.00243091583252

查询结果一样.但是耗时在0.002s 左右...效率差不多提高了800倍...

也许会有人说这是二分查找时碰到最佳情况..只循环几次就找到了结果..我测试的结果是循环了总共循环了22次.也就是最差的查找结果哦


其实这个原理和数据库建索引的原理是一个道理.我也顺便测试了一下不建索引,直接通过csv文件一行一行去查找耗时几何

代码如下:

<?php
$ip='59.173.248.188';
$ip=sprintf('%u',ip2long($ip));
$time=microtime(1);
$fp=fopen('ip-to-country.csv','r');
while($line=fgetcsv($fp)){
    if($ip>=$line[0]&&$ip<=$line[1]){
        break;
    }
}
fclose($fp);
echo implode(',',$line)."<br />";
echo microtime(1)-$time;

输出:

1001127936,1001390079,CN,CHN,Wuhan, Hubei, China
3.92840719223 

耗时接近 4秒 和无索引的数据库查询速度相接近了.

代码和算法思路都很简单..但是却能大大的提高数据检索效率。作为程序员有时候,也需要多多思考一下这方面的问题.


扩展思维: 现在是200w行数据,如果数据扩大到 2000w行 甚至2亿行数据呢? 这种方式的效率会呈几何数级的下降.

怎么解决? 这就是所谓的大数据了.不考虑什么hadoop之类的.如果这时候要你来解决,你会用什么方案?

其中一个思路: 比如 现在csv文件扩展到2亿行数据, 那么其实我们可以把文件切分成20个不同的小文件每个文件就只有1000w行数据. 然后利用多线程编程(php5.3 开始支持多线程了), 开10个线程,同时检索10个文件.马上效率就提高了10倍.

再极端一点.把文件分成100个小文件,这样每个文件只有200w行数据, 10台机器,每台机器开10个线程,每个线程检索一个文件.

此时对单一线程来说,检索效率就和上文的例子一样了. 这样你会发现2亿行的数据处理起来和200w行的数据,时间是差不多的.

这 其实就是所谓的大数据,分布式的概念了. 当然实际应用中会比这个要复杂的多的多.


你可能感兴趣的:(PHP,分布式,大数据,二分查找法,IP库)