[爬蟲] URL提取

參考 論文: 一種可擴展的高效鏈接提取模型的實現與驗證


作爲一個爬蟲,最基本的便是能夠從各個頁面中提取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 都無所謂,因爲只要函數結束,該對象生命器結束,自動會調用析構函數。

待寫...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章