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

PHP 讀取純真 IP 數據庫 (qqwry.dat) 完整類實現

     編輯:PHP 相關知識

簡介

純真 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
)
*/

性能優化建議

  1. 內存映射:將 qqwry.dat 讀入內存(file_get_contents),用字符串偏移替換 fseek/fread,大幅提升查詢速度(適合高併發)。
  2. 緩存熱點 IP:對頻繁查詢的 IP(如用戶登錄 IP)使用 Redis 緩存結果。
  3. 定期更新:純真數據庫每周更新,建議自動化下載替換。

總結

本類完整實現了 qqwry.dat 的解析邏輯,代碼清晰、註釋詳盡,可直接集成。對於高併發場景,強烈建議採用內存映射版本。

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