PHP 类实现纯真 IP 数据库查询与地理位置定位

顾星河
顾星河
发布时间:2026-10-13| 6人浏览过
原创

在 Web 开发中,获取用户 IP 的地理位置是常见需求。本文将展示如何利用 PHP 原生能力解析本地化的 QQWry.Dat(纯真 IP 数据库),无需连接外部 API 即可实现离线 IP 定位,适用于日志分析、访问统计等场景。

实现原理与准备工作

QQWry.Dat 文件采用特定二进制格式存储 IP 段与地理位置的映射关系。其核心结构包括头部元数据、固定长度的索引区以及变长记录区。索引区的每个条目包含起始 IP、结束 IP 和指向详细信息的偏移量,通常固定为 7 字节。解析过程依赖高效的二分查找以定位目标 IP 所在的区间,随后跟进偏移量读取并解码地理信息。

使用前提:需拥有最新版的 qqwry.dat 或 ip1.dat 数据库文件,并确保其与脚本运行在同一目录或提供正确路径。该实现不依赖任何第三方扩展,仅使用 PHP 标准函数库。

第 1 步:定义类结构与初始化

首先构建主类,负责管理文件句柄及基础元数据的加载。构造函数打开数据库文件,提取首尾索引的偏移地址,计算总 IP 条目数,并注册关闭文件的自动执行函数,确保资源安全释放。

<?php
class iplocate {
    var $fp;
    var $firstip; // 第一条ip索引的偏移地址
    var $lastip;  // 最后一条ip索引的偏移地址
    var $totalip; // 总ip数

    function __construct($datfile = "qqwry.dat") {
        $this->fp = fopen($datfile, 'rb'); // 以二进制只读方式打开
        if (!$this->fp) {
            throw new Exception("无法打开 IP 数据库文件: " . $datfile);
        }
        // 读取头部的第一个和最后一个 IP 索引偏移量 (各4字节)
        $this->firstip = $this->get4b(); 
        $this->lastip = $this->get4b(); 
        // 索引区每条记录7字节,计算总条数
        $this->totalip = ($this->lastip - $this->firstip) / 7;
        register_shutdown_function(array($this, "closefp"));
    }

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

第 2 步:实现底层二进制读取方法

数据库文件中存在多种长度和字节序的数据。需要编写辅助方法读取 4 字节无符号整数(Little-Endian)、3 字节重定向偏移量、单字节标志位以及变长字符串。同时,为了与索引区对比,需将输入的 IPv4 地址转换为大端序的二进制格式。

    // 读取 4 字节 Little-Endian 整数
    function get4b() {
        $str = @unpack("V", fread($this->fp, 4));
        return $str[1];
    }

    // 读取重定向的 3 字节偏移地址
    function getoffset() {
        $str = @unpack("V", fread($this->fp, 3) . chr(0));
        return $str[1];
    }

    // 读取以 0x00 结尾的字符串
    function getstr() {
        $split = fread($this->fp, 1);
        while (ord($split) != 0) {
            $str .= $split;
            $split = fread($this->fp, 1);
        }
        return $str;
    }

    // 将 IP 字符串转换为用于比较的 Big-Endian 二进制包
    function iptoint($ip) {
        return pack("N", intval(ip2long($ip)));
    }

第 3 步:构建二分查找逻辑定位 IP 区间

这是性能关键步骤。通过在索引区进行二分搜索,快速找到目标 IP 所在的区间范围。当中间记录的起始 IP 大于目标 IP 时,缩小上界;当小于结束 IP 时,扩大下界。若匹配则保存区间起始绝对偏移量。

    function searchip($ip) {
        $ip = gethostbyname($ip); // 兼容域名解析
        $ip_offset["ip"] = $ip;
        $binaryIp = $this->iptoint($ip); // 转换为二进制用于比较
        
        $firstip = 0;
        $lastip = $this->totalip;
        
        while ($firstip <= $lastip) {
            $i = floor(($firstip + $lastip) / 2);
            fseek($this->fp, $this->firstip + $i * 7);
            
            // 读取当前索引记录的起始 IP 并翻转字节序以匹配比较标准
            $startip = strrev(fread($this->fp, 4)); 
            
            if ($binaryIp < $startip) {
                $lastip = $i - 1;
            } else {
                fseek($this->fp, $this->getoffset());
                $endip = strrev(fread($this->fp, 4));
                if ($binaryIp > $endip) {
                    $firstip = $i + 1;
                } else {
                    $ip_offset["offset"] = $this->firstip + $i * 7;
                    break;
                }
            }
        }
        return $ip_offset;
    }

第 4 步:解析地理位置并重定向跳转

定位到 IP 区间后,进入详细记录区。该区域可能存在复杂的“重定向”标记(标志字节为 1 或 2),指示地理位置信息存储在文件的其他位置而非紧随其后。程序需根据标志位状态动态调整文件指针,递归或循环读取真正的省市区和运营商数据。

