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

PHP 純真 IP 庫(qqwry.dat)離線查詢實現:二分搜索 + 記錄區解析完整代碼

     編輯:PHP 相關知識

純真 IP 數據庫(qqwry.dat)是國內最經典的離線 IP 歸屬地查詢庫,體積小(~8MB)、無網絡依賴、查詢快(毫秒級)。本文深度解析其文件格式,提供可直接生產使用的 PHP 類,支援 IPv4 查詢、域名自動解析、編碼自動轉換。

qqwry.dat 文件結構速覽

區塊偏移結構說明
文件頭0-7首索引偏移(4B) + 尾索引偏移(4B)小端序
索引區首索引~尾索引每條 7B:起始IP(4B) + 記錄區偏移(3B)按起始IP升序,固定長度
記錄區索引區後結束IP(4B) + 國家/地區1 + 國家/地區2字符串以 \0 結尾,可能有重定向標誌 0x01/0x02

核心類完整代碼

<?php
class IpLocation {
    private $fp;
    private int $firstIndexOffset;
    private int $lastIndexOffset;
    private int $totalRecords;

    public function __construct(string $datFile = 'qqwry.dat') {
        if (!file_exists($datFile)) {
            throw new RuntimeException("qqwry.dat 不存在,請下載至當前目錄");
        }
        $this->fp = fopen($datFile, 'rb');
        $this->firstIndexOffset = $this->readInt4();
        $this->lastIndexOffset  = $this->readInt4();
        $this->totalRecords = ($this->lastIndexOffset - $this->firstIndexOffset) / 7;
        register_shutdown_function([$this, 'close']);
    }

    public function close(): void { fclose($this->fp); }

    // 讀 4 字節無符號整數(小端序)
    private function readInt4(): int {
        $bin = fread($this->fp, 4);
        return unpack('V', $bin)[1]; // V = little-endian unsigned long
    }

    // 讀 3 字節偏移(補齊 4 字節再 unpack)
    private function readOffset3(): int {
        $bin = fread($this->fp, 3) . "\0";
        return unpack('V', $bin)[1];
    }

    // 讀取以 \0 結尾的字符串
    private function readString(): string {
        $str = '';
        while (($ch = fread($this->fp, 1)) !== "\0") {
            $str .= $ch;
        }
        return $str;
    }

    // 讀取標誌位(1 字節)
    private function readFlag(): string { return fread($this->fp, 1); }

    // IP 字串轉 4 字節大端序二進制(用於與索引區比較)
    private function ip2bin(string $ip): string {
        return pack('N', ip2long($ip)); // N = big-endian unsigned long
    }

    // 二分查找索引區
    private function searchIndex(string $ipBin): int {
        $ip = $ipBin;
        $low = 0;
        $high = $this->totalRecords - 1;
        $offset = $this->lastIndexOffset; // 默認最後一條

        while ($low <= $high) {
            $mid = (($low + $high) >> 1);
            fseek($this->fp, $this->firstIndexOffset + $mid * 7);
            $startIpBin = fread($this->fp, 4); // 小端序
            $startIpBin = strrev($startIpBin); // 轉大端序比較

            if ($ip < $startIpBin) {
                $high = $mid - 1;
            } else {
                // 讀取記錄區偏移,獲取結束 IP
                fseek($this->fp, $this->firstIndexOffset + $mid * 7 + 4);
                $recOffset = $this->readOffset3();
                fseek($this->fp, $recOffset);
                $endIpBin = strrev(fread($this->fp, 4));
                if ($ip > $endIpBin) {
                    $low = $mid + 1;
                } else {
                    $offset = $this->firstIndexOffset + $mid * 7;
                    break;
                }
            }
        }
        return $offset;
    }

    // 解析記錄區(處理重定向 0x01/0x02)
    private function parseRecord(int $recOffset): array {
        fseek($this->fp, $recOffset);
        $endIp = long2ip($this->readInt4());
        $flag = ord($this->readFlag());
        $country = $area = '';

        switch ($flag) {
            case 0x01: // 國家重定向
                $countryOffset = $this->readOffset3();
                $flag2 = ord($this->readFlag());
                if ($flag2 === 0x02) { // 雙重定向
                    $areaOffset = $this->readOffset3();
                    fseek($this->fp, $countryOffset);
                    $country = $this->readString();
                    fseek($this->fp, $areaOffset);
                    $area = $this->readString();
                } else {
                    fseek($this->fp, $countryOffset);
                    $country = $this->readString();
                    $area = $this->parseRecord($recOffset + 4)['area']; // 區域可能再重定向
                }
                break;
            case 0x02: // 區域重定向
                $areaOffset = $this->readOffset3();
                $country = $this->readString();
                fseek($this->fp, $areaOffset);
                $area = $this->readString();
                break;
            default: // 無重定向
                fseek($this->fp, $recOffset + 4);
                $country = $this->readString();
                $area = $this->readString();
                break;
        }
        // 過濾純真廣告
        if (strpos($country, 'cz88.net') !== false) $country = '未知';
        if (strpos($area, 'cz88.net') !== false) $area = '';
        return ['country' => $country, 'area' => $area, 'end_ip' => $endIp];
    }

    // 公開查詢接口
    public function query(string $ipOrDomain): ?array {
        $ip = gethostbyname($ipOrDomain); // 域名自動解析
        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return null;
        $ipBin = $this->ip2bin($ip);
        $indexOffset = $this->searchIndex($ipBin);
        fseek($this->fp, $indexOffset);
        $startIp = long2ip($this->readInt4());
        fseek($this->fp, $indexOffset + 4);
        $recOffset = $this->readOffset3();
        $record = $this->parseRecord($recOffset);
        return [
            'ip' => $ip,
            'start_ip' => $startIp,
            'end_ip' => $record['end_ip'],
            'country' => $record['country'],
            'area' => $record['area'],
        ];
    }
}

// 使用示例
$ip = new IpLocation('qqwry.dat');
print_r($ip->query('61.129.51.27'));
print_r($ip->query('www.baidu.com'));

關鍵技術點解析

  1. 字節序轉換:索引區 IP 存儲為小端序,網絡序為大端序,比較前必須 strrev() 或 pack/unpack('N'/'V') 統一。
  2. 3 字節偏移讀取:記錄區偏移只佔 3 字節,讀取時補 \0 再 unpack('V')。
  3. 重定向鏈處理:標誌 0x01(國家重定向)、0x02(區域重定向)、0x01+0x02(雙重定向)需遞歸/迭代解析,防止死循環。
  4. 域名解析:gethostbyname() 支援直接傳域名,內部自動 DNS 查詢。
  5. 資源管理:register_shutdown_function 確保腳本結束時關閉文件句柄,兼容 PHP 5/7/8。

效能與避坑

  • 單次查詢 ~0.5-1ms(SSD),適合高併發場景。
  • 記憶體佔用極低(僅打開文件句柄),可長駐進程(Swoole/RoadRunner)。
  • 定期更新:純真庫約每月更新,建立定時任務下載最新 qqwry.dat 覆蓋舊文件(原子性 rename)。
  • IPv6 不支援:qqwry 僅含 IPv4,IPv6 需改用 MaxMind GeoIP2 或 ip2region。
  • 總結

    純真 IP 庫配合二分搜索是 PHP 離線 IP 歸屬地查詢的黃金組合。本文提供的 IpLocation 類已處理字節序、重定向、編碼、資源管理等所有細節,可直接納入項目。若需更高性能,可考慮將索引區載入記憶體陣列,查詢時純內存二分,進一步降低至 ~0.1ms。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved