首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 使用 QQWry.dat 實現 IP 地址地理位置查詢

     編輯:PHP 相關知識

QQWry.dat 簡介

QQWry.dat (纯真 IP 資料庫) 是國內常用的離線 IP 地址庫,包含 IP 範圍對應的國家、地區、城市、ISP 等資訊。格式為二進位,需特定解析演算法。

資料檔案取得

  • 官方更新較慢,建議使用維護較新的鏡像:
  • out0fmemory/qqwry.dat (每週更新)
  • ip2region (更現代、支援多語言、效能更好)

核心解析類實作

<?php
/**
 * QQWry.dat 解析器
 * 支援 IP 查詢地理位置 (國家/地區)
 */
class IpLocation
{
    private $fp;
    private $firstIpOffset;
    private $lastIpOffset;
    private $totalRecords;

    public function __construct(string $datFile = 'qqwry.dat')
    {
        if (!file_exists($datFile)) {
            throw new InvalidArgumentException("IP 資料庫檔案不存在: $datFile");
        }
        $this->fp = fopen($datFile, 'rb');
        if (!$this->fp) {
            throw new RuntimeException("無法開啟 IP 資料庫: $datFile");
        }
        
        // 檔案頭:前 4 bytes 為第一條記錄偏移,後 4 bytes 為最後一條記錄偏移
        $this->firstIpOffset = $this->readLong();
        $this->lastIpOffset  = $this->readLong();
        $this->totalRecords  = ($this->lastIpOffset - $this->firstIpOffset) / 7;  // 每條索引 7 bytes
    }

    // 讀取 4 bytes 無號長整數 (Little Endian)
    private function readLong(): int
    {
        $bytes = fread($this->fp, 4);
        return unpack('V', $bytes)[1];
    }

    // 讀取 3 bytes 偏移量 (Little Endian + 補 0)
    private function readLong3(): int
    {
        $bytes = fread($this->fp, 3) . "\x00";
        return unpack('V', $bytes)[1];
    }

    // 讀取以 \0 結尾的字串 (GB2312 → UTF-8)
    private function readString(string $prefix = ''): string
    {
        $str = $prefix;
        while (($char = fread($this->fp, 1)) !== false) {
            if ($char === "\x00") break;
            $str .= $char;
        }
        return mb_convert_encoding($str, 'UTF-8', 'GB2312');
    }

    // 讀取區域資訊 (處理重定向)
    private function readArea(): string
    {
        $flag = fread($this->fp, 1);
        if ($flag === false) return '';
        
        switch (ord($flag)) {
            case 0:  // 無區域資訊
                return '';
            case 1:  // 區域重定向
            case 2:  // 國家+區域重定向
                fseek($this->fp, $this->readLong3());
                return $this->readString();
            default: // 直接儲存區域字串
                return $this->readString($flag);
        }
    }

    // IP 字串轉 4 bytes Big Endian (用於比較)
    private function packIp(string $ip): string
    {
        return pack('N', ip2long($ip));
    }

    // 核心查詢:二分搜尋
    public function getLocation(string $ip): ?array
    {
        if (!$this->fp) return null;

        // 域名解析
        $ipAddr = gethostbyname($ip);
        if ($ipAddr === $ip && !filter_var($ip, FILTER_VALIDATE_IP)) {
            return null;  // 無效 IP/域名
        }

        $packedIp = $this->packIp($ipAddr);
        
        // 二分搜尋索引區
        $low = 0;
        $high = $this->totalRecords;
        $foundOffset = $this->lastIpOffset;  // 預設最後一條 (版本資訊)

        while ($low <= $high) {
            $mid = (int)(($low + $high) / 2);
            fseek($this->fp, $this->firstIpOffset + $mid * 7);
            
            $midStartIp = strrev(fread($this->fp, 4));  // Little Endian → Big Endian
            
            if ($packedIp < $midStartIp) {
                $high = $mid - 1;
            } else {
                // 讀取結束 IP
                $midEndIpOffset = $this->readLong3();
                fseek($this->fp, $midEndIpOffset);
                $midEndIp = strrev(fread($this->fp, 4));
                
                if ($packedIp > $midEndIp) {
                    $low = $mid + 1;
                } else {
                    // 命中範圍
                    $foundOffset = $this->firstIpOffset + $mid * 7;
                    break;
                }
            }
        }

        // 解析命中記錄
        fseek($this->fp, $foundOffset);
        $location = [
            'ip'      => $ipAddr,
            'beginIp' => long2ip($this->readLong()),
        ];

        $dataOffset = $this->readLong3();
        fseek($this->fp, $dataOffset);
        
        $location['endIp'] = long2ip($this->readLong());
        
        // 解析國家/區域 (複雜的重定向邏輯)
        $flag = fread($this->fp, 1);
        switch (ord($flag)) {
            case 1:  // 國家+區域皆重定向
                $countryOffset = $this->readLong3();
                fseek($this->fp, $countryOffset);
                $flag2 = fread($this->fp, 1);
                if (ord($flag2) === 2) {  // 國家再重定向
                    fseek($this->fp, $this->readLong3());
                    $location['country'] = $this->readString();
                    fseek($this->fp, $countryOffset + 4);
                } else {
                    $location['country'] = $this->readString($flag2);
                }
                $location['area'] = $this->readArea();
                break;
            case 2:  // 國家重定向
                fseek($this->fp, $this->readLong3());
                $location['country'] = $this->readString();
                fseek($this->fp, $dataOffset + 8);
                $location['area'] = $this->readArea();
                break;
            default: // 直接儲存
                $location['country'] = $this->readString($flag);
                $location['area'] = $this->readArea();
                break;
        }

        // 清理純真特有標記
        if (strpos($location['country'], 'CZ88.NET') !== false) $location['country'] = '未知';
        if (strpos($location['area'], 'CZ88.NET') !== false) $location['area'] = '';

        return $location;
    }

    public function __destruct()
    {
        if ($this->fp) fclose($this->fp);
    }
}

使用範例

<?php
require_once 'IpLocation.php';

$iploc = new IpLocation(__DIR__ . '/qqwry.dat');

// 查詢單個 IP
$result = $iploc->getLocation('114.114.114.114');
print_r($result);
/*
Array
(
    [ip] => 114.114.114.114
    [beginIp] => 114.114.114.0
    [endIp] => 114.114.114.255
    [country] => 中国
    [area] => 南京市 电信DNS服务器
)
*/

// 批次查詢
$ips = ['8.8.8.8', '1.1.1.1', '223.5.5.5', 'invalid'];
foreach ($ips as $ip) {
    $loc = $iploc->getLocation($ip);
    echo $ip . ' => ' . ($loc ? $loc['country'] . ' ' . $loc['area'] : '查詢失敗') . "\n";
}

效能優化建議

現代替代方案:ip2region


# 下載 xdb 檔案
wget https://github.com/lionsoul2014/ip2region/raw/master/data/ip2region.xdb

# PHP 客戶端
composer require segrex/ip2region-php

# 使用
$searcher = new \Ip2Region\Database\Searcher('/path/to/ip2region.xdb');
$result = $searcher->search('114.114.114.114');
// ['region' => '中国|0|江苏省|南京市|电信', 'io_count' => 3]

注意事項

  • ⚠️ 資料時效性:QQWry 更新頻率低,IP 歸屬變更無法即時反映
  • ⚠️ 精度限制:僅到城市/ISP 級,無法定位到街道/經緯度
  • ⚠️ IPv6 不支援:QQWry 僅含 IPv4,IPv6 需用 ip2region 或 MaxMind GeoIP2
  • ⚠️ 授權問題:商業用途請確認授權,或使用 MaxMind GeoLite2 (免費但需註冊)
  • 💡 推薦生產環境:使用 MaxMind GeoIP2 (準確度高、官方維護、PHP 官方擴展 geoip2) 或 ip2region (純 PHP、高效、無外部依賴)
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
策略說明
單例模式解析器實例化一次,複用檔案指標,避免重複開檔
記憶體映射大檔案可用 mmap 或將索引區載入記憶體陣列
快取熱點 IPRedis 快取近期查詢結果,TTL 1 天
改用 ip2regionB+ 樹索引、二分搜尋更快、支援 xdb 格式、多語言