摘要:對基于IE內核(如IE,Maxthon)與基于Gecko內核(如Firefox)的瀏覽器的網頁內容獲取與分析的技術進行了研究,采用Visual C++ 6.0為平臺,基于COM技術和微軟的MSAA技術,采用了多種方式實現了基于以上兩類不同內核的瀏覽器的網頁內容獲取,并對這幾種獲取方式進行了優劣比較。
關鍵詞:COM; DOM; MSAA; IE; Gecko; windows編程
中圖分類號:TP393文獻標識碼:A文章編號:1009-3044(2008)23-936-04
The Research of the Technique of Capturing and Analyzing the Web Page Contents Based on the Kernel of IE and Gecko
ZHOU Zhou
(College of Software Engineering, Beijing Jiao Tong University, Beijing 100044, China)
Abstract: With the instant development of World Wide Web, it becomes important to capture and analyze the web page contents in order to give the customers information better arranged. This article did a research in several ways and techniques which can capture and analyze the web pages by using the Visual C++ 6.0, on the basis of COM and MSAA. At last the article compared all the techniques and listed distinctly their advantages and disadvantages.
Key words: COM; DOM; MSAA; IE; Gecko; windows programming
1 引言
隨著互聯網的迅猛發展,如今我們面對的是一個包羅萬象的網絡世界,面對浩如煙海的信息,如何有效地提取網頁信息成了一個十分重要的課題。如果掌握了相關的網頁內容獲取技術,將可以進行多方面的拓展活動,比如實時網頁內容審查過濾、用戶瀏覽行為分析、用戶瀏覽網站的實時統計、網頁自動化測試、為殘疾人士瀏覽網頁提供輔助功能。以上許多活動中都可能隱藏著很多潛在的商機。
2 關鍵概念
2.1 組件技術(COM)
簡單地說,COM是一種跨應用和語言共享二進制代碼的方法。與C++不同,它提倡源代碼重用。COM通過定義二進制標準解決了這些問題,即COM明確指出二進制模塊(DLLs和EXEs)必須被編譯成與指定的結構匹配。這個標準也確切規定了在內存中如何組織COM對象。COM定義的二進制標準還必須獨立于任何編程語言(如C++中的命名修飾)。一旦滿足了這些條件,就可以輕松地從任何編程語言中存取這些模塊。由編譯器負責所產生的二進制代碼與標準兼容。這樣使后來的人就能更容易地使用這些二進制代碼。
2.2 MSAA (Microsoft? Active Accessibility)
Microsoft? Active Accessibility 是一種相對較新的技術。目的是方便身患殘疾的人士使用電腦——可用于放大器、屏幕閱讀器,以及觸覺型鼠標。同樣還可以用來開發驅動其它軟件的應用程序,其模擬用戶輸入的能力尤其適合測試軟件的開發。
Active Accessibility 的主要思想是提供一種以程序方式訪問UI元素信息或操作這些UI元素的功能。支持這種功能的 UI(User Interface) 元素是可訪問的。在大多數情況下,這意味著一個UI元素支持 IAccessible 接口。你也可以說在 Active Accessibility 的世界里,一個可訪問的UI元素可表示為 IAccessible 接口。每當你需要得到有關一個元素的信息,在其上執行一個動作,或者使用 Active Accessibility 做其它的什么,你通常需要通過使用代表這個元素的 IAccessible 接口的一種方法或者屬性來引用這個元素。
2.3 GUID (Globally Unique Identifier)
GUID(全球唯一標示符——Globally Unique Identifier)是個128位的數字。它是一種獨立于COM編程語言的標示方法。每一個接口和coclass有一個GUID。因為每一個GUID都是全球唯一的,所以避免了名字沖突(只要你用COM API創建它們)。有時你還會碰到另一個術語UUID(意思也是全球唯一標示符——universally unique identifier)。UUIDs和GUIDs在實際使用時的用途是一樣的。
3 程序實現
3.1 IE內核瀏覽器網頁內容獲取與分析(基于COM技術)
上述基于窗口遍歷的技術有十分大的缺陷,無法對網頁內容進行訪問,而且更為嚴重的是,當前幾乎所有的瀏覽器都支持標簽式的瀏覽,如果采用上述的方法只能訪問當前激活的標簽。鑒于以上原因,我們采用COM技術來進行實現,這里的關鍵就是取得每個網頁的IHTMLDocument2接口,這個接口封裝了眾多的方法,我們完全可以用它來進行豐富的二次開發。
這里引入一個windows編程中的對象Shell Windows Object。通俗來說,shell就是用戶和操作系統交互的一個接口,也就是接收和返回信息的一個通道;比如說windows操作系統中的explorer.exe,也就是桌面任務欄就是一個shell,如果通過任務管理器結束explorer.exe進程,桌面任務欄也就隨之沒有了。每一個網頁就是一個Shell Windows Object,我們通過枚舉所有的Shell Windows Object并試圖獲得其IHTEMLDocument2接口指針,如果成功說明這個Shell Windows Object是一個HTML容器,我們就可以通過其封裝的方法進行操作。
這里使用COM技術來取得所有網頁的URL,具體程序如下:
CComPtr <IShellWindows> spShellWin;// 定義智能指針
CoInitialize(NULL);// 初始化COM庫
HRESULT hr =spShellWin.CoCreateInstance(CLSID_ShellWindows);// 根據指定CLSID創建一個未初始化的對象
if (FAILED(hr)) return;
long nCount=0;
char URL[2000];
spShellWin->get_Count(nCount); // 獲得ShellWindows對象個數
for(long i=0; i<nCount; i++){
CComPtr <IDispatch> spDisp;
hr=spShellWin->Item(CComVariant(i), spDisp );// 返回InternetExplorer 對象
if (FAILED(hr)) continue;// 如失敗則直接跳出本次循環
CComQIPtr <IWebBrowser2> spBrowser=spDisp;// 活兒得IWebBrowser2指針
if (!spBrowser) continue;
spDisp.Release();
hr = spBrowser->get_Document(spDisp);// 返回Active Document對象
if (FAILED(hr)) continue;
CComQIPtr <IHTMLDocument2> spDoc=spDisp; // 獲得IHTMLDOCUMENT2指針
if (!spDoc)continue;
BSTR pstr; // 分配BSTR空間
spDoc->get_URL(pstr);// 取得URL
char* sp=_com_util::ConvertBSTRToString(pstr); }}// 轉換BSTR
以上程序所需的幾個特殊頭文件 atlbase.hmshtml.h、comdef.h、 exdisp.h、objbase.h
當我們取得IHTMLDOCUMENT2指針后,除了獲取URL,還可利用其他方法,如get_forms,get_links等來獲得網頁中的任何元素,而獲得的這些元素都是以IHTMLElementCollection接口的形式返回,可以它提供的get__newEnum方法來枚舉其中的每一個元素并使用相應的算法來進行分析。
3.2 IE內核瀏覽器網頁內容獲取與分析(基于MSAA技術)
這里我們使用MSAA技術來實現網頁獲取。首先解釋一下有關MASS的兩個問題:
1)Active Accessibility的工作原理:Active Accessibility 的核心功能由 OLEACC.DLL 提供的。每次當你調用一個函數來返回一個 IAccessible 接口指針,其與一個UI元素相對應,OLEACC.DLL就檢查此元素是否內在支持 IAccessible。內在的支持意思是該元素的 IAccessible 是用程序實現的。當一個UI元素不能內在的支持 IAccessible 時,OLEACC.DLL 檢查該元素的Windows 類名。如果該類是一個 USER 或者 COMCTL32 支持的類,OLEACC.DLL 就創建一個代理為 UI 元素實現 IAccessible 接口。
2)如何得到 IAccessible 接口指針 :每當你需要有關一個元素的信息,在其上執行一個動作,或者使用ActiveAccessibility 做其它的什么,你只需要通過使用代表這個元素的 IAccessible 接口的一種方法或者屬性來引用這個元素。
有幾種方法取得代表一個可訪問 UI 元素的 IAccessible 接口的指針。最普通的方法是使用 Active Accessibility 提供的一種函數,例如 AccessibleObjectFromPoint,AccessibleObjectFromWindow 等等,或者使用 IAccessible 支持的方法,例如 get_accChild,get_accParent。
IAccessible 接口支持允許你得到各 UI 元素信息的屬性,而其中對于例子程序最重要的屬性是名字、角色和狀態。
下面介紹如何得到 IAccessible 接口指針。前面已經提到過 AccessibleObjectFromWindow 這個 Active Accessibility 提供的函數,從字面上大家可以看出是通過窗口來得到對應的 IAccessible 接口指針。
因為 IAccessible 接口的數量比窗口要多(因為大多數,但不是全部,COMCTL32 控件都有被 OLEACC.DLL 支持的 IAccessible 接口。),使用 Win32 函數來搜索一個窗口將會比使用 Active Accessibility 樹搜索與該窗口相應的 IAccessible 接口要占用少得多的時間。這就意味著為了提高性能,你應該使用 FindWindow 和 EnumWindows 這樣的 Win32 函數來找到與希望的UI元素最接近的窗口。當然,在權衡 Win32 函數和 Active Accessibility 函數時,上面的規則只是使用它們的一般標準而不能盲目的遵照執行,重要的是理解它們的本來意義。
下面對上面涉及到的幾個重要的函數進行解釋:
// 此函數用于從窗口句柄獲得指定的接口或對象
STDAPIAccessibleObjectFromWindow(
HWND hwnd,
DWORD dwId,
REFIID riid,
void **ppvObject
);
//此函數枚舉所有的窗口并對每一個窗口應用lpEnumFunc所指想的回調函數
//在此處我們使用它尋找所有基于IE內核的瀏覽器窗口
BOOLEnumWindows(
WNDENUMPROC lpEnumFunc,// pointer to callback function
LPARAM lParam// application-defined value
);
//此函數枚舉某個父窗口的所有子窗口并對每一個子窗口應用lpEnumFunc所指想的回調函數
//在此處我們使用它尋找所有基于IE內核的瀏覽器窗口中的每一個標簽窗口
BOOL EnumChildWindows(
HWND hWndParent,// handle to parent window
WNDENUMPROC lpEnumFunc,// pointer to callback function
LPARAM lParam// application-defined value
);
程序基于以下思路:首先通過Spy++軟件獲得所有基于IE內核瀏覽器的頂級窗口類型(比如IE7.0的窗口類型為“ IEFrame ”,Maxthon2為” Maxthon2_Frame ”)和網頁容器窗口類型(IE7.0為” Internet Explorer_Server ”,然后枚舉所有的窗口并獲得其窗口類型,如果符合上述類型之一則對其子窗口進行枚舉并取得其網頁容器窗口類型,下面是最關鍵的一步,通過上面所說的AccessibleObjectFromWindow獲得容器窗口的Iaccessible指針,接著就可以進一步獲得前面所說的IhtmlDocument2指針。
下面以支持標簽式瀏覽的IE7.0為例,具體程序如下:
// 窗體過程函數
BOOLCALLBACK EnumWindowsProc(HWND hwnd,LPARAM lParam) {
char buf[100];
GetClassName(hwnd,(LPTSTR)buf,100);// 獲得窗體類型
if ( strcmp(buf,(\"IEFrame\"))==0 ) { // 如果窗體為IEFrame則枚舉子窗口
EnumChildWindows(hwnd,EnumChildProc,NULL); }
return true;
}
// 子窗體過程函數
BOOL CALLBACK EnumChildProc(HWND hWnd,LPARAM lParam) {
char buf[100];
GetClassName(hWnd,(LPTSTR)buf,100);
BSTR urlbuf;
if (strcmp(buf,_T(\"Internet Explorer_Server\"))==0) {
IHTMLDocument2* spDoc=GetIEDocInterface(hWnd);
if (spDoc){// 成功獲得IHTMLDocument2指針
spDoc->get_URL(urlbuf);// 獲得url地址
char* sp=_com_util::ConvertBSTRToString(urlbuf);}
return true;} }
// 核心函數,用于取得IHTMLDOCUMENT2接口
IHTMLDocument2* GetIEDocInterface(HWND hWnd) {
IAccessible* spAccessible=NULL;
// 從指定句柄獲得Iaccessible指針
If (S_OK==AccessibleObjectFromWindow( hWnd,
OBJID_WINDOW,
IID_Iaccessible,
(void**)spAccessible)) {
IServiceProvider*spServiceProv=NULL;
IHTMLWindow2* spWin=NULL;
IHTMLDocument2* spDoc=NULL;
HRESULT hResult;
// 查詢接口
hResult=spAccessible->QueryInterface(IID_IServiceProvider,(void**)spServiceProv);
if (SUCCEEDED(hResult)) {hResult=spServiceProv->QueryService(IID_IHTMLWindow2,
IID_IHTMLWindow2,
(void**)spWin);
if (SUCCEEDED(hResult)) {
hResult=spWin->get_document(spDoc);
if (SUCCEEDED(hResult)) {
return spDoc;} } } }
return NULL; }
// 以下是主程序代碼
CoInitialize(NULL); // 初始化
EnumWindows(EnumWindowsProc,0); //枚舉窗口
CoUninitialize();
這里用到的關鍵頭文件oleacc.和鏈接庫oleacc.dll。
至此我們就取得了所有標簽的IHTMLDOCUMENT2接口,并且獲取了相應的URL地址,接下來可以按照需要對網頁內容進行相應的處理和分析。
3.3 Gecko內核瀏覽器網頁內容獲取與分析(基于MSAA技術)
Gecko是目前除IE外另一個比較強大的瀏覽器內核,基于它的Firefox目前也是十分的流行,其對MSAA也提供了支持,通過查閱Mozilla開發手冊,我們得知其提供了ISimpleDOMNode、 ISimpleDOMText、、 ISimpleDOMDocument三個接口,其中的ISimpleDOMDocument類似于IE的IHTMLDOCUMENT2。
為了調用以上三個接口,我們先從Mozilla Developer Center下載三個文件ISimpleDOMNode.idl、ISimpleDOMText.idl 、ISimpleDOMDocument.idl。這里解釋一下IDL的概念: IDL(Interface Definition Language)即接口定義語言,它提供一套通用的數據類型,并以這些數據類型來定義更為復雜的數據類型。可變化 IDL 基本類型 整數類型 OMG IDL 摒棄int 類型在不同平臺上取值范圍不同帶來的多義性的問題。常數定義常數可以是整數、字符、浮點數、字符串、Boolean、octet 或枚舉型,不能是 any 類型或用戶定義的類型。OMG IDL數組類型IDL array 和 sequence,可以輕易地被映射到實現語言中。序列可以包含所有類型的元素,不管是基本類型還是用戶定義的類型。
我們使用微軟的IDL編譯器即MIDL來生成windows平臺下的頭文件,命令格式如下:
MIDLISimpleDOMNode.idl
MIDLISimpleDOMText.idl
MIDLISimpleDOMDocument.idl
然后我們就得到了三個頭文件ISimpleDOMNode.h、ISimpleDOMText.h、ISimpleDOMDocument.h可供我們使用,接著我們使用Spy++結構對Firefox的窗體結構進行分析,得知Firefox的頂級窗口類型為\" MozillaUIWindowClass \",網頁容器窗口類型為” MozillaContentWindowClass ”,實現網頁獲取的關鍵就是獲取ISimpleDOMNode和ISimpleDOMDocument指針。
下面以最新發布的Firefox3.0為例實現網頁獲取,大致流程與3.2相同,只需修改連個窗體函數
// 首先定義兩個接口的IID值
const IID IID_ISimpleDOMNode = {0x1814ceeb,0x49e2,0x407f,{0xaf,0x99,0xfa,0x75,0x5a,0x7d,0x26,0x07}};
const IID IID_ISimpleDOMDocument = {0x0D68D6D0,0xD93D,0x4d08,{0xA3,0x0D,0xF0,0x0D,0xD1,0xF4,0x5B,0x24}};
// 窗體過程函數
BOOLCALLBACK EnumWindowsProc(HWND hwnd,LPARAM lParam) {
char buf[100];
GetClassName(hwnd,(LPTSTR)buf,100);// 獲得窗體類型
if ( strcmp(buf,(\" MozillaUIWindowClass \"))==0 ) { // 如果為Firefox的窗體類型則枚舉子窗口
EnumChildWindows(hwnd,EnumChildProc,NULL); }
return true;
}
// 子窗體過程函數
BOOL CALLBACK EnumChildProc(HWND hWnd,LPARAM lParam) {
char buf[100];
GetClassName(hWnd,(LPTSTR)buf,100);
BSTR urlbuf;
if (strcmp(buf,_T(\"MozillaContentWindowClass\"))==0) {
ISimpleDOMDocument* spDoc=GetGeckoDocInterface(hWnd);
if (spDoc){// 成功獲得ISimpleDOMDocument指針
spDoc->get_URL(urlbuf); //獲得url地址
char* sp=_com_util::ConvertBSTRToString(urlbuf);}
return true;} }
// 核心函數,用于取得IHTMLDOCUMENT2接口
ISimpleDOMDocument* GetGeckoDocInterface(HWND hWnd) {
IAccessible* spAccessible=NULL;
ISimpleDOMNode* spDOMNode=NULL;
ISimpleDOMDocument* spDoc=NULL;
HRESULT hResult;
if (S_OK==AccessibleObjectFromWindow(hWnd,
OBJID_CLIENT, // 注意,由于Firefox窗口結構的特殊性
// 這里只能使用OBJID_CLIENT
IID_IAccessible,
(void**)spAccessible))
{ hResult=spAccessible->QueryInterface(IID_ISimpleDOMNode,(void**)spDOMNode);
if (SUCCEEDED(hResult)) {
hResult=spDOMNode->QueryInterface(IID_ISimpleDOMDocument,(void**)spDoc);
if (SUCCEEDED(hResult)) {
return spDoc;} } }
return NULL; }
這里的頭文件除了3.3中的外還有剛才生成的ISimpleDOMDocument.h和ISimpleDOMNode.h
ISimpleDOMNode接口中提供了諸如get_parentNode()、get_firstChild()、get_nextSibling()等方法,可以用來對文檔結構進行樹狀解析。
4 關鍵技術對比
1)基于COM技術:這是與windows內核緊密結合的技術,對基于IE內核的瀏覽器可以說一種完美的解決方案,只要其內核不發生本質性的變化,將永遠奏效。不過它卻有兩個缺點:①由于COM技術本身的封閉性,導致只能對基于IE內核的瀏覽器起作用,而其他非IE瀏覽器則無法用這種方式訪問(Gecko內核可以考慮使用XPCOM技術,這是一種開源的組件技術);②由于windows系統和許多軟件都調用了IE內核,比如資源管理器、CHM幫助文檔、某些Active插件等,使用這種方式將會導致可能會捕獲到原本對用戶來說不在計劃之內的頁面,對頁面過濾能力提出了挑戰。
2)基于MSAA技術:這種技術其實也涉及了COM技術的成份,但是由于它的本意是實現平臺無關性(類似SOA的觀念),只要第三方的軟件遵循相應的標準,用戶就可以同樣的方式訪問不同的接口。但是上文獲取網頁內容的方式有一個缺陷:對窗口句柄的獲得依賴與窗口類型,如果某些瀏覽器版本升級或改版后導致窗口類型變化,則需要修改原先的代碼,對新的瀏覽器進行支持,隨著各種瀏覽器的不斷升級改進,可能會造成代碼的不斷擴充,導致程序臃腫不堪。
5 總結
討論了windows平臺下針對IE和Gecko內核的瀏覽器網頁內容獲取并對各種技術進行了比較和點評,僅起到一個拋磚引玉的作用,因為針對一項技術而言,其本身是固化的,而其帶來的結果則是千變萬化的。基于上文討論的網頁獲取技術,我們可以想象有可能會誕生基于此的新興技術或商業行為,也有可能帶來新的隱患(如網頁密碼竊?。┑取5夹g本身是中性的,使用的方向不同則會產生截然不同的結果。
參考文獻:
[1] (美)Rogerson D.Inside COM[M].Microsoft Press,1997.
[2] (美)Mayfield C.COM/DCOM Primer Plus[M].Sams Publishing,1999.
[3]潘愛民.COM原理與應用[M].北京:清華大學出版社,2001.
[4] (美)理查特.Windows核心編程[M].北京:機械工業出版社,2006.
[5]孫鑫,余安萍.VC++深入詳解[M].北京:電子工業出版社,2006.
[6] (美)Kruglinski D J. Visual C++技術內幕[M].4版.潘愛民,王國印,譯.清華大學出版社,1999.
[7]Microsoft.MSDN Library Visual Studio 6.0[EB/DK].Microsoft Developer Network,2000.
[8]Mozilla.Accessibility:AT-APIs:ImplementationFeatures:MSAA[EB/OL]. http://developer.mozilla.org/en/docs/Accessibility:AT-APIs:ImplementationFeatures:MSAA#Additional_DOM_Support.
[9]Klementiev D.Software Driving Software: Active Accessibility-Compliant Apps Give Programmers New Tools to Manipulate Software [EB/OL].http://msdn.microsoft.com/en-us/library/cc301312.aspx.