[爬虫] URL提取

来源:互联网 发布:知峰竹纤维内衣 编辑:程序博客网 时间:2024/05/23 17:11

参考 论文: 一种可扩展的高效链接提取模型的实现与验证


作为一个爬虫,最基本的便是能够从各个页面中提取URL,这里介绍我实现的提取器。基于可扩展性,我首先实现了一个不针对任何元素的HTML元素提取,即可以提取所有的元素,并通过HOOK的模型,再根据具体需要实现不同的HOOK来获得针对性的信息。当然爬虫的HOOK便是获取URL。


HOOK的接口大致如下:

// [消息通知接口]
void begin_parser()
void begin_element()
void end_parser()
void end_element()

// [具体处理接口]
void get_element_name(...)
void get_attr_name(...)
void get_attr_value(...)


基础提取器的过程大致如下:

获得元素名  hook.get_element_name(...)
获得属性名  hook.get_attr_name(...)
获得属性值  hook.get_attr_value(...)
获得属性名  hook.get_attr_name(...)
获得属性值  hook.get_attr_value(...)
……
如此循环直到分析完页面。

 由于考虑了可扩展性,或多火或少的会对性能有些影响,比如我们要实现一个完全针对性的URL分析,则可以减少多次的元素,属性等的提取,此过程还有多次无意义的字符判断。因此我改变了 hook 的接口,把 get_element_name() 的返回值设置为 bool 值,这样提取器便可以通过hook的接口得到不同的hook程序对不同的元素信息的关心情况,做出适当的处理,在我的实现中,该函数返回true,表示关心该元素的情况,false反之,如果是不关心的话,提取器直接跳到下一个元素进行分析,这样无疑提高了分析器的性能,使得它同针对性的提取器性能差距缩小。还有上面的begin_parser() begin_element()等通知函数是为了让不同的hook能知道当前提取器的工作情况,以方便处理获得的数据信息。

 不过在页面分析前,有必要对页面数据进行一次预处理,比如爬虫时对脚本,注释的过滤,因此我在提取器中加入一个HTML过滤器,可根据不同需要来设置不同的过滤器,在本爬虫中,我派生一个过滤器 html_filter 用于把脚本和注释进行过滤。

我的爬虫中的URL提取器大致就是这样,下面是部分代码 :

加入这两个宏纯粹是为了代码紧凑些。一个用于判断是否分析到数据流的结尾,另一个加入多少有点SB,如果flag为真,返回ret_code。HOHO

#define return_if_end(rs)   do{ if(rs == rs_end_of_file) return rs_end_of_file;}while(0)
#define return_if(flag,ret_code)  do{ if(flag) return ret_code;}while(0)

const string cauc_html_parser::m_brank = "  ";

