zoukankan      html  css  js  c++  java
  • larbin之哈希之谈

    由于工作原因,打算对larbin的源码进行分析一番

    用的是2.6.3版本的larbin源码,由于这是业余,会断断续续的分析上传,已做记录笔记

    今天我们分析一下larbin的哈希表

    这个哈希表结构比较简单,因为它的主要用处是排重,因此只给出了用于排重的简单函数,

    我们来看一下头文件怎么定义的:

    // Larbin
    // Sebastien Ailleret
    // 23-11-99 -> 14-01-00
    
    /* class hashTable
     * This class is in charge of making sure we don't crawl twice the same url
     */
    
    #ifndef HASHTABLE_H
    #define HASHTABLE_H
    
    #include "types.h"
    #include "utils/url.h"
    
    class hashTable {
     private:
      ssize_t size;
      char *table;
    
     public:
      /* constructor */
      hashTable (bool create);
    
      /* destructor */
      ~hashTable ();
    
      /* save the hashTable in a file */
      void save();
    
      /* test if this url is allready in the hashtable
       * return true if it has been added
       * return false if it has allready been seen
       */
      bool test (url *U);
    
      /* set a url as present in the hashtable
       */
      void set (url *U);
    
      /* add a new url in the hashtable
       * return true if it has been added
       * return false if it has allready been seen
       */
      bool testSet (url *U);
    };
    
    #endif // HASHTABLE_H

    由头文件我们可以看出,这个哈希表仅仅有四个成员函数(除了构造和析构)

    save 函数是用于保存哈希表内部的数据,用于防止程序异常退出而造成数据丢失,因此把哈希内数据保存到一个文件中

    test  函数用于测试参数指定的URL是否在哈希表内存在,只要是排重

    set   函数就是判断出需要设置哈希表内值得时候设置该位置的URL对应的值,表示该URL从此开始存在于哈希表中

    testset 是一个辅助函数,先判断,然后设置该位置的值,并且返回设置前的判断结果

    下面我们就仔细来看一看各个函数的实现,比较简单,我就在程序中做了简单注释,就不再多余的文字解释了

    构造函数:

    hashTable::hashTable (bool create) {  //构造函数,申请哈希需求的空间并初始化
      ssize_t total = hashSize/8;   //因为是位集合判断,所以每个字节8位,对于哈希的总成都除以8
      table = new char[total];      //申请哈希空间,其实这个地方主要是以数组巧妙勾勒哈希功能
      if (create) {                 //是一个标志,也就是说哈希内部的数据是从文件内读取还是初始化位0
        for (ssize_t i=0; i<hashSize/8; i++) {
          table[i] = 0;
        }
      } else {
        int fds = open("hashtable.bak", O_RDONLY);   //从bak备份文件读取数据
        if (fds < 0) {
          cerr << "Cannot find hashtable.bak, restart from scratch
    ";
          for (ssize_t i=0; i<hashSize/8; i++) {     //如果打开备份文件失败,就重新赋值位0,当做第一次看待
            table[i] = 0;
          }
        } else {
          ssize_t sr = 0;
          while (sr < total) {
            ssize_t tmp = read(fds, table+sr, total-sr); //然后循环读取文件,直到成功读取所有数据
            if (tmp <= 0) {
              cerr << "Cannot read hashtable.bak : "
                   << strerror(errno) << endl;
              exit(1);
            } else {
              sr += tmp;        //增加每次读取的数据
            }
          }
          close(fds);          //关闭文件描述符
        }
      }
    }

    析构函数:

    hashTable::~hashTable () {    //析构函数,释放哈希申请的空间
      delete [] table;
    }

    测试函数test:

    bool hashTable::test (url *U) {    //判断该url对应的是否存在哈希中,如果存在返回true,否则false
      unsigned int code = U->hashCode();  //根据hashCode函数求散列值
      unsigned int pos = code / 8;        
      unsigned int bits = 1 << (code % 8);
      return table[pos] & bits;         
    }

    设置函数:

    void hashTable::set (url *U) {   //设置url对应哈希值
      unsigned int code = U->hashCode();
      unsigned int pos = code / 8;
      unsigned int bits = 1 << (code % 8);
      table[pos] |= bits;
    }

    测试设置函数:

    bool hashTable::testSet (url *U) { //返回测试结果,并且设置url对应的值
      unsigned int code = U->hashCode();
      unsigned int pos = code / 8;
      unsigned int bits = 1 << (code % 8);
      int res = table[pos] & bits;
      table[pos] |= bits;
      return !res;
    }

    保存文件函数:

    void hashTable::save() {     //把哈希内部数据存到文件,该过程是循序渐进的,防止时间间隔过程造成数据大量失真
      rename("hashtable.bak", "hashtable.old");   //先把先前备份文件保存,直到最后本次成功备份后删除
      int fds = creat("hashtable.bak", 00600);
      if (fds >= 0) {
        ecrireBuff(fds, table, hashSize/8);       //辅助函数,把哈希数据存到备份文件
        close(fds);
      }
      unlink("hashtable.old");
    }

    该哈希的处理部分就这么多,下面重点来看看我们两个知识点

    1,散列函数 hashCode:

    uint url::hashCode () {
      unsigned int h=port;
      unsigned int i=0;
      while (host[i] != 0) {
        h = 31*h + host[i];
        i++;
      }
      i=0;
      while (file[i] != 0) {
        h = 31*h + file[i];
        i++;
      }
      return h % hashSize;
    }

    说起来散列函数,要求很有艺术的,而且散列函数也不可能有百分百的通用性。

    一般都要自己根据哈希设置自己的散列函数。最起码要设置某些数值,用同一个散列方法和框架

    该散列函数比较简单,就是把host和file(URL类中的两个字段,表示主机和文件路径)依次乘以31

    然后对哈希最大值求余数,最大值这样定义的:

    #define hashSize 64000000

     另外对于host和file的先后哈希顺序也是设计的,先host而后file是为了让同一host对应的file的差异更大,减缓相似冲突

    2,下面我们就来谈谈URL这个类,上面那个哈希散列函数就是这个类中的一个成员函数,之所以单独摘出去说,是因为散列函数也是重大的一块

    我们先来看一下URL类的定义:

    class url {
     private:
      char *host;
      char *file;
      uint16_t port; // the order of variables is important for physical size
      int8_t depth;
      /* parse the url */
      void parse (char *s);
      /** parse a file with base */
      void parseWithBase (char *u, url *base);
      /* normalize file name */
      bool normalize (char *file);
      /* Does this url starts with a protocol name */
      bool isProtocol (char *s);
      /* constructor used by giveBase */
      url (char *host, uint port, char *file);
    
     public:
      /* Constructor : Parses an url (u is deleted) */
      url (char *u, int8_t depth, url *base);
    
      /* constructor used by input */
      url (char *line, int8_t depth);
    
      /* Constructor : read the url from a file (cf serialize) */
      url (char *line);
    
      /* Destructor */
      ~url ();
    
      /* inet addr (once calculated) */
      struct in_addr addr;
    
      /* Is it a valid url ? */
      bool isValid ();
    
      /* print an URL */
      void print ();
    
      /* return the host */
      inline char *getHost () { return host; }
    
      /* return the port */
      inline uint getPort () { return port; }
    
      /* return the file */
      inline char *getFile () { return file; }
    
      /** Depth in the Site */
      inline int8_t getDepth () { return depth; }
    
      /* Set depth to max if we are at an entry point in the site
       * try to find the ip addr
       * answer false if forbidden by robots.txt, true otherwise */
      bool initOK (url *from);
    
      /** return the base of the url
       * give means that you have to delete the string yourself
       */
      url *giveBase ();
    
      /** return a char * representation of the url
       * give means that you have to delete the string yourself
       */
      char *giveUrl ();
    
      /** write the url in a buffer
       * buf must be at least of size maxUrlSize
       * returns the size of what has been written (not including '')
       */
      int writeUrl (char *buf);
    
      /* serialize the url for the Persistent Fifo */
      char *serialize ();
    
      /* very thread unsafe serialisation in a static buffer */
      char *getUrl();
    
      /* return a hashcode for the host of this url */
      uint hostHashCode ();
    
      /* return a hashcode for this url */
      uint hashCode ();
    
    #ifdef URL_TAGS
      /* tag associated to this url */
      uint tag;
    #endif // URL_TAGS
    
    #ifdef COOKIES
      /* cookies associated with this page */
      char *cookie;
      void addCookie(char *header);
    #else // COOKIES
      inline void addCookie(char *header) {}
    #endif // COOKIES
    };

    URL这个类的声明比较复杂,因为它把设计URL的部分集中在这一块

    我们分开来分析它的实现:

    先看一个构造函数

    url::url (char *u, int8_t depth, url *base) {
      newUrl();
      this->depth = depth;
      host = NULL;
      port = 80;
      file = NULL;
      initCookie();
    #ifdef URL_TAGS
      tag = 0;
    #endif // URL_TAGS
      if (startWith("http://", u)) {
        // absolute url
        parse (u + 7);
        // normalize file name
        if (file != NULL && !normalize(file)) {
          delete [] file;
          file = NULL;
          delete [] host;
          host = NULL;
        }
      } else if (base != NULL) {
        if (startWith("http:", u)) {
          parseWithBase(u+5, base);
        } else if (isProtocol(u)) {
          // Unknown protocol (mailto, ftp, news, file, gopher...)
        } else {
          parseWithBase(u, base);
        }
      }
    }


    这是其中一个url类的构造函数,它接受3个参数

    第一个参数 u 是URL字符串

    第二个参数 depth 是爬虫深度

    第三个参数 base 是基类URL,也就是要解析需要用的上一个URL,相当于referer

    它虽然只是一个构造函数,但是设计中为了更方便的使用该类,在其构造函数中进行了一系列的处理,相当于我们只要构造这个对象,就获取了相应的转化处理

    newUrl()  是一个宏定义,调试用的:

    #define newUrl() debUrl++
    #define delUrl() debUrl--

    然后就是一些初始化变量

    下面调用了一个 initCookie函数,该函数是宏条件控制的宏:

    #ifdef COOKIES
    #define initCookie() cookie=NULL
    #else // COOKIES
    #define initCookie() ((void) 0)
    #endif // COOKIES

    其实就是对cookie变量赋值为NULL,或者一个空语句

    接着,如果使用tag,用宏开关设置tag为0

    然后根据url字符串判断是否是http://开始的标准绝对路径URL

    如果是绝对路径URL 就从后面开始解析这个url字符串。

    parse是URL解析函数,随后分析

    调用parse,可以分析出 host和file,然后下一步,如果file不为空,就对file就行序列化调整

    也就是把file中不合适的路径修改合适标准,此处调用的事normalize

    相反,如果不是url绝对路径,如果base为NULL,则无法判断决定这个url因此就不处理,返回

    否则表示可以组建url

    如果是以http:开始的,就调用parseWithBase函数解析url

    否则,如果使用了不识别的协议,就无法解析

    其他情况调用 parseWithBase函数解析

    这样这个URL构造函数就结束了,我们对处理的流程清楚,但是其中有几个重点函数要具体分析一下

    1,file序列化函数 normalize函数

    bool fileNormalize (char *file) {
      int i=0;
      while (file[i] != 0 && file[i] != '#') {   
        if (file[i] == '/') {               //如果当前字符是/, 我们分情况处理下一个字符是什么
          if (file[i+1] == '.' && file[i+2] == '/') {   //如果接下来的字符是./也就是出现/./的情况,我们把这个路径
            // suppress /./                             //规范化,去掉这三个字符,过程就是从后向前覆盖
            int j=i+3;
            while (file[j] != 0) {
              file[j-2] = file[j];
              j++;
            }
            file[j-2] = 0;
          } else if (file[i+1] == '/') {              //如果下一个是/,也就是出现 // 的情况我们把它替换为 一个 /
            // replace // by /
            int j=i+2;
            while (file[j] != 0) {
              file[j-1] = file[j];
              j++;
            }
            file[j-1] = 0;
          } else if (file[i+1] == '.' && file[i+2] == '.' && file[i+3] == '/') { //如果出现 /../的情况,相当于
            // suppress /../                             //回到父目录,因此我们回朔到父目录,覆盖回朔过程中无用的路径
            if (i == 0) {
              // the file name starts with /../ : error  //如果是刚开始出现这种情况,视为错误
              return false;
            } else {
              int j = i+4, dec;
              i--;
              while (file[i] != '/') { i--; }
              dec = i+1-j; // dec < 0
              while (file[j] != 0) {
                file[j+dec] = file[j];
                j++;
              }
              file[j+dec] = 0;
            }
          } else if (file[i+1] == '.' && file[i+2] == 0) {  //如果为 /.0 这种情况,则表示当前路径,直接赋值结束
            // suppress /.
            file[i+1] = 0;
            return true;
          } else if (file[i+1] == '.' && file[i+2] == '.' && file[i+3] == 0) {
            // suppress /..                        //如果是 /..0 这样情况
            if (i == 0) {
              // the file name starts with /.. : error    //如果在开始,则按错误处理
              return false;
            } else {          //如果出现在路径中间,则表示上级目录,回朔覆盖
              i--;
              while (file[i] != '/') {
                i--;
              }
              file[i+1] = 0;
              return true;
            }
          } else { // nothing special, go forward   //如果是其他正常情况,继续处理
            i++;
          }
        } else if (file[i] == '%') {   //因为url中一个字符不能出现就需要转义,转义是以%开始,加两个十六进制数字
          int v1 = int_of_hexa(file[i+1]);
          int v2 = int_of_hexa(file[i+2]);   
          if (v1 < 0 || v2 < 0) return false;
          char c = 16 * v1 + v2;   //解析转义字符
          if (isgraph(c)) {   //如果是可视字符,把它转为可视字符,然后多余字符覆盖移动
            file[i] = c;
            int j = i+3;
            while (file[j] != 0) {
              file[j-2] = file[j];
              j++;
            }
            file[j-2] = 0;
            i++;
          } else if (c == ' ' || c == '/') { // keep it with the % notation //如果是空格或者/ 则不处理,跳过
            i += 3;
          } else { // bad url    //其他情况则视为错误
            return false;
          }
        } else { // nothing special, go forward  //正常的流程,继续执行
          i++;
        }
      }
      file[i] = 0;
      return true;
    }

    这是一个辅助函数,主要用于纠正分析文件路径的错误不识别情况,上面代码已经注释的很清楚
    其中用到的一个辅助函数为:

    static int int_of_hexa (char c) {  //十六进制数字转化为10进制
      if (c >= '0' && c <= '9')
        return (c - '0');
      else if (c >= 'a' && c <= 'f')
        return (c - 'a' + 10);
      else if (c >= 'A' && c <= 'F')
        return (c - 'A' + 10);
      else
        return -1;
    }

    这个就是16进制字符串转化为10进制,很简单易懂

    2,小的辅助函数,不多做解释:

    bool startWith (char *a, char *b) {
      int i=0;
      while (a[i] != 0) {
        if (a[i] != b[i]) return false;
        i++;
      }
      return true;
    }

    3,url构造函数还有两个重量级的函数 parse 和 parseWithBase两个

    我们先来分析一下parse函数:

    void url::parse (char *arg) {
      int deb = 0, fin = deb;
      // Find the end of host name (put it into lowerCase)
      while (arg[fin] != '/' && arg[fin] != ':' && arg[fin] != 0) {
        fin++;
      }
      if (fin == 0) return;
    
      // get host name
      host = new char[fin+1];
      for (int  i=0; i<fin; i++) {
        host[i] = lowerCase(arg[i]);
      }
      host[fin] = 0;
    
      // get port number
      if (arg[fin] == ':') {
        port = 0;
        fin++;
        while (arg[fin] >= '0' && arg[fin] <= '9') {
          port = port*10 + arg[fin]-'0';
          fin++;
        }
      }
    
      // get file name
      if (arg[fin] != '/') {
        // www.inria.fr => add the final /
        file = newString("/");
      } else {
        file = newString(arg + fin);
      }
    }

    这个代码写的太有爱了,一目了然,我们很容易明白,这就是一个字符串解析

    然后分析出 host,port,和file。

    4,然后下一个解析函数 parseWithBase函数:

    void url::parseWithBase (char *u, url *base) {
      // cat filebase and file
      if (u[0] == '/') {
        file = newString(u);
      } else {
        uint lenb = strlen(base->file);
        char *tmp = new char[lenb + strlen(u) + 1];
        memcpy(tmp, base->file, lenb);
        strcpy(tmp + lenb, u);
        file = tmp;
      }
      if (!normalize(file)) {
        delete [] file;
        file = NULL;
        return;
      }
      host = newString(base->host);
      port = base->port;
    }


    这个函数主要是使用了已知URL类来构造新的URL,主机和端口都不会变

    相当于相对路径的基准路径,然后根据URL路径+现在路径 然后序列化位标准路经

    就是相对路径转化为绝对路径,这个相对比较简单

    这样这个url的构造就结束了,

  • 相关阅读:
    测试思想-测试设计 史上最详细测试用例设计实践总结 Part2
    测试思想-测试设计 史上最详细测试用例设计实践总结 Part1
    loadrunner 脚本开发-参数化之将内容保存为参数、参数数组及参数值获取Part 2
    loadrunner 脚本开发-参数化之将内容保存为参数、参数数组及参数值获取
    测试思想-测试计划 软件测试方法与测试策略简述
    测试思想-文档评审 关于需求评审
    loadrunner 运行脚本-命令行运行脚本
    loadrunner 运行脚本-Run-time Settings-Browser Enmulation设置详解
    loadrunner 运行脚本-Run-time Settings-ContentCheck简单设置
    python 全栈开发,Day53(jQuery的介绍,jQuery的选择器,jQuery动画效果)
  • 原文地址:https://www.cnblogs.com/lfsblack/p/3891573.html
Copyright © 2011-2022 走看看