天天看點

WPF中使用WebBrowser Com元件,實作動态HTML抽取。

業務需求:

一個測試工具系統,在原有對靜态HTML測試的基礎上增加對動态HTML進行驗證,驗證的是HTML标簽的完整性。

測試對象:

一套内部辦公系統,大量使用了AngularJs,除去登入位址外,頁面中幾乎所有的功能按鈕,菜單,連結均是由AngularJs完成。

使用技術:

WPF+WebBrowser元件+多線程

經驗總結:

  • 開發中遇到問題絕大部分都能在google中搜尋到解決方案,通路google方法一是翻牆,二是通過http://www.baigoogledu.com/
  • http://stackoverflow.com/是個好地方,Google上搜尋到的資料都是出自此處。
  • 在開發中主要是圍繞WebBrowser元件進行,多線程最後使用了System.Window.Forms.Timer實作,而其他方式遇到了跨線程通路WebBrowser元件的問題,尤其是工具自動點選A Tag(調用AngularJs代碼)時,會停止響應(卡死,或者說一直block)
  • 大部分跨線程通路UI元件的問題,都可以使用Control.Invoke(new Action(()=>{ 業務代碼}));這樣方式解決。
  • WebBrowser元件隻有在調用Navigate(url)之後,才會觸發DocumentCompleted事件,多線程的業務調用中,也隻有在DocumentCompleted事件中進行業務處理,才能得到WebBrowser元件屬性值和Document文檔結構,進而得到HTML代碼。
  • 靜态頁面可以從MainURL開始不斷的抽取裡面的各種連結(URL,Js事件)然後或者Navigate或者InvokeScript調用JS,擷取HTML代碼進行完整性驗證,最後進行子節點的抽取。
  • 動态頁面也要沖MainURL開始,不過抽取到的可能是各種含有js的A Tag,而這些js(例如AngularJs)及其有可能在InvokeScript調用後無反應,這是需要在Document中周遊所需要執行的A Tag,轉換為HtmlElement對象,在調用Click方法,這樣就實作了程式自動點選連結,動态生成HTML,抽取,驗證。
  • WebBrowser Session是共享的,在同一個程序中無論是Control.WebBrowser 還是Forms.WebBrowser,無論是一個還是多個,他們之間預設是共享Session的,這就為登陸一次,多次抽取建立了便利
  • AngularJs,jQuery中類似于jQuery(document).ready(function($) {

      $("#content").load("AElementList.html");

     }); 這樣的函數是與WebBrowser.DocumentCompleted同時發生的,是以這就需在WebBrowser.DocumentCompleted事件中加入 延時等待功能,對ReadyState和IsBusy屬性的檢測,判斷HTML頁面動态加載完成。

補充

  • WebBrowser Control存在記憶體洩露的Bug查了許多資料,這篇資料較全

 http://stackoverflow.com/questions/8302933/how-to-get-around-the-memory-leak-in-the-net-webbrowser-control/

我準備了,三個解決方案,目前使用了第一個,後面兩個沒有嘗試。

Solution1:

class MemoryHelper
    {
        [DllImport("KERNEL32.DLL", EntryPoint = "SetProcessWorkingSetSize", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
        internal static extern bool SetProcessWorkingSetSize( IntPtr pProcess, int dwMinimumWorkingSetSize, int dwMaximumWorkingSetSize );

        [DllImport("KERNEL32.DLL", EntryPoint = "GetCurrentProcess", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
        internal static extern IntPtr GetCurrentProcess( );

        public static void ReleaseMemory( )
        {
            SetProcessWorkingSetSize(GetCurrentProcess(), -1, -1);
        }
    }

//在每個線程中
MemoryHelper.ReleaseMemory();
 GC.Collect();
 GC.WaitForPendingFinalizers();
GC.Collect();
           

Solution2:

try
        {
            if (webBrowser.Url.Equals("about:blank")) //first visit
            {
                webBrowser.Navigate(new Uri("http://url"));
            }
            else
            {
                webBrowser.Refresh(WebBrowserRefreshOption.Completely);
            }
        }
        catch (System.UriFormatException)
        {
            return;
        }
        System.GC.Collect(); // may be omitted, Windows can do this automatically
           

Solution3:

I have been looking for a solution for this for ages, and finally found one.

I am using Delphi so the syntax for others may be slightly different

The code I used is:

(web1.Document as IPersistStreamInit).InitNew;

where web1 is a tWebbroser component.

Hope this helps!
           

補充2

上述Solutions經過測試,都不能解決問題,Solution1可以起到緩解記憶體洩露的問題,但不能從根本上解決問題。

不斷的Google這個問題,結論是WebBrowser Leak Memory是控件本身的一個bug,而且是一個存在了很多年的一個Bug

  • 這個文章中有詳細的讨論,有趣的是一樓開始于2008年,看這裡
  • 這個文章中羅列的十種Do NOT WORK的解決方案,看這裡
  • 這是微軟給出的Bug描述,看這裡
現在就剩下曲線救國的解決方案了:
  • 使用第三方Web Browser控件替換.Net WebBrowser Control,看這裡
  • 放棄目前的多線程結構,使用程序間通訊IPC,在程序A中啟動程序B,WebBrowser工作程序B主要處理抽取頁面代碼,程序A主要負責展示,排程程序程序B,尤其是在程序B記憶體超過1GB後,重新開機程序B

補充3

再将程式在AnyCPU模式下進行bulid後,放到Win64系統進行運作(記憶體4G),雖然記憶體洩露依然但已經不行崩潰,線程的記憶體使用量達到2GB。估計換成8GB記憶體的系統中,效果會更好。但是更好的方法還是要用上面多程序的結構。