純真 IP 數據庫 (qqwry.dat) 是國內廣泛使用的離線 IP 歸屬地查詢庫。本文提供完整的 PHP 解析類,支持二分查找、遞歸重定向解析、GB2312 轉 UTF-8,可直接用於生產環境。
__construct():打開文件,讀取首尾索引偏移,計算總記錄數(每條 7 字節:4 字節起始 IP + 3 字節數據區偏移)。getLong4()/getLong3():小端序讀取 4/3 字節無符號整數。ip2addr():核心查詢入口,二分查找定位記錄,解析國家/地區信息。getArea():處理地區信息的重定向(模式 1/2)與直接讀取。__destruct():關閉文件句柄。<?php
class IPLocation {
private $fh; // 文件句柄
private $first; // 首索引偏移
private $last; // 尾索引偏移
private $total; // 總記錄數
public function __construct($datPath = 'qqwry.dat') {
if (!file_exists($datPath)) throw new Exception("IP 數據庫文件不存在: $datPath");
$this->fh = fopen($datPath, 'rb');
$this->first = $this->readLong4();
$this->last = $this->readLong4();
$this->total = ($this->last - $this->first) / 7;
}
// 小端序讀取 4 字節
private function readLong4() {
return unpack('V', fread($this->fh, 4))[1];
}
// 小端序讀取 3 字節(補齊 1 字節 0)
private function readLong3() {
return unpack('V', fread($this->fh, 3) . chr(0))[1];
}
// 讀取以 \0 結束的字符串
private function readString($prefix = '') {
$str = $prefix;
while (($char = fread($this->fh, 1)) !== false && $char !== "\0") {
$str .= $char;
}
return $str;
}
// 解析地區信息(處理重定向模式 1/2)
private function parseArea() {
$flag = ord(fread($this->fh, 1));
switch ($flag) {
case 0: return ''; // 無地區信息
case 1: // 地區重定向
case 2:
fseek($this->fh, $this->readLong3());
return $this->readString();
default: // 直接讀取
return $this->readString($flag);
}
}
// 核心查詢
public function lookup($ip) {
if (!$this->validIp($ip)) return false;
$ipBin = pack('N', ip2long($ip)); // 網絡序(大端)
// 二分查找
$l = 0; $r = $this->total;
while ($l <= $r) {
$m = (($l + $r) >> 1);
fseek($this->fh, $this->first + $m * 7);
$startIp = strrev(fread($this->fh, 4)); // 索引中 IP 為小端,轉大端比較
$dataOffset = $this->readLong3();
fseek($this->fh, $dataOffset);
$endIp = strrev(fread($this->fh, 4));
if ($ipBin < $startIp) {
$r = $m - 1;
} elseif ($ipBin > $endIp) {
$l = $m + 1;
} else {
// 命中,解析詳細信息
return $this->parseRecord($dataOffset);
}
}
return false;
}
private function parseRecord($dataOffset) {
fseek($this->fh, $dataOffset);
$loc = [];
$loc['start_ip'] = long2ip($this->readLong4());
$countryOffset = $this->readLong3();
fseek($this->fh, $countryOffset);
$flag = ord(fread($this->fh, 1));
switch ($flag) {
case 1: // 國家和地區都重定向
$countryRedirect = $this->readLong3();
fseek($this->fh, $countryRedirect);
$flag2 = ord(fread($this->fh, 1));
if ($flag2 == 2) { // 國家二次重定向
fseek($this->fh, $this->readLong3());
$loc['country'] = $this->readString();
fseek($this->fh, $countryRedirect + 4);
} else {
$loc['country'] = $this->readString($flag2);
}
$loc['area'] = $this->parseArea();
break;
case 2: // 國家重定向
fseek($this->fh, $this->readLong3());
$loc['country'] = $this->readString();
fseek($this->fh, $countryOffset + 8);
$loc['area'] = $this->parseArea();
break;
default: // 國家直接讀取
$loc['country'] = $this->readString($flag);
$loc['area'] = $this->parseArea();
}
$loc['end_ip'] = long2ip($this->readLong4()); // 讀取結束 IP(其實在國家信息前已讀過,此處補全)
// 編碼轉換 & 清洗
foreach ($loc as &$v) {
$v = str_replace('CZ88.NET', '', iconv('GB2312', 'UTF-8//IGNORE', $v));
}
return $loc;
}
private function validIp($ip) {
$parts = explode('.', $ip);
if (count($parts) !== 4) return false;
foreach ($parts as $p) if ($p < 0 || $p > 255) return false;
return true;
}
public function __destruct() {
if ($this->fh) fclose($this->fh);
}
}
<?php
require 'IPLocation.php';
$ip = new IPLocation(__DIR__ . '/qqwry.dat');
$result = $ip->lookup('114.114.114.114');
print_r($result);
/*
Array
(
[start_ip] => 114.114.114.0
[country] => 中国 江苏 南京 电信
[area] =>
[end_ip] => 114.114.114.255
)
*/
qqwry.dat 讀入內存(file_get_contents),用字符串偏移替換 fseek/fread,大幅提升查詢速度(適合高併發)。本類完整實現了 qqwry.dat 的解析邏輯,代碼清晰、註釋詳盡,可直接集成。對於高併發場景,強烈建議採用內存映射版本。