最近要做一个全球的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行的数据,时间是差不多的.
这 其实就是所谓的大数据,分布式的概念了. 当然实际应用中会比这个要复杂的多的多.