纯真IP数据库格式读取方法(JAVA/PHP/Python)

摘要:
第一个字节表示重定向模式。记录的格式为[IP地址]、[国家记录]、[地区记录]。图2显示了IP记录的最简单形式。图2显示了最简单的IP记录格式。我想没什么好解释的。图3。重定向模式1。图3显示了重定向模式1的情况。图4。重定向模式2图4说明了重定向模式2的情况。我们已经看到,在模式2的情况下(其标识字节为0×02),模式1的国家记录之后将没有区域记录,模式2的国家记录后将有区域记录。

一、基本结构

QQWry.dat文件在结构上分为3块:文件头,记录区,索引区。一般我们要查找IP时,先在索引区查找记录偏移,然后再到记录区读出信息。由于 记录区的记录是不定长的,所以直接在记录区中搜索是不可能的。由于记录数比较多,如果我们遍历索引区也会是有点慢的,一般来说,我们可以用二分查找法搜索 索引区,其速度比遍历索引区快若干数量级。图1是QQWry.dat的文件结构图。

纯真IP数据库格式读取方法(JAVA/PHP/Python)第1张

图1. QQWry.dat文件结构

要注意的是,QQWry.dat里面全部采用了little-endian字节序。

1、文件头

QQWry.dat的文件头只有8个字节,其结构非常简单,首四个字节是第一条索引的绝对偏移,后四个字节是最后一条索引的绝对偏移。

2、记录区

每条IP记录都由国家和地区名组成,国家地区在这里并不是太确切,因为可能会查出来“清华大学计算机系”之类的,这里清华大学就成了国家名了,所以 这个国家地区名和IP数据库制作的时候有关系。所以记录的格式有点像QName,有一个全局部分和局部部分组成,我们这里还是沿用国家名和地区名的说法。

于是我们想象着一条记录的格式应该是: [IP地址][国家名][地区名],当然,这个没有什么问题,但是这只是最简单的情况。很显然,国家名和地区名可能会有很多的重复,如果每条记录都保存一 个完整的名称拷贝是非常不理想的,所以我们就需要重定向以节省空间。所以为了得到一个国家名或者地区名,我们就有了两个可能:第一就是直接的字符串表示的 国家名,第二就是一个4字节的结构,第一个字节表明了重定向的模式,后面3个字节是国家名或者地区名的实际偏移位置。对于国家名来说,情况还可能更复杂 些,因为这样的重定向最多可能有两次。

那么什么是重定向模式?根据上面所说,一条记录的格式是[IP地址][国家记录][地区记录],如果国家记录是重定向的话,那么地区记录是有可能没有的,于是就有了两种情况,我管他叫做模式1和模式2。我们对这些格式的情况举图说明:

纯真IP数据库格式读取方法(JAVA/PHP/Python)第2张

图2. IP记录的最简单形式

图2表示了最简单的IP记录格式,我想没有什么可以解释的

纯真IP数据库格式读取方法(JAVA/PHP/Python)第3张

图3. 重定向模式1

图3演示了重定向模式1的情况。我们看到在模式1的情况下,地区记录也跟着国家记录走了,在IP地址之后只剩下了国家记录的4字节,后面3个字节构成了一个指针,指向了实际的国家名,然后又跟着地址名。模式1的标识字节是0×01。

纯真IP数据库格式读取方法(JAVA/PHP/Python)第4张

图4. 重定向模式2

图4演示了重定向模式2的情况。我们看到了在模式2的情况下(其标识字节是0×02),地区记录没有跟着国家记录走,因此在国家记录之后4个字节之 后还是有地区记录。我想你已经明白了模式1和模式2的区别,即:模式1的国家记录后面不会再有地区记录,模式2的国家记录后会有地区记录。下面我们来看一 下更复杂的情况。

纯真IP数据库格式读取方法(JAVA/PHP/Python)第5张

图5. 混和情况1

图5演示了当国家记录为模式1的时候可能出现的更复杂情况,在这种情况下,重定向指向的位置仍然是个重定向,不过第二次重定向为模式2。大家不用担 心,没有模式3了,这个重定向也最多只有两次,并且如果发生了第二次重定向,则其一定为模式2,而且这种情况只会发生在国家记录上,对于地区记录,模式1 和模式2是一样的,地区记录也不会发生2次重定向。不过,这个图还可以更复杂,如图7:

纯真IP数据库格式读取方法(JAVA/PHP/Python)第6张

图6. 混和情况2

图6是模式1下最复杂的混和情况,不过我想应该也很好理解,只不过地区记录也来重定向而已,有一点我要提醒你,如果重定向的地址是0,则表示未知的地区名。

所以我们总结如下:一条IP记录由[IP地址][国家记录][地区记录]组成,对于国家记录,可以有三种表示方式:字符串形式,重定向模式1和重定 向模式2。对于地区记录,可以有两种表示方式:字符串形式和重定向,另外有一条规则:重定向模式1的国家记录后不能跟地区记录。按照这个总结,在这些方式 中合理组合,就构成了IP记录的所有可能情况。

二、设计的理由

在我们继续去了解索引区的结构之前,我们先来了解一下为何记录区的结构要如此设计。我想你可能想到了答案:字符串重用。没错,在这种结构下,对于一 个国家名和地区名,我只需要保存其一次就可以了。我们举例说明,为了表示方便,我们用小写字母代表IP记录,C表示国家名,A表示地区名:

  1. 有两条记录a(C1, A1), b(C2, A2),如果C1 = C2, A1 = A2,那么我们就可以使用图3显示的结构来实现重用
  2. 有三条记录a(C1, A1), b(C2, A2), c(C3, A3),如果C1 = C2, A2 = A3,现在我们想存储记录b,那么我们可以用图6的结构来实现重用
  3. 有两条记录a(C1, A1), b(C2, A2),如果C1 = C2,现在我们想存储记录b,那么我们可以采用模式2表示C2,用字符串表示A2

你可以举出更多的情况,你也会发现在这种结构下,不同的字符串只需要存储一次。

三、索引区

在”了解文件头”部分,我们说明了文件头实际上是两个指针,分别指向了第一条索引和最后一条索引的绝对偏移。如图8所示:

纯真IP数据库格式读取方法(JAVA/PHP/Python)第7张

图8. 文件头指向索引区图示

实在是很简单,不是吗?从文件头你就可以定位到索引区,然后你就可以开始搜索IP了!每条索引长度为7个字节,前4个字节是起始IP地址,后三个字 节就指向了IP记录。这里有些概念需要说明一下,什么是起始IP,那么有没有结束IP? 假设有这么一条记录:166.111.0.0 – 166.111.255.255,那么166.111.0.0就是起始IP,166.111.255.255就是结束IP,结束IP就是IP记录中的那头 4个字节,这下你应该就清楚了吧。于是乎,每条索引配合一条记录,构成了一个IP范围,如果你要查找166.111.138.138所在的位置,你就会发 现166.111.138.138落在了166.111.0.0 – 166.111.255.255 这个范围内,那么你就可以顺着这条索引去读取国家和地区名了。那么我们给出一个最详细的图解吧:

纯真IP数据库格式读取方法(JAVA/PHP/Python)第8张

图9. 文件详细结构

现在一切都清楚了是不是?也许还有一点你不清楚,QQWry.dat的版本信息存在哪里呢? 答案是:最后一条IP记录实际上就是版本信息,最后一条记录显示出来就是这样:255.255.255.0 255.255.255.255 纯真网络 2004年6月25日IP数据。OK,到现在你应该全部清楚了。

四、使用示例

下一步:我给出一个读取IP记录的程序片断,此片断摘录自LumaQQ源文件edu.tsinghua.lumaqq.IPSeeker.java,如果你有兴趣,可以下载源代码详细看看。

