純真 IP 庫是騰訊 QQ 旗下的產品,開放給大眾使用的 IP 數據庫,純真 IP 庫是一個輕量級的 IP 數據庫,數據格式簡單,查詢速度快,功能全面,也是免費共享任用的,很適合 Web 應用;IP 查詢工具經常需要根據使用者的 IP 地址,獲取其地理位置資訊,例如國家、省份、城市等信息;純真 IP 庫(QQWry)是一個常用的 IP 地址資料庫,提供了豐富的 IP 位址與地理位置的映射關係, 本文將介紹如何使用 PHP 接入純真 IP 庫,並通過一個完整的案例演示如何實現 IP 位址的地理位置,從而調用純真 IP 庫獲取國家地區歸屬地等查詢。
純真 IP 庫 (QQWry.dat) 你可以前往純真網站或 GitHub 下載。
純真官方網站:cz88.net
純真 IP 社區版:cz88.net/geo-public
IP 查詢應用可利用純真 IP 庫的 DAT 數據格式,編寫一個 PHP 類來實現 IP 地址的查詢功能。
完整代碼如下:
01. 將以下代碼儲存成「cz88.php」檔案文件
<?php
error_reporting(0); // 關閉錯誤報告
class QQWry {
private $file; // IP數據庫文件路徑
private $fd; // 文件句柄
private $total; // 總記錄數
// 索引區
private $indexStartOffset; // 索引區起始偏移量
private $indexEndOffset; // 索引區結束偏移量
/**
* 構造函數,初始化IP數據庫
* @param string $file IP數據庫文件路徑
* @throws Exception 文件不存在或不可讀時拋出異常
*/
public function __construct($file) {
if (!file_exists($file) || !is_readable($file)) {
throw new Exception("{$file} does not exist, or is not readable");
}
$this->file = $file;
$this->fd = fopen($file, 'rb');
// 讀取索引區起始偏移量
$this->indexStartOffset = $this->unpackLong($this->readOffset(4, 0));
// 讀取索引區結束偏移量
$this->indexEndOffset = $this->unpackLong($this->readOffset(4));
// 計算總記錄數
$this->total = ($this->indexEndOffset - $this->indexStartOffset) / 7 + 1;
}
/**
* 查詢IP地址對應的地理位置
* @param string $ip IP地址
* @return array 包含國家和地區的數組
* @throws Exception IP地址無效時拋出異常
*/
public function query($ip) {
// 驗證IP地址格式
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
throw new Exception("{$ip} is not a valid IP address");
}
// 將IP地址轉換為整數
$ipNum = ip2long($ip);
// 查找IP所在的索引
$ipFind = $this->find($ipNum, 0, $this->total);
// 計算IP記錄的偏移量
$ipOffset = $this->indexStartOffset + $ipFind * 7 + 4;
$ipRecordOffset = $this->unpackLong($this->readOffset(3, $ipOffset) . chr(0));
// 讀取並返回IP記錄
return $this->readRecord($ipRecordOffset);
}
/**
* 讀取IP記錄
* @param int $offset 記錄偏移量
* @return array 包含國家和地區的數組
*/
private function readRecord($offset) {
$record = ['', ''];
$offset += 4;
$flag = ord($this->readOffset(1, $offset));
if ($flag == 1) {
$locationOffset = $this->unpackLong($this->readOffset(3, $offset + 1) . chr(0));
$subFlag = ord($this->readOffset(1, $locationOffset));
if ($subFlag == 2) {
// 國家
$countryOffset = $this->unpackLong($this->readOffset(3, $locationOffset + 1) . chr(0));
$record[0] = $this->readLocation($countryOffset);
// 地區
$record[1] = $this->readLocation($locationOffset + 4);
} else {
$record[0] = $this->readLocation($locationOffset);
$record[1] = $this->readLocation($locationOffset + strlen($record[0]) + 1);
}
} elseif ($flag == 2) {
// 地區
$record[1] = $this->readLocation($offset + 4);
// 國家
$countryOffset = $this->unpackLong($this->readOffset(3, $offset + 1) . chr(0));
$record[0] = $this->readLocation($countryOffset);
} else {
$record[0] = $this->readLocation($offset);
$record[1] = $this->readLocation($offset + strlen($record[0]) + 1);
}
return $record;
}
/**
* 讀取地區信息
* @param int $offset 地區偏移量
* @return string 地區信息
*/
private function readLocation($offset) {
if ($offset == 0) {
return '';
}
$flag = ord($this->readOffset(1, $offset));
// 出錯
if ($flag == 0) {
return '';
}
// 仍然為重定向
if ($flag == 2) {
$offset = $this->unpackLong($this->readOffset(3, $offset + 1) . chr(0));
return $this->readLocation($offset);
}
$location = '';
$chr = $this->readOffset(1, $offset);
while (ord($chr) != 0) {
$location .= $chr;
$offset++;
$chr = $this->readOffset(1, $offset);
}
return iconv('GBK', 'UTF-8//IGNORE', $location);
}
/**
* 查找IP所在的索引
* @param int $ipNum IP地址的整數表示
* @param int $l 左邊界
* @param int $r 右邊界
* @return int 找到的索引
*/
private function find($ipNum, $l, $r) {
if ($l + 1 >= $r) {
return $l;
}
$m = intval(($l + $r) / 2);
$find = $this->readOffset(4, $this->indexStartOffset + $m * 7);
$mIp = $this->unpackLong($find);
if ($ipNum < $mIp) {
return $this->find($ipNum, $l, $m);
} else {
return $this->find($ipNum, $m, $r);
}
}
/**
* 讀取指定偏移量的數據
* @param int $numberOfBytes 要讀取的字節數
* @param int|null $offset 偏移量
* @return string 讀取的數據
*/
private function readOffset($numberOfBytes, $offset = null) {
if (!is_null($offset)) {
fseek($this->fd, $offset);
}
return fread($this->fd, $numberOfBytes);
}
/**
* 將4字節的字符串轉換為長整型
* @param string $str 4字節的字符串
* @return int 長整型
*/
private function unpackLong($str) {
return unpack('L', $str)[1];
}
/**
* 析構函數,關閉文件句柄
*/
public function __destruct() {
if ($this->fd) {
fclose($this->fd);
}
}
}
?>
02. 將以下代碼儲存成「index.php」檔案文件
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="zh-TW">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title>查詢IP位址</title>
</head>
<body>
<?php
function getClientIp() {
$ipKeys = [
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'HTTP_CF_CONNECTING_IP', // 適配 Cloudflare
'REMOTE_ADDR'
];
foreach ($ipKeys as $key) {
if (!empty($_SERVER[$key])) {
// 有時標頭會包含多個逗號分隔的 IP,取第一個即可
$ipList = explode(',', $_SERVER[$key]);
foreach ($ipList as $ip) {
$ip = trim($ip);
// 驗證是否為有效的 IPv4 或 IPv6
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
}
return '0.0.0.0';
}
?>
<?php
require 'cz88.php';
try {
// 初始化IP庫
$qqwry = new QQWry('./src/qqwry.dat');
// 查詢IP地址
$ip = getClientIp(); // 要查詢的IP地址
$result = $qqwry->query($ip);
// 輸出結果
echo "IP: {$ip} \n";
echo "國家: {$result[0]} \n";
echo "地區: {$result[1]} \n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}
?>
</body>
</html>
提醒:其中「src/qqwry.dat」為放置純真 IP 的路徑,如有變更請自行修改相對路徑。
圖片預覧:

技術文檔:
01. PHP接入純真IP庫
純真 IP 查詢工具: