參考 論文: 一種可擴展的高效鏈接提取模型的實現與驗證
作爲一個爬蟲,最基本的便是能夠從各個頁面中提取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 都無所謂,因爲只要函數結束,該對象生命器結束,自動會調用析構函數。
待寫...