/**
* 给定一个ip国家地区记录的偏移,返回一个IPLocation结构
* @param offset 国家记录的起始偏移
* @return IPLocation对象
*/
private IPLocation getIPLocation(long offset) {
 try {
  // 跳过4字节ip
  ipFile.seek(offset + 4);
  // 读取第一个字节判断是否标志字节
  byte b = ipFile.readByte();
  if(b == REDIRECT_MODE_1) {
   // 读取国家偏移
   long countryOffset = readLong3();
   // 跳转至偏移处
   ipFile.seek(countryOffset);
   // 再检查一次标志字节,因为这个时候这个地方仍然可能是个重定向
   b = ipFile.readByte();
   if(b == REDIRECT_MODE_2) {
    loc.country = readString(readLong3());
    ipFile.seek(countryOffset + 4);
   } else
   loc.country = readString(countryOffset);
   // 读取地区标志
   loc.area = readArea(ipFile.getFilePointer());
  } else if(b == REDIRECT_MODE_2) {
   loc.country = readString(readLong3());
   loc.area = readArea(offset + 8);
  } else {
   loc.country = readString(ipFile.getFilePointer() - 1);
   loc.area = readArea(ipFile.getFilePointer());
  }
  return loc;
 } catch (IOException e) {
  return null;
 }
}

/**
* 从offset偏移开始解析后面的字节,读出一个地区名
* @param offset 地区记录的起始偏移
* @return 地区名字符串
* @throws IOException 地区名字符串
*/
private String readArea(long offset) throws IOException {
 ipFile.seek(offset);
 byte b = ipFile.readByte();
 if(b == REDIRECT_MODE_1 || b == REDIRECT_MODE_2) {
  long areaOffset = readLong3(offset + 1);
  if(areaOffset == 0)
  return LumaQQ.getString("unknown.area");
  else
  return readString(areaOffset);
 } else
 return readString(offset);
}

/**
* 从offset位置读取3个字节为一个long,因为java为big-endian格式,所以没办法
* 用了这么一个函数来做转换
* @param offset 整数的起始偏移
* @return 读取的long值,返回-1表示读取文件失败
*/
private long readLong3(long offset) {
 long ret = 0;
 try {
  ipFile.seek(offset);
  ipFile.readFully(b3);
  ret |= (b3[0] & 0xFF);
  ret |= ((b3[1] << 8) & 0xFF00);
  ret |= ((b3[2] << 16) & 0xFF0000);
  return ret;
 } catch (IOException e) {
  return -1;
 }
}

/**
* 从当前位置读取3个字节转换成long
* @return 读取的long值,返回-1表示读取文件失败
*/
private long readLong3() {
 long ret = 0;
 try {
  ipFile.readFully(b3);
  ret |= (b3[0] & 0xFF);
  ret |= ((b3[1] << 8) & 0xFF00);
  ret |= ((b3[2] << 16) & 0xFF0000);
  return ret;
 } catch (IOException e) {
  return -1;
 }
}

/**
* 从offset偏移处读取一个以0结束的字符串
* @param offset 字符串起始偏移
* @return 读取的字符串,出错返回空字符串
*/
private String readString(long offset) {
 try {
  ipFile.seek(offset);
  int i;
  for(i = 0, buf[i] = ipFile.readByte(); buf[i] != 0; buf[++i] = ipFile.readByte());
  if(i != 0)
  return Utils.getString(buf, 0, i, "GBK");
 } catch (IOException e) {
  log.error(e.getMessage());
 }
 return "";
}

代码并不复杂,getIPLocation是主要方法,它检查国家记录格式,并针对字符串形式,模式1,模式2采用不同的代码,readArea则相对简单,因为只有字符串和重定向两种情况需要处理。

以下为PHP版:

<?php
/**
* IP 地理位置查询类
*
* @author 马秉尧
* @version 1.5
* @copyright 2005 CoolCode.CN
*/
class IpLocation {
 /**
 * QQWry.Dat文件指针
 *
 * @var resource
 */
 var $fp;

 /**
 * 第一条IP记录的偏移地址
 *
 * @var int
 */
 var $firstip;

 /**
 * 最后一条IP记录的偏移地址
 *
 * @var int
 */
 var $lastip;

 /**
 * IP记录的总条数(不包含版本信息记录)
 *
 * @var int
 */
 var $totalip;

