<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    Senior

    RESEARCH behind THINK beside CODE beyond

    Development Overview : 0.0.0.2 : Krabber : kVitrail : senior

      基于Internet Explorer內(nèi)核的網(wǎng)頁信息抓取程序

     

     

    程序開發(fā)背景

      本程序來源于我們項目組最近正在開發(fā)的一個開源項目——網(wǎng)頁分塊工具。其目的是作為一個底層的信息抽取模塊,為后期分析提供盡可能詳盡的分塊線索,包括盡可能完整的HTML源代碼和網(wǎng)頁元素的位置、顏色、字體、背景色等信息。程序還 要具有較好的適應性,能夠支持多種網(wǎng)頁,而事實上很多網(wǎng)頁都是不標準的。從通用性考慮,程序應該能夠支持多種應用,而不僅限于網(wǎng)頁分塊。

     

    預期目標分析

      程序應達到以下幾點設計要求:

    • 能夠指定要處理的網(wǎng)頁的URL。
    • 能夠為HTML源代碼添加附件信息,如元素位置。
    • 對于JavaScript等動態(tài)腳本具有良好的解析能力。
    • 通過命令行調(diào)用,提供良好的通用性。
    • 通過socket套接字返回HTML源代碼。
    • 支持延時讀取,保證抓取的成功率。
    • 支持超時退出,保證程序不會因為加載不成功而卡死。

     

    使用IE內(nèi)核的原因

      本程序的核心部分使用的是IE內(nèi)核。至于為什么要基于IE內(nèi)核,而不使用其他瀏覽器的內(nèi)核,有以下幾方面的原因:

      首先,firefox、google chrome等瀏覽器雖然是開放源代碼的,但是其源代碼的閱讀難度相當大,想在短時間內(nèi)弄明白是很困難的。

      其次,IE的相關開發(fā)文檔比較完整,開發(fā)環(huán)境比較容易構建,開發(fā)起來更容易上手。

      最后,從網(wǎng)頁的兼容性考慮,得益于IE的廣泛的市場占有率,其兼容性明顯要比其他瀏覽器要好很多,盡管對很多標準都支持得不是很好。

      綜上,就可以確定本程序使用IE內(nèi)核進行開發(fā),實驗證明,這個做法是正確的。

     

    Internet Explorer的程序結構

      對于本程序來說,其中最重要的的就是網(wǎng)頁內(nèi)容處理層,所用到的接口也都位于mshtml.dll文件中。

    Internet Explorer程序架構

     

    開發(fā)環(huán)境

      系統(tǒng):Windows XP

      IDE:Visual Studio 2005中文版

      IE版本:Internet Explorer 6

     

    構建基于對話框的MFC程序

      運行visual studio 2005(c++),新建一個項目,選擇MFC標簽下的 “MFC應用程序”作為模板,填入項目名稱,確定。此時會彈出一個向導,按照以下步驟操作:點擊左側的”應用程序類型”,選擇”基于對話框”,”在靜態(tài)庫中使用MFC”(方便以后發(fā)布),其他保持默認即可。然后單擊完成,程序會自動生成相應的類。

      切換到資源視圖,依次展開,在DIALOG中找到以項目名稱命名的對話框,雙擊打開。刪除“確定”和“取消”按鈕。在對話框窗口上單擊右鍵,選擇“插入activeX控件”。在新彈出的窗口中選擇”Microsoft web 瀏覽器”,確定。調(diào)整好IE控件的位置后,在其上單擊右鍵,選擇“添加變量”,輸入名稱m_webBrowser。

      切換到解決方案視圖,打開對話框的源文件,名稱通常為***Dlg.cpp(***為項目名)。將下面的代碼添加到對話框初始化函數(shù)OnInitDialog()中。

      LPCTSTR url = _T(“http://***”);

      m_webBrowser.Navigate(url,&vtEmpty,&vtEmpty,&vtEmpty,&vtEmpty);

     

    如何確定WebBrowser控件中的網(wǎng)頁加載完成

      當網(wǎng)頁下載完成后,WebBrowser控件觸發(fā)DocumentComplete 事件。通過在程序中添加響應DocumentComplete事件的程序,我們就可以在網(wǎng)頁下載完成后對其進行分析和處理。

      添加事件處理程序的操作步驟如下:切換到資源視圖,打開包含WebBrowser控件的對話框,在WebBrowser控件上單擊右鍵,選擇“添加事件處理程序”,然后在彈出的對話框中選擇DocumentComplete消息,點擊“添加編輯”以確認。

      WebBrowser控件觸發(fā)DocumentComplete事件的ReadyState屬性更改為 READYSTATE_COMPLETE時,這表示 WebBrowser 控件已完成下載網(wǎng)頁。

      雖然通過響應DocumentComplete事件可以知道網(wǎng)頁是否加載完成,但是有的網(wǎng)頁觸發(fā)了不止一次DocumentComplete事件,例如網(wǎng)易首頁會從加載開始到完全加載完畢會激發(fā)二十多次DocumentComplete事件。出現(xiàn)這種情況的主要原因是:網(wǎng)頁中包含JavaScript等動態(tài)腳本,而且有可能會改變網(wǎng)頁元素的結構,當這些腳本完成解析后會觸發(fā)DocumentComplete事件;如果網(wǎng)頁是由多個frame框架組成的,每個框架中的網(wǎng)頁加載完成也會觸發(fā)DocumentComplete事件。

      針對第二種情況,微軟給出了具體的解決方案,<http://support.microsoft.com/kb/180366/zh-cn>,但是第一種情況仍無法解決。通過查閱相關的社區(qū),我找到了能夠基本解決第一種情況的方法 —— 通過將DocumentComplete事件處理函數(shù)的參數(shù)中的URL與當前的網(wǎng)頁文檔的URL相比較,若相同,則說明整個網(wǎng)頁都已經(jīng)完成加載,此時再對網(wǎng)頁進行分析和處理,然后退出。按照理論,只需對網(wǎng)頁做一次處理就可以了。然而在測試新浪博客時,我又發(fā)現(xiàn)了問題 —— 在博客評論加載完成之前觸發(fā)很多次DocumentComplete事件,其中的一次事件對應的URL與網(wǎng)頁文檔的URL一樣。如果只對網(wǎng)頁處理一次,程序是無法處理獲取加載評論之后的網(wǎng)頁。這就是為什么程序需要加上延時讀取功能的原因,具體思路請參照下一節(jié)。

      當然,在WebBrowser控件的事件中,還有其他的事件,比如NavigateComplete2事件。我曾經(jīng)嘗試在其他事件觸發(fā)時對網(wǎng)頁進行分析,但是都會出錯,要么只能獲取到一部分元素,要么直接就彈出錯誤信息。這是因為此時網(wǎng)頁尚未完全加載,很多元素的屬性都沒確定,當然無法確定元素的具體信息,如元素位置信息。

     

    延時讀取和超時退出

      為了降低網(wǎng)絡、機器配置、系統(tǒng)軟件等外界因素對程序的影響,提高讀取的成功率。本程序加入了延時讀取和超時退出的功能。具體實現(xiàn)方法是:

      首先在程序的初始化函數(shù)中,如對話框的OnInitDialog函數(shù),添加一個固定ID的定時器,使程序定時發(fā)出一個WM_TIME消息。具體函數(shù)如下:

      SetTimer(8888,1000,NULL);//8888為該定時器的ID,1000為定時發(fā)出WM_TIME消息的時間,單位為毫秒。

      然后添加一個處理WM_TIME消息的函數(shù),其代碼如下:

       1:  void CMyBrowserDlg::OnTimer(UINT_PTR nIDEvent)
       2:  {
       3:      CTime ct;
       4:      CTimeSpan cts(0,0,0,5000);            //程序延時執(zhí)行時間
       5:      CTimeSpan timeOut(0,0,0,m_timeOut);        //程序超時退出時間
       6:   
       7:      //判斷定時器ID,若非指定的定時器ID則退出
       8:      if(nIDEvent =! 8888){
       9:          CDialog::OnTimer(nIDEvent);
      10:          return;
      11:      }
      12:   
      13:      //獲取當前時間
      14:      ct = CTime::GetCurrentTime();
      15:      
      16:      //超時退出,并輸出錯誤信息
      17:      if(ct > (m_time+timeOut)){
      18:          ::PostQuitMessage(3);    //強制退出程序,并返回一個int型的值
      19:      }
      20:   
      21:      //獲取IHTMLDocument2指針,以便進行下面的操作
      22:      CComQIPtr < IHTMLDocument2 >  spDoc2 = (IHTMLDocument2 *)m_webBrowser.get_Document();
      23:   
      24:      //判斷網(wǎng)頁加載狀態(tài),若加載完成則繼續(xù)處理;否則返回
      25:      
      26:      if(1 != m_flag){    //m_flag為documentComplete事件觸發(fā)標志,1表示已觸發(fā),0表示尚未觸發(fā)
      27:          return;
      28:      }else if(m_webBrowser.READYSTATE_COMPLETE != m_webBrowser.get_ReadyState()){
      29:          return;
      30:      }else if(ct <= (m_time+cts)){
      31:          return;
      32:      }
      33:      ……
      34:  }

     

    使用IE提供的接口

      網(wǎng)頁內(nèi)容處理模塊的接口都包含在mshtml.h的頭文件中,使用IE接口時需將此頭文件包含在源文件中。在VC++平臺中,可以通過使用接口指針來調(diào)用接口提供的函數(shù)。

      下面是該程序中用到的幾個重要的IE接口

    接口 功能說明
    IHTMLDocument2 獲取HTML文件的信息,并審查和修改HTML元素和文本。
    IHTMLDocument3 提供文件對象的額外的屬性和方法。
    IHTMLElement 此接口提供了訪問所有元素對象共同的屬性和方法的能力
    IHTMLDOMNode 提供方法來訪問所有在文檔對象模型( DOM )中的節(jié)點 ,包括節(jié)點的迭代,插入節(jié)點,刪除節(jié)點,并得到的屬性節(jié)點。
    IHTMLDOMChildrenCollection 提供方法來存取子節(jié)點的集合。

      接下來,我將針對每個接口,逐個列舉在本程序中較為重要的幾個函數(shù),展示其示例代碼,以及解析在編寫相關程序時遇到的問題。

     

    IHTMLDocument2接口

      下面的代碼演示的是如何從WebBrowser控件中獲取IHTMLDocument接口。

      IHTMLDocument2 * pDoc2 = (IHTMLDocument2 *)m_webBrowser.get_Document();

      IHTMLDocument2接口中有一個比較重要的函數(shù)

      HRESULT?get_body(IHTMLElement?**p); 獲取HTML文檔中body對象的借口指針

      通過get_body函數(shù),我們就可以獲得BODY元素的接口指針。在程序中,所有的分析和處理工作都是基于BODY元素的,而不是從HTML文檔的根節(jié)點開始處理。之所以這么做,是因為本程序的目的是獲取網(wǎng)頁內(nèi)容的布局信息,而真正能顯示在屏幕上的信息都是位于BODY標簽內(nèi)的,因此就沒有必要從根節(jié)點開始處理。

      下面是該函數(shù)的簡單實例代碼:

      IHTMLElement * pBody;// IHTMLElement接口指針,指向body對象

      HRESULT hr;//用于存放函數(shù)調(diào)用結果

      hr = pDoc2->get_body(&pBody);//獲取body對象的指針,返回操作結果

      if( SUCCEEDED( hr ) ){//若操作成功,則繼續(xù)執(zhí)行

      // Something to do

      }

     

    IHTMLDocument3接口

      前面說過了IHTMLDocument3只是IHTMLDocument2接口的擴展,而且在本程序中用到該接口的地方也就一兩處。使用IHTMLDocument3接口的原因是其提供了一個get_documentElement函數(shù),下面是其介紹和簡單的示例:

      HRESULT?get_documentElement(IHTMLElement?**p); 獲取HTML文檔中根節(jié)點的接口指針

      示例:

      IHTMLElement * pDocElem;// IHTMLElement接口指針,指向body對象

      HRESULT hr;//用于存放函數(shù)調(diào)用結果

      hr = pDoc2->get_documentElement (& pDocElem);//獲取body對象的指針,返回操作結果

      if( SUCCEEDED( hr ) ){//若操作成功,則繼續(xù)執(zhí)行

      // Something to do

      }

      獲取根節(jié)點的目的是通過它獲取整個HTML文檔的源代碼,具體如何獲得請看下面關于IHTMLElement接口的介紹。

     

    IHTMLElement接口

    函數(shù)原型 功能說明
    HRESULT?get_innerHTML(BSTR?*p); 獲取當前對象開始和結束標簽之間的HTML源代碼(動態(tài)內(nèi)容)
    HRESULT?get_innerText(BSTR?*p); 獲取當前對象開始和結束標簽之間的文本內(nèi)容(動態(tài)內(nèi)容)
    HRESULT get_outerHTML(BSTR?*p); 獲取對象的HTML的內(nèi)容(靜態(tài)內(nèi)容)
    HRESULT get_outerText(BSTR?*p); 獲取對象的文本內(nèi)容(靜態(tài)內(nèi)容)

      下面只給出get_innerHTML函數(shù)的使用方法示例,另外三個函數(shù)類似:

      IHTMLElement * pElem;// IHTMLElement接口指針,指向body對象

      BSTR html;//存放html源代碼

      _bstr_t html_t;//用于將BSTR轉換為cout可以處理的字符串

      hr = pElem->get_innerHTML(&html);

      if( SUCCEEDED( hr ) ){

      html_t = html;

      cout<<”The html within this element is:”<< html_t;

      }

     

    get_innerHTML與get_outerHTML的區(qū)別

      對于這四個函數(shù),我所要強調(diào)的就是他們之間的區(qū)別。InnerHTML和outerHTML函數(shù)最大的區(qū)別就是前者可以獲取到網(wǎng)頁中動態(tài)的HTML源代碼,如利用javascript加載的評論,而后者只能獲取未解析前的靜態(tài)內(nèi)容,其功能與在網(wǎng)頁上單擊右鍵“查看網(wǎng)頁源文件”獲取到的內(nèi)容一致。

      在程序設計的早期階段,先使用get_documentElement獲取根節(jié)點docElem,然后再用get_innerHTML獲取完整的HTML源代碼。后來在測試中發(fā)現(xiàn)了問題,對于docElem來說,無論是使用get_innerHTML還是get_outerHTML都無法獲取包含javascript解析結果的HTML源代碼。又經(jīng)過多次的測試后,發(fā)現(xiàn)只有通過get_body函數(shù)獲取到的bodyElem才能得到真正的動態(tài)內(nèi)容。如何得到完整的真正的動態(tài)HTML源代碼?針對這個問題,在本程序中采用了一種比較簡單的解決方案:先從docElem中獲取到完整的HTML源代碼,再從bodyElem中獲取到動態(tài)的內(nèi)容,然后再將原先靜態(tài)的HTML中的BODY標簽內(nèi)的內(nèi)容用這些動態(tài)的內(nèi)容替換掉,最后就可以得到了完整的包含javascript執(zhí)行結果的動態(tài)HTML源代碼。

      有人可能會問,完整的HTML和body間的內(nèi)容差別在哪?了解HTML的人都知道完整的HTML源代碼不僅包含BODY標簽,還包含了HEAD標簽,而HEAD標簽對于網(wǎng)頁的正常顯示起著很大的作用。出于通用性方面的考慮,本程序就以獲取盡量完整HTML源代碼作為設計要求。

      上面這個問題足足困擾了我一個星期,很奇怪微軟為什么不允許從根節(jié)點獲取動態(tài)內(nèi)容呢?!

     

    BSTR和_bstr_t

      細心的話可能會發(fā)現(xiàn),代碼示例中,在輸出HTML源代碼之前,先將BSTR類型的變量html賦值給了_bstr_t類型的變量html_t,然后再輸出到控制臺中。這里涉及到得是BSTR類型在VC++平臺中的處理問題。

      BSTR是COM中默認的字符串數(shù)據(jù)格式,和char* 及std::string等不同,BSTR是以 '\0 '結尾,長度為前綴的unicode 字符串。char *指針指向的是該串的第一個字符,而BSTR的指針是指向該字符串的長度。操作系統(tǒng)提供相應的API函數(shù)(如SysAllocString、SysFreeString)來管理它以及一些默認的調(diào)度代碼。

      缺點: 對于字符串來說理所應當提供的字符串操作如 查找子串,字符串比較等函數(shù)都沒有。更重要的是,似乎沒有任何函數(shù)能復制BSTR。

      BSTR有兩個包裝類,分別是CComBSTR和_bstr_t。_bstr_t是“native COM support”類,而CComBSTR是ATL中的BSTR包裝類。這兩個功能上很相似,都提供了BSTR字符串的操作函數(shù),但實現(xiàn)機制不同, _bstr_t更通用些,不過如果使用ATL的話,可能 CComBSTR更方便些。由于本程序是MFC程序,所以使用的是_bstr_t。

      總的來說,_bstr_t的作用就是將BSTR轉換成大多數(shù)函數(shù)都能處理的類型,從而對BSTR字符串的內(nèi)容進行操作。

     

    獲取網(wǎng)頁元素的位置信息

      在IHTMLElement接口中,還提供了兩個計算網(wǎng)頁元素位置的函數(shù):

      HRESULT?get_offsetLeft(long?*p); 獲取對象相對于父節(jié)點左側的位置,即x坐標

      HRESULT?get_offsetTop(long?*p); 獲取對象相對于父節(jié)點頂部的位置,即y坐標

      示例代碼:

      Node * pNode;//父節(jié)點

      …….

      long absX;

      long parentAbsX;

      parentAbsX = pNode->getAbsX();//獲取父節(jié)點的絕對坐標

      if(SUCCEEDED(spElement->get_offsetLeft(&absX))){

      absX += parentAbsX;

      }

      值得注意的是這兩個函數(shù)獲取到的都是相對于父節(jié)點的坐標,計算元素絕對坐標時還需要加上父節(jié)點的絕對坐標。因此在設計程序時使用了一個自定義的Node類,其中包含著當前節(jié)點位置信息,然后傳遞給子節(jié)點,子節(jié)點計算出相對坐標后再加上該絕對坐標就可以得到子節(jié)點的絕對坐標。

      在IE的內(nèi)存模型中,網(wǎng)頁文檔是以DOM(Document Object Model文檔對象模型)存放在內(nèi)存中的,對網(wǎng)頁的處理和分析都是基于DOM來操作的,其操作方法與普通的DOM并無太大區(qū)別。下面簡單介紹IE處理網(wǎng)頁的兩個DOM相關接口:

     

    IHTMLDOMNode接口

      HRESULT?get_childNodes(IDispatch?**p); 獲取指定節(jié)點的所有直接后裔節(jié)點的集合

      HRESULT?get_nodeType(long?*p); 返回指定節(jié)點的類型

      在網(wǎng)頁文檔的DOM結構中,標簽的屬性、文本和注釋都是以節(jié)點的形式存在的。然而這些節(jié)點卻無法使用其他接口來處理,如IHTMLElement接口,如果要對這些類型的節(jié)點強行操作,程序就會報錯退出。因此在DOM遞歸時要進行查詢IHTMLElement接口時,就要通過IHTMLDOMNode的nodeType來進行判斷。只有當nodeType為element時才有子節(jié)點,向下遞歸才不會出錯。

      nodeType所對應的節(jié)點類型:(attribute屬性) 1(element元素) 3(text文本) 8(comment注釋)。

     

    IHTMLDOMChildrenCollection接口

      HRESULT?get_length(long?*p); 獲取集合中子節(jié)點的個數(shù)

      HRESULT?item(?long?index, IDispatch?**ppItem ); 獲取指定索引位置的子節(jié)點

     

    遍歷DOM中的所有節(jié)點

      結合IHTMLDOMNode接口和IHTMLDOMChildrenCollection接口就可以遍歷DOM中的所有節(jié)點。下面是示例代碼:

      void getAllChild(IHTMLDOMNode * pNode){

      CComPtr<IDispatch> spChildrenDisp;//用于子節(jié)點的集合

      CComPtr<IDispatch> spChildDisp;//正在處理的子節(jié)點

      IHTMLDOMChildrenCollection *spChildrenNode;

      longnodeType;//節(jié)點類型

      pNode->get_nodeType(&nodeType);

      pNode->get_childNodes(&spChildrenDisp);

      if( 3 == nodeType ){//判斷節(jié)點類型是否為element

      ……  //一些額外的操作

      spChildrenNode = (IHTMLDOMChildrenCollection *)spChildrenDisp;

      spChildrenNode->get_length(&childrenNum);//獲取子節(jié)點的集合長度

      for(long i = 0 ; i<childrenNum ; i++){//循環(huán)遞歸遍歷所有孩子節(jié)點

      spChildrenNode->item(i,&spChildDisp);

      getAllChild( (IHTMLDOMNode *) spChildDisp );

      if(spChildDisp != NULL){

      spChildDisp.Detach();// spChildDisp每次使用后都需要釋放,

      //因為若spChildDisp在使用時非空會報錯

      }

      }

      }

      }

     

    JAVA與C++的進程間通信

      由于本程序是底層模塊,需要被上層的java程序調(diào)用,因此就設計到了JAVA與c++進程間通信的問題。經(jīng)調(diào)查了解到JAVA與C++通信方式有幾種:1.JNI  2.CORBA  3.Socket套接字 4.文件等。我曾嘗試過使用JNI和CORBA,但是都因為太過麻煩而放棄。而利用文件的方法雖然可以使用,但是開銷太大——要頻繁地進行I/O層的讀取操作,而且效率低、靈活性差。所以暫時決定使用命令行加socket的方式實現(xiàn)進程間通信,以下是整個程序的架構:

    wps_clip_image-0

      具體實現(xiàn)細節(jié)可以查閱MSDN和JAVA API關于socket套接字的實現(xiàn)和通過java runtime調(diào)用exe程序的相關文檔,本文就不一一贅述了。

      以上就是本文的所有內(nèi)容,本人第一次寫文章,如果有問題歡迎指正。

      聯(lián)系方式:pinlin168@tom.com

     

    相關資源及鏈接

      MSDN 技術資源庫: http://msdn.microsoft.com/en-us/library/aa155133.aspx

      VC知識庫: http://www.vckbase.com/

      《Programming Microsoft Internet Explorer 5》 -  Scott Roberts

      《深入淺出MFC 第二版》 -  候俊杰

      《VC++深入詳解》 -  孫鑫

    (by: pinlin : senior, pinlin168@tom.com

    posted on 2009-05-07 12:07 Senior 閱讀(1239) 評論(0)  編輯  收藏


    只有注冊用戶登錄后才能發(fā)表評論。


    網(wǎng)站導航:
     
    主站蜘蛛池模板: 亚洲av午夜电影在线观看| 亚洲理论片在线中文字幕| 黄色免费网址大全| 又粗又硬又大又爽免费视频播放| 亚洲午夜久久久精品电影院| 最新黄色免费网站| 亚洲一区二区三区播放在线| 毛片a级毛片免费播放100| 亚洲永久在线观看| 国产精品va无码免费麻豆| 国产成人免费视频| 亚洲网址在线观看你懂的| 亚洲一区免费在线观看| 国产色在线|亚洲| 免费看一级做a爰片久久| 一区二区三区免费精品视频| 国产亚洲精品美女久久久| 99精品视频免费在线观看| 亚洲国产片在线观看| 免费被黄网站在观看| 日韩在线观看免费| 亚洲激情视频在线观看| 免费观看的毛片大全| 精品亚洲一区二区三区在线观看| 本道天堂成在人线av无码免费| 亚洲第一中文字幕| 青青青免费国产在线视频小草| 亚洲av成人一区二区三区在线播放| 亚洲AV无码乱码在线观看性色扶| 天堂在线免费观看| 国产精品成人免费综合| 51午夜精品免费视频| 亚洲欧洲日韩国产综合在线二区| 亚欧免费视频一区二区三区| 青青青亚洲精品国产| 久久精品国产精品亚洲艾草网 | 黄色网址免费大全| 国产精品亚洲一区二区三区在线观看| 国产亚洲一区二区三区在线不卡| 亚洲免费在线视频播放| 黄人成a动漫片免费网站|