生在红旗下长在春风里,长期浸泡在河蟹社会里面所以久而久之就有了一些河蟹的思维方式,正好有一段时间在做一个.NET的网站访问统计系统,顺便想着怎么“监视”下每一个留言的博主们的所在地,于是就有了如今下图(网易留言板的样式)所示的根据获取到的留言者的IP地址得到留言者所在的地区,当然并没有河蟹社会监视人民群众的意思,纯属了解一下各位博主所在的真实地点,万一是个美女博主不就可以让我有线索可循。

 

想要把IPv4地址转为真实的地址,肯定要参考IP数据库,商业的IP数据库存储在关系型数据库中,查询和使用都非常方便,但是成本不是个人和小公司愿意承受的,所以简单应用的思路就是利用一些免费的IP数据库或者一些大网站提供的查询API,他们的数据量足够我们使用了。

1. 利用纯真IP数据库

利用本地的QQWry.Dat文件,优点是查询速度非常快,缺点是数据库文件要放在自己的空间内并且要偶尔更新数据库。时间关系废话不多说,下面是使用这个文件的函数,如果是在WordPress里面使用这个功能,把下面的代码写入主题下面的functions.php里面,然后在comments-list的输出<?php echo convertip(get_comment_author_ip()); ?>即可;如果是其他程序引用,输入一个有效的IPv4地址就可以得到一个真实的地址。

 

  1. function convertip($ip) { 
  2.     //IP数据文件路径 
  3.     $dat_path = 'QQWry.Dat'
  4.  
  5.     //检查IP地址 
  6.     //if(!preg_match("/^d{1,3}.d{1,3}.d{1,3}.d{1,3}$/", $ip)) { 
  7.     //    return 'IP Address Error'; 
  8.     //} 
  9.     //打开IP数据文件 
  10.     if(!$fd = @fopen($dat_path'rb')){ 
  11.         return 'IP date file not exists or access denied'
  12.     } 
  13.  
  14.     //分解IP进行运算,得出×××数 
  15.     $ip = explode('.'$ip); 
  16.     $ipNum = $ip[0] * 16777216 + $ip[1] * 65536 + $ip[2] * 256 + $ip[3]; 
  17.  
  18.     //获取IP数据索引开始和结束位置 
  19.     $DataBegin = fread($fd, 4); 
  20.     $DataEnd = fread($fd, 4); 
  21.     $ipbegin = implode('', unpack('L'$DataBegin)); 
  22.     if($ipbegin < 0) $ipbegin += pow(2, 32); 
  23.     $ipend = implode('', unpack('L'$DataEnd)); 
  24.     if($ipend < 0) $ipend += pow(2, 32); 
  25.     $ipAllNum = ($ipend - $ipbegin) / 7 + 1; 
  26.  
  27.     $BeginNum = 0; 
  28.     $EndNum = $ipAllNum
  29.  
  30.     //使用二分查找法从索引记录中搜索匹配的IP记录 
  31.     while($ip1num>$ipNum || $ip2num<$ipNum) { 
  32.         $Middleintval(($EndNum + $BeginNum) / 2); 
  33.  
  34.         //偏移指针到索引位置读取4个字节 
  35.         fseek($fd$ipbegin + 7 * $Middle); 
  36.         $ipData1 = fread($fd, 4); 
  37.         if(strlen($ipData1) < 4) { 
  38.             fclose($fd); 
  39.             return 'System Error'
  40.         } 
  41.         //提取出来的数据转换成长×××,如果数据是负数则加上2的32次幂 
  42.         $ip1num = implode('', unpack('L'$ipData1)); 
  43.         if($ip1num < 0) $ip1num += pow(2, 32); 
  44.  
  45.         //提取的长整型数大于我们IP地址则修改结束位置进行下一次循环 
  46.         if($ip1num > $ipNum) { 
  47.             $EndNum = $Middle
  48.             continue
  49.         } 
  50.  
  51.         //取完上一个索引后取下一个索引 
  52.         $DataSeek = fread($fd, 3); 
  53.         if(strlen($DataSeek) < 3) { 
  54.             fclose($fd); 
  55.             return 'System Error'
  56.         } 
  57.         $DataSeek = implode('', unpack('L'$DataSeek.chr(0))); 
  58.         fseek($fd$DataSeek); 
  59.         $ipData2 = fread($fd, 4); 
  60.         if(strlen($ipData2) < 4) { 
  61.             fclose($fd); 
  62.             return 'System Error'
  63.         } 
  64.         $ip2num = implode('', unpack('L'$ipData2)); 
  65.         if($ip2num < 0) $ip2num += pow(2, 32); 
  66.  
  67.         //没找到提示未知 
  68.         if($ip2num < $ipNum) { 
  69.             if($Middle == $BeginNum) { 
  70.                 fclose($fd); 
  71.                 return 'Unknown'
  72.             } 
  73.             $BeginNum = $Middle
  74.         } 
  75.     } 
  76.  
  77.     $ipFlag = fread($fd, 1); 
  78.     if($ipFlag == chr(1)) { 
  79.         $ipSeek = fread($fd, 3); 
  80.         if(strlen($ipSeek) < 3) { 
  81.             fclose($fd); 
  82.             return 'System Error'
  83.         } 
  84.         $ipSeek = implode('', unpack('L'$ipSeek.chr(0))); 
  85.         fseek($fd$ipSeek); 
  86.         $ipFlag = fread($fd, 1); 
  87.     } 
  88.  
  89.     if($ipFlag == chr(2)) { 
  90.         $AddrSeek = fread($fd, 3); 
  91.         if(strlen($AddrSeek) < 3) { 
  92.             fclose($fd); 
  93.             return 'System Error'
  94.         } 
  95.         $ipFlag = fread($fd, 1); 
  96.         if($ipFlag == chr(2)) { 
  97.             $AddrSeek2 = fread($fd, 3); 
  98.             if(strlen($AddrSeek2) < 3) { 
  99.                 fclose($fd); 
  100.                 return 'System Error'
  101.             } 
  102.             $AddrSeek2 = implode('', unpack('L'$AddrSeek2.chr(0))); 
  103.             fseek($fd$AddrSeek2); 
  104.         } else { 
  105.             fseek($fd, -1, SEEK_CUR); 
  106.         } 
  107.  
  108.         while(($char = fread($fd, 1)) != chr(0)) 
  109.             $ipAddr2 .= $char
  110.  
  111.         $AddrSeek = implode('', unpack('L'$AddrSeek.chr(0))); 
  112.         fseek($fd$AddrSeek); 
  113.  
  114.         while(($char = fread($fd, 1)) != chr(0)) 
  115.             $ipAddr1 .= $char
  116.     } else { 
  117.         fseek($fd, -1, SEEK_CUR); 
  118.         while(($char = fread($fd, 1)) != chr(0)) 
  119.             $ipAddr1 .= $char
  120.  
  121.         $ipFlag = fread($fd, 1); 
  122.         if($ipFlag == chr(2)) { 
  123.             $AddrSeek2 = fread($fd, 3); 
  124.             if(strlen($AddrSeek2) < 3) { 
  125.                 fclose($fd); 
  126.                 return 'System Error'
  127.             } 
  128.             $AddrSeek2 = implode('', unpack('L'$AddrSeek2.chr(0))); 
  129.             fseek($fd$AddrSeek2); 
  130.         } else { 
  131.             fseek($fd, -1, SEEK_CUR); 
  132.         } 
  133.         while(($char = fread($fd, 1)) != chr(0)){ 
  134.             $ipAddr2 .= $char
  135.         } 
  136.     } 
  137.     fclose($fd); 
  138.  
  139.     //最后做相应的替换操作后返回结果 
  140.     if(preg_match('/http/i'$ipAddr2)) { 
  141.         $ipAddr2 = ''
  142.     } 
  143.     $ipaddr = "$ipAddr1 $ipAddr2"
  144.     $ipaddr = preg_replace('/CZ88.Net/is'''$ipaddr); 
  145.     $ipaddr = preg_replace('/^s*/is'''$ipaddr); 
  146.     $ipaddr = preg_replace('/s*$/is'''$ipaddr); 
  147.     if(preg_match('/http/i'$ipaddr) || $ipaddr == '') { 
  148.         $ipaddr = 'Unknown'
  149.     } 
  150.     $ipaddr = iconv('gbk''utf-8//IGNORE'$ipaddr); //转换编码,如果网页的gbk可以删除此行 
  151.     return $ipaddr

 

2. 利用门户网站的接口

目前已知的有腾讯、新浪、网易、搜狐和Google提供IP地址查询API,但是找得到的只有腾讯、新浪和网易的,Google的貌似要用Google Maps所以没有研究。看了下国内的几个腾讯提供的是JavaScript的,网易提供的是XML,而新浪的有多种格式可以用,注意非XML的数据源都是GBK格式的,不管是JavaScript调用还是PHP调用都要转换一下编码,不然得到的是乱码。而更需要注意的是,如果一次性查询多个IP,使用门户网站的API来查询会非常缓慢,我大概写了个for循环试了下,不管是用PHP解析XML还是file_get_contents()函数获取内容,查询10次以上会变得非常缓慢,甚至可能超时。

腾讯的IP地址API接口地址:http://fw.qq.com/ipaddress,返回的是数据格式为:var IPData = new Array("123.124.2.85","","北京市","");,一个JavaScript的对象,目前还不知道如何输入IP查询。

新浪的IP地址查询接口:http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=js

新浪多地域测试方法:http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=js&ip=123.124.2.85

网易有道的IP地址查询接口:http://www.youdao.com/smartresult-xml/search.s?type=ip&q=123.124.2.85

 腾讯的调用方法

  1. //腾讯API的PHP调用方法 
  2. function getIpPlace(){ 
  3.  $ip=file_get_contents("http://fw.qq.com/ipaddress"); 
  4.  $ip=str_replace('"',' ',$ip); 
  5.  $ip2=explode("(",$ip); 
  6.  $a=substr($ip2[1],0,-2); 
  7.  $b=explode(",",$a); 
  8.  return $b
  9. $ip=getIpPlace(); 
  10. print_r($ip); 


 网易有道的调用方法

  1. //有道API的PHP调用方法 
  2. $url = "http:www.youdao.com/smartresult-xml/search.s?type=ip&q=".$ip
  3. $doc = new DOMDocument(); 
  4.    $doc->load($url); 
  5.    $smartresult = $doc->getElementsByTagName("product"); 
  6.    foreach($smartresult as $product
  7.    { 
  8.       $locations = $product->getElementsByTagName("location"); 
  9.       $location = $locations->item(0)->nodeValue; 
  10.    } 
  11.    if($location != ""
  12.    { 
  13.        echo $i.".".$ip
  14.        echo "  来自".$location."的网友"
  15.    } 
  16.    else 
  17.    { 
  18.        echo $i.".".$ip
  19.        echo "  来自火星的网友"
  20.    } 

新浪的调用方法

  1. public function sinaIPApi($ip){ 
  2.    $str = file_get_contents("http://int.dpool.sina.com.cn/iplookup/iplookup.php?ip=".$ip); 
  3.    $str = iconv("gbk""utf-8//IGNORE"$str); 
  4.    preg_match_all("/[\x{4e00}-\x{9fa5}]+/u",$str,$get); 
  5.    $add = implode('',$get[0]); 
  6.    return $add
  7. //$get是一个非常棒的二维数组 

其中有道和新浪的是我自己写的,新浪API也可以像腾讯API那样用file_get_contents()函数获取完地址后使用一连串的字符串函数处理,我写的函数使用正则表达式从新浪的返回结果中提供包含中文的字符串,并且分段存入一个二维数组,这个可能只是针对新浪的API有用并且存在bug。举个例子查询学校分配给我的IP地址后var_dump()一下函数里面的$get变量得到以下结果: array(1) { [0]=> array(6) { [0]=> string(6) "中国" [1]=> string(6) "北京" [2]=> string(6) "北京" [3]=> string(9) "教育网" [4]=> string(6) "学校" [5]=> string(18) "中国地质大学" } },而函数输出的结果则是“中国北京北京教育网学校中国地质大学”,希望我的思路和方法能对别人有用。

最后再次提醒,如果是WordPress请使用第一种方法,否则使用API同时查询所有留言者的真实地址会让PHP超时的,希望各路大牛有更好的方法,至于限制显示和显示方式等神马的都是WordPress应用问题,同时对于Java和C#来说思路也是一样的,这些后续的问题等我考完试再细说。