 /**
 * 返回读取的长整型数
 *
 * @access private
 * @return int
 */
 function getlong() {
  //将读取的little-endian编码的4个字节转化为长整型数
  $result = unpack('Vlong', fread($this->fp, 4));
  return $result['long'];
 }

 /**
 * 返回读取的3个字节的长整型数
 *
 * @access private
 * @return int
 */
 function getlong3() {
  //将读取的little-endian编码的3个字节转化为长整型数
  $result = unpack('Vlong', fread($this->fp, 3).chr(0));
  return $result['long'];
 }

 /**
 * 返回压缩后可进行比较的IP地址
 *
 * @access private
 * @param string $ip
 * @return string
 */
 function packip($ip) {
  // 将IP地址转化为长整型数,如果在PHP5中,IP地址错误,则返回False,
  // 这时intval将Flase转化为整数-1,之后压缩成big-endian编码的字符串
  return pack('N', intval(ip2long($ip)));
 }

 /**
 * 返回读取的字符串
 *
 * @access private
 * @param string $data
 * @return string
 */
 function getstring($data = "") {
  $char = fread($this->fp, 1);
  while (ord($char) > 0) {        // 字符串按照C格式保存,以\0结束
   $data .= $char;             // 将读取的字符连接到给定字符串之后
   $char = fread($this->fp, 1);
  }
  return $data;
 }

 /**
 * 返回地区信息
 *
 * @access private
 * @return string
 */
 function getarea() {
  $byte = fread($this->fp, 1);    // 标志字节
  switch (ord($byte)) {
   case 0:                     // 没有区域信息
   $area = "";
   break;
   case 1:
   case 2:                     // 标志字节为1或2,表示区域信息被重定向
   fseek($this->fp, $this->getlong3());
   $area = $this->getstring();
   break;
   default:                    // 否则,表示区域信息没有被重定向
   $area = $this->getstring($byte);
   break;
  }
  return $area;
 }

 /**
 * 根据所给 IP 地址或域名返回所在地区信息
 *
 * @access public
 * @param string $ip
 * @return array
 */
 function getlocation($ip) {
  if (!$this->fp) return null;            // 如果数据文件没有被正确打开,则直接返回空
  $location['ip'] = gethostbyname($ip);   // 将输入的域名转化为IP地址
  $ip = $this->packip($location['ip']);   // 将输入的IP地址转化为可比较的IP地址
  // 不合法的IP地址会被转化为255.255.255.255
  // 对分搜索
  $l = 0;                         // 搜索的下边界
  $u = $this->totalip;            // 搜索的上边界
  $findip = $this->lastip;        // 如果没有找到就返回最后一条IP记录(QQWry.Dat的版本信息)
  while ($l <= $u) {              // 当上边界小于下边界时,查找失败
   $i = floor(($l + $u) / 2);  // 计算近似中间记录
   fseek($this->fp, $this->firstip + $i * 7);
   $beginip = strrev(fread($this->fp, 4));     // 获取中间记录的开始IP地址
   // strrev函数在这里的作用是将little-endian的压缩IP地址转化为big-endian的格式
   // 以便用于比较,后面相同。
   if ($ip < $beginip) {       // 用户的IP小于中间记录的开始IP地址时
    $u = $i - 1;            // 将搜索的上边界修改为中间记录减一
   }
   else {
    fseek($this->fp, $this->getlong3());
    $endip = strrev(fread($this->fp, 4));   // 获取中间记录的结束IP地址
    if ($ip > $endip) {     // 用户的IP大于中间记录的结束IP地址时
     $l = $i + 1;        // 将搜索的下边界修改为中间记录加一
    }
    else {                  // 用户的IP在中间记录的IP范围内时
     $findip = $this->firstip + $i * 7;
     break;              // 则表示找到结果,退出循环
    }
   }
  }

  //获取查找到的IP地理位置信息
  fseek($this->fp, $findip);
  $location['beginip'] = long2ip($this->getlong());   // 用户IP所在范围的开始地址
  $offset = $this->getlong3();
  fseek($this->fp, $offset);
  $location['endip'] = long2ip($this->getlong());     // 用户IP所在范围的结束地址
  $byte = fread($this->fp, 1);    // 标志字节
  switch (ord($byte)) {
   case 1:                     // 标志字节为1,表示国家和区域信息都被同时重定向
   $countryOffset = $this->getlong3();         // 重定向地址
   fseek($this->fp, $countryOffset);
   $byte = fread($this->fp, 1);    // 标志字节
   switch (ord($byte)) {
    case 2:             // 标志字节为2,表示国家信息又被重定向
    fseek($this->fp, $this->getlong3());
    $location['country'] = $this->getstring();
    fseek($this->fp, $countryOffset + 4);
    $location['area'] = $this->getarea();
    break;
    default:            // 否则,表示国家信息没有被重定向
    $location['country'] = $this->getstring($byte);
    $location['area'] = $this->getarea();
    break;
   }
   break;
   case 2:                     // 标志字节为2,表示国家信息被重定向
   fseek($this->fp, $this->getlong3());
   $location['country'] = $this->getstring();
   fseek($this->fp, $offset + 8);
   $location['area'] = $this->getarea();
   break;
   default:                    // 否则,表示国家信息没有被重定向
   $location['country'] = $this->getstring($byte);
   $location['area'] = $this->getarea();
   break;
  }
  if ($location['country'] == " CZ88.NET") {  // CZ88.NET表示没有有效信息
   $location['country'] = "未知";
  }
  if ($location['area'] == " CZ88.NET") {
   $location['area'] = "";
  }
  return $location;
 }

