之前介绍过很多第三方的 IP 地址查询 API 接口,详见: 分享几个IP获取地理位置的API接口,直接调用第三方的接口很方便,但也容易失效导致无法使用。因此今天来分享一个基于本地数据库的 IP 地址查询源码!
模块代码
<?php /** * 纯真 IP 数据库查询 * * 参考资料: * - 纯真 IP 数据库 http://www.cz88.net/ip/ * - PHP 读取纯真IP地址数据库 http://ju.outofmemory.cn/entry/42500 * - 纯真 IP 数据库自动更新文件教程 https://www.22vd.com/40035.html * - IpLocation https://github.com/nauxliu/IpLocation/ * - 基于本地数据库的 IP 地址查询 PHP 源码 https://mkblog.cn/?p=1951 * * 使用示例: * $ip = new IPQuery(); * $addr = $ip->query('IP地址'); * print_r($addr); */ class IPQuery { private $fh; // IP数据库文件句柄 private $first; // 第一条索引 private $last; // 最后一条索引 private $total; // 索引总数 private $dbFile = __DIR__ . DIRECTORY_SEPARATOR . 'qqwry.dat'; // 纯真 IP 数据库文件存放路径 private $dbExpires = 86400 * 10; // 数据库文件有效期(10天)如无需自动更新 IP 数据库,请将此值改为 0 // 构造函数 function __construct() { // IP 数据库文件不存在或已过期,则自动获取 if(!file_exists($this->dbFile) || ($this->dbExpires && ((time() - filemtime($this->dbFile)) > $this->dbExpires))) { $this->update(); } } // 忽略超时 private function ignore_timeout() { @ignore_user_abort(true); @ini_set('max_execution_time', 48 * 60 * 60); @set_time_limit(48 * 60 * 60); // set_time_limit(0) 2day @ini_set('memory_limit', '4000M');// 4G; } // 读取little-endian编码的4个字节转化为长整型数 private function getLong4() { $result = unpack('Vlong', fread($this->fh, 4)); return $result['long']; } // 读取little-endian编码的3个字节转化为长整型数 private function getLong3() { $result = unpack('Vlong', fread($this->fh, 3).chr(0)); return $result['long']; } // 查询位置信息 private function getPos($data = '') { $char = fread($this->fh, 1); while (ord($char) != 0) { // 地区信息以 0 结束 $data .= $char; $char = fread($this->fh, 1); } return $data; } // 查询运营商 private function getISP() { $byte = fread($this->fh, 1); // 标志字节 switch (ord($byte)) { case 0: $area = ''; break; // 没有相关信息 case 1: // 被重定向 fseek($this->fh, $this->getLong3()); $area = $this->getPos(); break; case 2: // 被重定向 fseek($this->fh, $this->getLong3()); $area = $this->getPos(); break; default: $area = $this->getPos($byte); break; // 没有被重定向 } return $area; } // 检查 IP 格式是否正确 public function checkIp($ip) { $arr = explode('.', $ip); if(count($arr) != 4) return false; for ($i = 0; $i < 4; $i++) { if ($arr[$i] < '0' || $arr[$i] > '255') { return false; } } return true; } // 查询 IP 地址 public function query($ip) { if(!$this->checkIp($ip)) { return false; } $this->fh = fopen($this->dbFile, 'rb'); $this->first = $this->getLong4(); $this->last = $this->getLong4(); $this->total = ($this->last - $this->first) / 7; // 每条索引7字节 $ip = pack('N', intval(ip2long($ip))); // 二分查找 IP 位置 $l = 0; $r = $this->total; while($l <= $r) { $m = floor(($l + $r) / 2); // 计算中间索引 fseek($this->fh, $this->first + $m * 7); $beginip = strrev(fread($this->fh, 4)); // 中间索引的开始IP地址 fseek($this->fh, $this->getLong3()); $endip = strrev(fread($this->fh, 4)); // 中间索引的结束IP地址 if ($ip < $beginip) { // 用户的IP小于中间索引的开始IP地址时 $r = $m - 1; } else { if ($ip > $endip) { // 用户的IP大于中间索引的结束IP地址时 $l = $m + 1; } else { // 用户IP在中间索引的IP范围内时 $findip = $this->first + $m * 7; break; } } } // 查找 IP 地址段 fseek($this->fh, $findip); $location['beginip'] = long2ip($this->getLong4()); // 用户IP所在范围的开始地址 $offset = $this->getlong3(); fseek($this->fh, $offset); $location['endip'] = long2ip($this->getLong4()); // 用户IP所在范围的结束地址 // 查找 IP 信息 $byte = fread($this->fh, 1); // 标志字节 switch (ord($byte)) { case 1: // 都被重定向 $countryOffset = $this->getLong3(); // 重定向地址 fseek($this->fh, $countryOffset); $byte = fread($this->fh, 1); // 标志字节 switch (ord($byte)) { case 2: // 信息被二次重定向 fseek($this->fh, $this->getLong3()); $location['pos'] = $this->getPos(); fseek($this->fh, $countryOffset + 4); $location['isp'] = $this->getISP(); break; default: // 信息没有被二次重定向 $location['pos'] = $this->getPos($byte); $location['isp'] = $this->getISP(); break; } break; case 2: // 信息被重定向 fseek($this->fh, $this->getLong3()); $location['pos'] = $this->getPos(); fseek($this->fh, $offset + 8); $location['isp'] = $this->getISP(); break; default: // 信息没有被重定向 $location['pos'] = $this->getPos($byte); $location['isp'] = $this->getISP(); break; } // 信息转码处理 foreach ($location as $k => $v) { $location[$k] = iconv('gb2312', 'utf-8', $v); $location[$k] = preg_replace(array('/^.*CZ88\.NET.*$/isU', '/^.*纯真.*$/isU', '/^.*日IP数据/'), '', $location[$k]); $location[$k] = htmlspecialchars($location[$k]); } return $location; } // 更新数据库 https://www.22vd.com/40035.html public function update() { $this->ignore_timeout(); $copywrite = file_get_contents('http://update.cz88.net/ip/copywrite.rar'); $qqwry = file_get_contents('http://update.cz88.net/ip/qqwry.rar'); $key = unpack('V6', $copywrite)[6]; for($i = 0; $i < 0x200; $i++) { $key *= 0x805; $key ++; $key = $key & 0xFF; $qqwry[$i] = chr(ord($qqwry[$i]) ^ $key); } $qqwry = gzuncompress($qqwry); file_put_contents($this->dbFile, $qqwry); } // 析构函数 function __destruct() { if($this->fh) { fclose($this->fh); } $this->fp = null; } }
使用方法
将上面的模块代码保存为 IPQuery.class.php
,然后按照如下方法调用即可:
<?php require_once('IPQuery.class.php'); $ip = new IPQuery(); $addr = $ip->query('123.233.233.233'); echo "<pre> IP起始段:{$addr['beginip']} IP结束段:{$addr['endip']} 实际地址:{$addr['pos']} 运 营 商:{$addr['isp']} </pre>";
输出效果如下所示:
注意事项
本模块会在第一次被调用时自动从纯真网下载最新的 IP 数据库到本地,因此第一次进行查询时会有点慢。如果你的服务器因为某些原因,无法连接到纯真网获取数据库,可以直接下载离线版,并将 IPQuery.class.php
第 25 行的 $dbExpires
值改为“0”(即永不自动更新数据库)。
本文作者为mengkun,转载请注明。
最新的纯真ip库改了格式,此代码已经获取不到ip地址
@笑更改一段代码即可 编码问题而已
@fate[aru_15] 您好 ,求教编码如何修改 可以识别到现在的 版本呢
@虔诚的教徒把iconv('gb2312', 'utf-8', $v)改成iconv('GBK', 'utf-8', $v)
不错不错,正在使用
还有一个GeoIP的工具 不过感觉那个精度很低