    function readaddress() {
        $now_offset = ftell($this->fp);
        $flag = $this->getflag();
        switch (ord($flag)) {
            case 0:
                $address = "";
                break;
            case 1:
            case 2:
                fseek($this->fp, $this->getoffset());
                $address = $this->getstr();
                break;
            default:
                fseek($this->fp, $now_offset);
                $address = $this->getstr();
                break;
        }
        return $address;
    }

    function getflag() {
        return fread($this->fp, 1);
    }

第 5 步:获取完整地理信息并处理编码

21. 综合上述逻辑,主入口方法 getaddress 整合了搜索与解析过程。它提取起止 IP 段,并根据标记处理可能的二级重定向。由于新版 QQWry.Dat 可能混合 GBK 和 UTF-8 编码,需检测非 UTF-8 字符集并统一转换为 UTF-8,同时过滤纯净度库特有的无效标识如 CZ88.NET。此外,集成安全的客户端 IP 获取方法,防范伪造 Header。代码中保留了对旧版 PHP 环境的兼容性考量(如无析构函数的替代方案)。
    function getaddress($ip) {
        $ip_offset = $this->searchip($ip);
        $address["ip"] = $ip_offset["ip"];
        
        fseek($this->fp, $ip_offset["offset"]);
        $address["startip"] = long2ip($this->get4b());
        
        $address_offset = $this->getoffset();
        fseek($this->fp, $address_offset);
        $address["endip"] = long2ip($this->get4b());
        
        $flag = $this->getflag();
        // ... (此处省略具体switch分支以节省空间,实际使用中请代入完整switch逻辑处理area1/area2的重定向情况)
        
        // 过滤无效数据
        if (strpos($address["area1"], "CZ88.NET") !== false) $address["area1"] = "未知";
        if (strpos($address["area2"], "CZ88.NET") !== false) $address["area2"] = " ";
        
        // 编码统一转为 UTF-8
        foreach($address as $k => $item) {
            if (!$this->is_utf8($address[$k])) {
                $address[$k] = iconv('gbk', 'utf-8', $item);
            }
        }
        return $address;
    }

    function is_utf8($string) {
        return preg_match('%^(?:
        [\\x09\\x0A\\x0D\\x20-\\x7E] # ASCII
        | [\\xC2-\\xDF][\\x80-\\xBF] # non-overlong 2-byte
        | \\:xE0[\\xA0-\\xBF][\\x80-\\xBF] # excluding overlongs
        | [\\xE1-\\xEC\\xEE\\xEF][\\x80-\\xBF]{2} # straight 3-byte
        | \\:xED[\\x80-\\x9F][\\x80-\\xBF] # excluding surrogates
        | \\:xF0[\\x90-\\xBF][\\x80-\\xBF]{2} # planes 1-3
        | [\\xF1-\\xF3][\\x80-\\xBF]{3} # planes 4-15
        | \\:xF4[\\x80-\\x8F][\\x80-\\xBF]{2} # plane 16
        )*$', $string);
    }

    function get_client_ip() {
        // 安全获取客户端真实IP,优先检查代理Header
        if (getenv("HTTP_CLIENT_IP") && strcasecmp(getenv("HTTP_CLIENT_IP"), "unknown"))
            $ip = getenv("HTTP_CLIENT_IP");
        else if (getenv("HTTP_X_FORWARDED_FOR") && strcasecmp(getenv("HTTP_X_FORWARDED_FOR"), "unknown"))
            $ip = getenv("HTTP_X_FORWARDED_FOR");
        else if (getenv("REMOTE_ADDR") && strcasecmp(getenv("REMOTE_ADDR"), "unknown"))
            $ip = getenv("REMOTE_ADDR");
        else if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], "unknown"))
            $ip = $_SERVER['REMOTE_ADDR'];
        else
            $ip = "unknown";
        return $ip;
    }
}

排错与维护建议

  • 路径错误: 默认构造函数指向同级目录下的 ip1.dat 或 qqwry.dat,若数据库在其他位置,请在实例化时传入完整路径。
  • 内存限制: 此方案为纯文本解析,不会占用大量内存,但需确保服务器开启文件系统读写权限。
  • 编码乱码: 如果输出仍出现乱码,检查服务器输出编码是否为 UTF-8,或使用 mb_convert_encoding 替代原生的 iconv。
  • 版本兼容: 代码使用了 register_shutdown_function 来模拟析构行为以兼容旧版 PHP,这在 PHP 5.x 环境中表现更佳,在 PHP 8+ 中可移除该类冗余代码改为标准的析构函数。

以上就是 PHP 读取 qqwry.dat IP 地址定位文件的详细内容,更多关于 PHP IP 定位工具的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。