 /**
 * 构造函数,打开 QQWry.Dat 文件并初始化类中的信息
 *
 * @param string $filename
 * @return IpLocation
 */
 function IpLocation($filename = "QQWry.Dat") {
  $this->fp = 0;
  if (($this->fp = @fopen($filename, 'rb')) !== false) {
   $this->firstip = $this->getlong();
   $this->lastip = $this->getlong();
   $this->totalip = ($this->lastip - $this->firstip) / 7;
   //注册析构函数,使其在程序执行结束时执行
   register_shutdown_function(array(&$this, '_IpLocation'));
  }
 }

 /**
 * 析构函数,用于在页面执行结束后自动关闭打开的文件。
 *
 */
 function _IpLocation() {
  if ($this->fp) {
   fclose($this->fp);
  }
  $this->fp = 0;
 }
}
?>

以下为Python版的读取程序:

#!/usr/bin/env python
# coding: utf-8

'''用Python脚本查询纯真IP库

QQWry.Dat的格式如下:

+----------+
|  文件头  |  (8字节)
+----------+
|  记录区  | (不定长)
+----------+
|  索引区  | (大小由文件头决定)
+----------+

文件头:4字节开始索引偏移值+4字节结尾索引偏移值

记录区: 每条IP记录格式 ==> IP地址[国家信息][地区信息]

   对于国家记录,可以有三种表示方式:

       字符串形式(IP记录第5字节不等于0x01和0x02的情况),
       重定向模式1(第5字节为0x01),则接下来3字节为国家信息存储地的偏移值
       重定向模式(第5字节为0x02),

   对于地区记录,可以有两种表示方式: 字符串形式和重定向

   最后一条规则:重定向模式1的国家记录后不能跟地区记录

索引区: 每条索引记录格式 ==> 4字节起始IP地址 + 3字节指向IP记录的偏移值

   索引区的IP和它指向的记录区一条记录中的IP构成一个IP范围。查询信息是这个
   范围内IP的信息

'''

import sys
import socket
from struct import pack, unpack

