WPF中使用WebBrowser Com组件,实现动态HTML抽取。

本文介绍了在WPF应用中使用WebBrowser组件进行动态HTML抽取和验证,面对AngularJs等动态加载场景,通过Click方法模拟用户交互。探讨了WebBrowser的内存泄露问题,提供多种解决方案,并建议使用多进程通讯来优化内存管理。


业务需求:

一个测试工具系统,在原有对静态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内存的系统中,效果会更好。但是更好的方法还是要用上面多进程的结构。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值