void    cauc_html_parser::set_stream(istream& is)
{
 this->istream_it = istream::_Iter(is);
}
bool    cauc_html_parser::handle_by_hook(Command cmd)
{
 if(!m_hook)
  return false;
 switch(cmd)
 {
 case cm_element_name:
  return m_hook->get_element_name(get_cur_word(1));
  break;
 case cm_attr_name:
  return m_hook->get_attr_name(get_cur_word(1));
  break;
 case cm_attr_value:
  {
   return m_hook->get_attr_value(get_cur_word(1));
  }
  break;
 case cm_text:
  return m_hook->get_text(get_cur_word(1));
  break;
 }
 return true;
}
cauc_html_parser::Result cauc_html_parser::ignored_brank()
{
 while(  m_brank.find(get_cur_letter()) != string::npos)
  return_if_end(advance());
 return rs_ok;
}
cauc_html_parser::Result  cauc_html_parser::advance()
{
 istream_it++;
 if(istream_it != istream_end)
 {
  m_cur_letter = *istream_it;
  m_curWord += *istream_it;
  return rs_ok;
 }
 return rs_end_of_file;
}
void    cauc_html_parser::set_hook(cauc_hook* hook,bool del)
{
 m_delete_hook = del;
 m_hook = hook;
}
cauc_html_parser::Result  cauc_html_parser::advance_to(const string& split)
{
 do
 {
  return_if_end(advance());
 }while(split.find(get_cur_letter()) == string::npos);
 return rs_ok;
}
bool  cauc_html_parser::is_quot()
{
 return is_oneof("'/"");
}
bool  cauc_html_parser::is_oneof(const string& flag)
{
 const string& lt = get_cur_letter();
 return flag.find(lt) != string::npos;
}
string&  cauc_html_parser::get_cur_word(size_t back_offset)
{
 if(back_offset && back_offset < m_curWord.length())
  m_curWord = m_curWord.substr(0,m_curWord.length() - back_offset);
 return m_curWord;
}
void  cauc_html_parser::begin_word()
{
 m_curWord = "";
 m_curWord += get_cur_letter();
}
string  cauc_html_parser::get_cur_letter()
{
 return m_cur_letter;
}
void cauc_html_parser::parser()
{
 /*
  * [通知hook程序开始分析网页]
  **/
 if(m_hook)
  m_hook->begin_parser();

 while(this->get_next() != rs_end_of_file)
 {
  this->get_text();
 }

 /*
  * [通知 hook 程序结束分析网页]
  **/
 if(m_hook)
  m_hook->end_parser();
}
cauc_html_parser::Result cauc_html_parser::get_next()
{
 /*
  * [通知 hook 程序 开始获取元素]
  **/

 if(m_hook)
  m_hook->begin_element();
 return_if_end(ignored_brank());
 /*
  * [获得元素名]
  **/
 if(! is_oneof("<") )
  return_if_end(advance_to("<"));  
 return_if_end(advance());
 return_if_end(ignored_brank()); 
 begin_word();
 return_if_end(advance_to(">"+m_brank));

 /*
  * [hook处理]
  **/
 return_if(!handle_by_hook(cm_element_name),rs_ok);
 return_if_end(ignored_brank());

 if(is_oneof(">"))
  return_if_end(advance());

 /*
  * [获得属性信息]
  **/
 do {
  return_if_end(get_attribute());
  return_if_end(ignored_brank());
 } while(!is_oneof(">"));

 /*
  * [通知 hook 程序 结束获取元素]
  **/
 if(m_hook)
  m_hook->end_element();
 return_if_end(advance());
 return rs_ok;
}
/*
 * [属性]
 **/
cauc_html_parser::Result cauc_html_parser::get_attribute()
{
 do {// [属性名]
 
  return_if_end(ignored_brank());
  return_if_end(get_attr_name());
 } while(!is_oneof("="));
 return_if(is_oneof(">"),rs_ok);  // [如果是'>'说明后面没有属性值]
 return_if_end(advance());   // [跳过'=']
 return_if_end(get_attr_value()); // [获得属性值]
 return_if_end(ignored_brank());  // [跳过空格]
 return rs_ok;
}
/*
 * [获取属性名]
 **/
cauc_html_parser::Result cauc_html_parser::get_attr_name()
{
 begin_word();
 return_if_end(advance_to(m_brank + ">="));
 /*
  * [hook处理]
  **/
 return_if(!handle_by_hook(cm_attr_name),rs_ok);
 return rs_ok;
}
/*
 * [获取属性值]
 **/
cauc_html_parser::Result cauc_html_parser::get_attr_value()
{
 return_if_end( ignored_brank() );
 
 if(is_oneof("'/""))
 {
  string quot = get_cur_letter();
  return_if_end( advance() );
  begin_word();
  if(!is_oneof("'/""))
  {
   return_if_end( advance_to(quot) );
  }
  /*
   * [hook处理]
   **/
  return_if(!handle_by_hook(cm_attr_value),rs_ok);
  return_if_end(advance());
 }
 else
 {
  begin_word() ;
  if(!is_oneof(">"))
   return_if_end( advance_to( m_brank + ">") );
  /*
   * [hook处理]
   **/
  return_if(!handle_by_hook(cm_attr_value),rs_ok);
 }
 return rs_ok;
}
/*
 * [获取正文]
 **/
cauc_html_parser::Result cauc_html_parser::get_text()
{
 begin_word() ;
 if(!is_oneof("<"))
  return_if_end( advance_to("<") );
 handle_by_hook(cm_text);
 return rs_ok;
}


 下面是 html_filter 的实现,实现过程没有回溯,因此数据流只要具有前向迭代器功能皆可。
不过有一点现在觉得浪费了很多的时间,就是先把数据流全部过滤后得到新的数据流再交给提取器处理,由于原先认为这样使结构比较清晰,不至于把提取器实现得太过于……(恶劣)。 但是最近由于调试时发现提取器花费时间挺长的,便重新考虑了一下,有了新的模型,当然是基于把过滤器象hook处理程序一样挂到基础的提取器中,减少遍历数据一次。 当然还没实现()。下次再更新……


//[ 5月5号 更新]

今天把提取器重新实现了一下,就是完成了之前的想发,对大于100K的页面提取速度提高将近一倍……:)。

其中过滤器接口是:

void  filter(istream::__Iter& start, const istream::_Iter& end, TypeBuffer& buffer);

实现要求: 如果从流中读取了字符,且不属于过滤范围,则把读取的字符按流中的顺序保存到 buffer 中。过滤后 start 迭代器的位置为紧接着过滤掉的最后一个位置。end 参数是用于标记过滤范围,一般而言是流的结束标记。即 istream::_Iter()默认的构造对象。

过滤器在提取器中在两个地方使用,一个是一开始分析页面时调用,一个是在 advance()中调用。

在更新的提取器中我暂时使用 std::deque<char> 作为缓冲,即保存那些被过滤器错误读取的字符,然后在advance() 函数中做相应的更改即可。(先判断缓冲中是否存在字符)

 还加入了一个辅助结构,用于处理消息通知,这里主要是 begin_parser ,begin_element...,他们一般在函数开始和函数结束的时候调用,但是由于函数中间有可能有多个 return ,因此会导致一些错误或者不匹配的通知。于是我想到了临时对象的特点,他会在生命周期结束时调用他的析构函数,这正好可以用来处理我的情况,于是该结构如下:
struct notify_helper
{
 enum notify_flag
 {
   nf_parser = 0,
   nf_element
 };
 notify_helper(hook*& hook,notify_flag flag)
 : m_hook(hook), m_flag(flag)
 {
   if(!m_hook)
      return;
   switch(m_flag)
   {
      case nf_parser:
 m_hook->begin_parser();  break;
      case nf_element:
 m_hook->begin_element(); break;
   }
 }
 ~notify_helper()
 {
    if(!m_hook)
      return;
   switch(m_flag)
   {
      case nf_parser:
 m_hook->end_parser();  break;
      case nf_element:
 m_hook->end_element(); break;
   }
 }
 notify_flag m_flag;
 hook*&      m_hook;
};

然后在函数开始的时候,比如开始分析的函数中,生成一个对象即可:
   {
      notify_helper helper(m_hook, notify_helper::nf_parser);
  ……
   }
这样不管函数中有多少个return 都无所谓,因为只要函数结束,该对象生命器结束,自动会调用析构函数。

待写...

原创粉丝点击