class IPInfo(object):
    '''QQWry.Dat数据库查询功能集合
    '''
    def __init__(self, dbname):
        ''' 初始化类,读取数据库内容为一个字符串,
        通过开始8字节确定数据库的索引信息'''

        self.dbname = dbname
        f = file(dbname, 'r')
        self.img = f.read()
        f.close()

        # QQWry.Dat文件的开始8字节是索引信息,前4字节是开始索引的偏移值,
        # 后4字节是结束索引的偏移值。
        (self.firstIndex, self.lastIndex) = unpack('II', self.img[:8])
        # 每条索引长7字节,这里得到索引总个数
        self.indexCount = (self.lastIndex - self.firstIndex) / 7 + 1

    def getString(self, offset = 0):
        ''' 读取字符串信息,包括"国家"信息和"地区"信息

        QQWry.Dat的记录区每条信息都是一个以'\0'结尾的字符串'''

        o2 = self.img.find('\0', offset)
        #return self.img[offset:o2]
        # 有可能只有国家信息没有地区信息,
        gb2312_str = self.img[offset:o2]
        try:
            utf8_str = unicode(gb2312_str,'gb2312').encode('utf-8')
        except:
            return '未知'
        return utf8_str

    def getLong3(self, offset = 0):
        '''QQWry.Dat中的偏移记录都是3字节,本函数取得3字节的偏移量的常规表示
        QQWry.Dat使用“字符串“存储这些值'''
        s = self.img[offset: offset + 3]
        s += '\0'
        # unpack用一个'I'作为format,后面的字符串必须是4字节
        return unpack('I', s)[0]

    def getAreaAddr(self, offset = 0):
        ''' 通过给出偏移值,取得区域信息字符串,'''

        byte = ord(self.img[offset])
        if byte == 1 or byte == 2:
            # 第一个字节为1或者2时,取得2-4字节作为一个偏移量调用自己
            p = self.getLong3(offset + 1)
            return self.getAreaAddr(p)
        else:
            return self.getString(offset)

    def getAddr(self, offset, ip = 0):
        img = self.img
        o = offset
        byte = ord(img[o])

        if byte == 1:
            # 重定向模式1
            # [IP][0x01][国家和地区信息的绝对偏移地址]
            # 使用接下来的3字节作为偏移量调用字节取得信息
            return self.getAddr(self.getLong3(o + 1))

        if byte == 2:
            # 重定向模式2
            # [IP][0x02][国家信息的绝对偏移][地区信息字符串]
            # 使用国家信息偏移量调用自己取得字符串信息
            cArea = self.getAreaAddr(self.getLong3(o + 1))
            o += 4
            # 跳过前4字节取字符串作为地区信息
            aArea = self.getAreaAddr(o)
            return cArea, aArea

        if byte != 1 and byte != 2:
            # 最简单的IP记录形式,[IP][国家信息][地区信息]
            # 重定向模式1有种情况就是偏移量指向包含国家和地区信息两个字符串
            # 即偏移量指向的第一个字节不是1或2,就使用这里的分支
            # 简单地说:取连续取两个字符串!

            cArea = self.getString(o)
            #o += len(cArea) + 1
            # 我们已经修改cArea为utf-8字符编码了,len取得的长度会有变,
            # 用下面方法得到offset
            o = self.img.find('\0',o) + 1
            aArea = self.getString(o)
            return cArea, aArea

    def find(self, ip, l, r):
        ''' 使用二分法查找网络字节编码的IP地址的索引记录'''
        if r - l <= 1:
            return l

        m = (l + r) / 2
        o = self.firstIndex + m * 7
        new_ip = unpack('I', self.img[o: o+4])[0]
        if ip <= new_ip:
            return self.find(ip, l, m)
        else:
            return self.find(ip, m, r)

    def getIPAddr(self, ip):
        ''' 调用其他函数,取得信息!'''
        # 使用网络字节编码IP地址
        ip = unpack('!I', socket.inet_aton(ip))[0]
        # 使用 self.find 函数查找ip的索引偏移
        i = self.find(ip, 0, self.indexCount - 1)
        # 得到索引记录
        o = self.firstIndex + i * 7
        # 索引记录格式是: 前4字节IP信息+3字节指向IP记录信息的偏移量
        # 这里就是使用后3字节作为偏移量得到其常规表示(QQWry.Dat用字符串表示值)
        o2 = self.getLong3(o + 4)
        # IP记录偏移值+4可以丢弃前4字节的IP地址信息。
        (c, a) = self.getAddr(o2 + 4)
        return c, a

    def output(self, first, last):
        for i in range(first, last):
            o = self.firstIndex +  i * 7
            ip = socket.inet_ntoa(pack('!I', unpack('I', self.img[o:o+4])[0]))
            offset = self.getLong3(o + 4)
            (c, a) = self.getAddr(offset + 4)
            print "%s %d %s/%s" % (ip, offset, c, a)

def main():
    i = IPInfo('QQWry.Dat')
    (c, a) = i.getIPAddr(sys.argv[1])
    print '%s %s/%s' % (sys.argv[1], c, a)

if __name__ == '__main__':
    main()

# changelog
# 时间:2009年5月29日
# 1. 工具下面网友的建议,修改"o += len(cArea) + 1"
#    http://linuxtoy.org/archives/python-ip.html#comment-113960
#    因为这个时候我已经把得到的字符串变成utf-8编码了,长度会有变化!

五、总结

纯真IP数据库的结构使得查找IP简单迅速,不过你想要编辑它却是比较麻烦的,我想应该需要专门的工具来生成QQWry.dat文件,由于其文件格式的限制,你要直接添加IP记录就不容易了。不过,能查到IP已经很开心了,希望纯真记录越来越多~。

下面就为大家推荐一款IP数据库解压软甲:IPLook,其可将QQIP库(如纯真版、Phoenix版、梧州版等)转换成txt文件格式;可将txt文件格式的IP数据转换成dat文件格式(QQWry.dat),轻松打造自己的IP库。

IPLook下载地址:http://vdisk.weibo.com/s/nPtD

QQWry.dat下载地址:http://www.cz88.net/fox/

参考地址:http://lumaqq.linuxsir.org/article/qqwry_format_detail.html

免责声明:文章转载自《纯真IP数据库格式读取方法(JAVA/PHP/Python)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇互联网开发模式的经验之谈新东方人工智能中台实践和AI部门管理经验分享下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

IP地址划分

    IP地址介绍:        IP地址是指互联网协议地址(英语:Internet Protocol Address,又译为网际协议地址),是IP Address的缩写。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。        IP地址是一个32位的二进制数,通常被分割...

shell学习(18)- split切分文件命令

Linux split命令用于将一个文件分割成数个。 该指令将大文件分割成较小的文件,在默认情况下将按照每1000行切割成一个小文件。 语法: split [--help][--version][-<行数>][-b <字节>][-C <字节>][-l <行数>][要切割的文件][输出文件名] 参数: -&l...

mysql db [Warning] IP address 'xxxx' could not be resolved: Name or service not known

mysql 使用版本:mysql 8.0 异常现象:每次访问,mysql server 的日志都会打印出如下内容,[Warning] IP address 'xxxx' could not be resolved: Name or service not known 异常分析:默认情况下,mysql 总会对访问者使用的 IP 或主机名进行 DNS 反向...

day28 客户端服务端架构介绍

一.客户端与服务端架构介绍 客户端与服务端实现通信 客户端将处理好的数据发给服务端,服务端把反馈好的数据再发回给客户端(客户端与服务端实现交互)    CS架构 跟CS相对应的还有一个BS架构 (浏览器和服务端软件) 为什么程序员要了解互联网通信协议? 程序员要写一个客户端和服务端的软件,两者实现基于网络通信,物理链接介质程序员不用管,但是协议需要管,...

oracle去重操作

上次用过忘记了,所以备份一下 在几千条记录里,存在着些相同的记录,如何能用SQL语句,删除掉重复的呢 1、查找表中多余的重复记录,重复记录是根据单个字段(peopleId)来判断 select * from people where peopleId in (select peopleId from people group by peopleId hav...

dnslog搭建

为什么想重写这个呢,想说后面扫描ssrf和命令执行的时候,能快速改成自己想要的api,更容易修改一些。 工具改自:https://github.com/bugScanTeam/DNSLog 需要两个域名和一台服务器。 推荐这里买一个.pw域名,可以自定义dns服务器。https://cn.resellerclub.com/ 上面两张图是在resell...