webClient用法

今天在网上看到一个问题,问HtmlUnit在多线程环境下怎么使用才能避免网页抓取失败的问题。下面结合自己的使用经验,浅谈该问题的解决办法。

导致这个问题的原因其实蛮简单,举个例子来说,A线程正在使用一个WebClient对象抓取网页,在整个抓取流程结束之前,当前线程被CPU挂起,因此线程B被激活,然后B使用正在被A使用的WebClient对象进行其他网页的抓取工作,那么这时,WebCLient对象将清除刚刚未完成的工作遗留的数据,以此类推,越多线程共享一个WebClient,问题出现的就越频繁,网页丢失地概率也就越高。但其实这个问题是不难解决的,而且其解决办法也具有很广泛的适用性:无论是任何对象,在多线程环境下遇到资源共享的问题时,通常有两个解决办法,一个是使用JDK1.2之后的ThreadLocal对象,另外一个就是使用对象池化技术

早在JDK 1.2的版本中就提供的java.lang.ThreadLocal,ThreadLocal,为解决多线程程序的并发问题提供了一种新的思路,使用这个工具类可以很简洁地编写出优美的多线程程序。其原理是为每一个线程保存一个本地变量副本,以保证不会和其他线程共享该变量——这是一种保守但有效的办法。本文意不在于介绍ThreadLocal的用法(具体用法请参考百度百科),而在于借助ThreadLocal来解决HtmlUnit的WebClient对象在多线程环境下的共享问题。
请看如何使用ThreadLocal对象解决上述问题:

package cn.ysh.studio.crawler.htmlunit;import com.gargoylesoftware.htmlunit.BrowserVersion;import com.gargoylesoftware.htmlunit.WebClient;/**
 * 
 * @author Shenghany
 * @date 2013-5-27
 */publicclassThreadLocalClientFactory{//单例工厂模式privatefinalstaticThreadLocalClientFactory instance =newThreadLocalClientFactory();//线程的本地实例存储器,用于存储WebClient实例privateThreadLocal<WebClient> clientThreadLocal;/**
	 * 构造方法,初始时线程的本地变量存储器
	 */publicThreadLocalClientFactory(){
		clientThreadLocal =newThreadLocal<WebClient>();}/**
	 * 获取工厂实例
	 * @return 工厂实例
	 */publicstaticThreadLocalClientFactory getInstance(){return instance;}/**
	 * 获取一个模拟FireFox3.6版本的WebClient实例
	 * @return 模拟FireFox3.6版本的WebClient实例
	 */publicWebClient getClient(){WebClient client =null;/**
		 * 如果当前线程已有WebClient实例,则直接返回该实例
		 * 否则重新创建一个WebClient实例并存储于当前线程的本地变量存储器
		 */if((client = clientThreadLocal.get())==null){
			client =newWebClient(BrowserVersion.FIREFOX_3_6);
			client.setCssEnabled(false);
			client.setJavaScriptEnabled(false);
			clientThreadLocal.set(client);System.out.println("为线程 [ "+Thread.currentThread().getName()+" ] 创建新的WebClient实例!");}else{System.out.println("线程 [ "+Thread.currentThread().getName()+" ] 已有WebClient实例,直接使用. . .");}return client;}}

测试代码:

 

package cn.ysh.studio.crawler.htmlunit;import com.gargoylesoftware.htmlunit.WebClient;import com.gargoylesoftware.htmlunit.html.HtmlPage;/**
 * 
 * @author Shenghany
 * @date 2013-5-27
 */publicclassThreadLocalHtmlUnitTester{/**
	 * 获取目标页面,并打印网页标题
	 * @param url 目标页面地址
	 */publicstaticvoid getPage(String url){//从工厂中获取一个WebClient实例WebClient client =ThreadLocalClientFactory.getInstance().getClient();try{//抓取网页HtmlPage page =(HtmlPage)client.getPage(url);//打印当前线程名称及网页标题System.out.println(Thread.currentThread().getName()+" [ "+ url +" ] : "+ page.getTitleText());}catch(Exception e){
			e.printStackTrace();}}/**
	 * 测试程序执行入口
	 * @param s
	 */publicstaticvoid main(String[] s){//文章编号int postId =50;//目标网页的部分内容String http ="http://www.yshjava.cn/post/4";/**
		 * 共16篇文章,每个线程抓取两篇,共计将产生8个线程
		 */for(int i = postId; i<66;){//计算两篇文章的urlfinalString url1 = http +(i++)+".html";finalString url2 = http +(i++)+".html";//创建线程并抓取网页newThread(){publicvoid run(){ThreadLocalHtmlUnitTester.getPage(url1);ThreadLocalHtmlUnitTester.getPage(url2);}}.start();//线程睡眠//			try {//				Thread.sleep(1000 * 5);//			} catch (Exception e) {//			}}}}

 

测试结果:

 

为线程[Thread-7]创建新的WebClient实例!为线程[Thread-6]创建新的WebClient实例!为线程[Thread-4]创建新的WebClient实例!为线程[Thread-0]创建新的WebClient实例!为线程[Thread-3]创建新的WebClient实例!为线程[Thread-5]创建新的WebClient实例!为线程[Thread-1]创建新的WebClient实例!为线程[Thread-2]创建新的WebClient实例!Thread-5[ http://www.yshjava.cn/post/460.html]:基于JavaScript的超轻量级日期选择控件date-input - yshjava的个人博客主页线程[Thread-5]已有WebClient实例,直接使用...Thread-4[ http://www.yshjava.cn/post/458.html]:利用中文url提升网页排名- yshjava的个人博客主页线程[Thread-4]已有WebClient实例,直接使用...Thread-0[ http://www.yshjava.cn/post/450.html]: mybatis3 动态SQL - yshjava的个人博客主页线程[Thread-0]已有WebClient实例,直接使用...Thread-3[ http://www.yshjava.cn/post/456.html]:FreeMarker中对字符串做URL转码- yshjava的个人博客主页线程[Thread-3]已有WebClient实例,直接使用...Thread-7[ http://www.yshjava.cn/post/464.html]:FreeMarker快速入门- yshjava的个人博客主页线程[Thread-7]已有WebClient实例,直接使用...Thread-6[ http://www.yshjava.cn/post/462.html]:12款美轮美奂的jQuery图片轮播插件- yshjava的个人博客主页线程[Thread-6]已有WebClient实例,直接使用...Thread-1[ http://www.yshjava.cn/post/452.html]: mybatis3 StatementBuilders- yshjava的个人博客主页线程[Thread-1]已有WebClient实例,直接使用...Thread-2[ http://www.yshjava.cn/post/454.html]: OGNL表达式原理及应用- yshjava的个人博客主页线程[Thread-2]已有WebClient实例,直接使用...Thread-3[ http://www.yshjava.cn/post/457.html]: yshjava的个人博客主页Thread-6[ http://www.yshjava.cn/post/463.html]: freemarker自定义标签范例- yshjava的个人博客主页Thread-5[ http://www.yshjava.cn/post/461.html]:程序员的职场潜意识Top10- yshjava的个人博客主页Thread-4[ http://www.yshjava.cn/post/459.html]:Linux系统各种压缩/解压缩命令- yshjava的个人博客主页Thread-0[ http://www.yshjava.cn/post/451.html]: mybatis3 logging日志- yshjava的个人博客主页Thread-2[ http://www.yshjava.cn/post/455.html]:基于JavaScriptWeb代码编辑器-ACE范例- yshjava的个人博客主页Thread-7[ http://www.yshjava.cn/post/465.html]:FreeMarker数据模型(DataModel)- yshjava的个人博客主页Thread-1[ http://www.yshjava.cn/post/453.html]: mybatis3 Java API - yshjava的个人博客主页

 

上述方案简单、小巧,容易理解,但是另外一种方案或许更为合适——对象池化技术。

下面的代码,是基于Apache的commons-pool池化包做的一个WebClient对象池,目的在于解决WebCLient对象在多线程环境下的共享问题的同时,尽最大可能节省对象的创建数量以节约资源。Apache commons-pool组件的先关信息请至官网查看,此处不再赘述。请看代码:

 

package cn.ysh.studio.crawler.htmlunit;import org.apache.commons.pool.PoolableObjectFactory;import org.apache.commons.pool.impl.GenericObjectPool;import com.gargoylesoftware.htmlunit.BrowserVersion;import com.gargoylesoftware.htmlunit.WebClient;/**
 * 
 * @author Shenghany
 * @date 2013-5-27
 */publicclassPooledClientFactory{privatefinalstaticPooledClientFactory instance =newPooledClientFactory();privatefinalGenericObjectPool clientPool =newGenericObjectPool();publicPooledClientFactory(){
		clientPool.setFactory(newPoolableObjectFactory(){@Overridepublicboolean validateObject(Object arg0){returnfalse;}@Overridepublicvoid passivateObject(Object arg0)throwsException{}@OverridepublicObject makeObject()throwsException{System.out.println("为线程 [ "+Thread.currentThread().getName()+" ] 创建新的WebClient实例!");WebClient client =newWebClient(BrowserVersion.FIREFOX_3_6);
				client.setCssEnabled(false);
				client.setJavaScriptEnabled(false);return client;}@Overridepublicvoid destroyObject(Object arg0)throwsException{WebClient client =(WebClient)arg0;
				client.closeAllWindows();
				client =null;}@Overridepublicvoid activateObject(Object arg0)throwsException{}});}publicstaticPooledClientFactory getInstance(){return instance;}publicWebClient getClient()throwsException{return(WebClient)this.clientPool.borrowObject();}publicvoid returnClient(WebClient client)throwsException{this.clientPool.returnObject(client);}}

 

测试代码:

 

package cn.ysh.studio.crawler.htmlunit;import com.gargoylesoftware.htmlunit.WebClient;import com.gargoylesoftware.htmlunit.html.HtmlPage;/**
 * 
 * @author Shenghany
 * @date 2013-5-27
 */publicclassPooledHtmlUnitTester{/**
	 * 获取目标页面,并打印网页标题
	 * @param url 目标页面地址
	 */publicstaticvoid getPage(String url){try{//从工厂中获取一个WebClient实例WebClient client =PooledClientFactory.getInstance().getClient();//抓取网页HtmlPage page =(HtmlPage)client.getPage(url);//打印当前线程名称及网页标题System.out.println(Thread.currentThread().getName()+" [ "+ url +" ] : "+ page.getTitleText());PooledClientFactory.getInstance().returnClient(client);}catch(Exception e){
			e.printStackTrace();}}/**
	 * 测试程序执行入口
	 * @param s
	 */publicstaticvoid main(String[] s){//文章编号int postId =50;//目标网页的部分内容String http ="http://www.yshjava.cn/post/4";/**
		 * 共16篇文章,每个线程抓取两篇,共计将产生8个线程
		 */for(int i = postId; i<66;){//计算两篇文章的urlfinalString url1 = http +(i++)+".html";finalString url2 = http +(i++)+".html";//创建线程并抓取网页newThread(){publicvoid run(){PooledHtmlUnitTester.getPage(url1);PooledHtmlUnitTester.getPage(url2);}}.start();//线程睡眠//try {//Thread.sleep(1000 * 5);//} catch (Exception e) {//}}}}

 

测试结果:

 

为线程[Thread-0]创建新的WebClient实例!为线程[Thread-6]创建新的WebClient实例!为线程[Thread-4]创建新的WebClient实例!为线程[Thread-2]创建新的WebClient实例!为线程[Thread-7]创建新的WebClient实例!为线程[Thread-5]创建新的WebClient实例!为线程[Thread-3]创建新的WebClient实例!为线程[Thread-1]创建新的WebClient实例!Thread-4[ http://www.yshjava.cn/post/458.html]:利用中文url提升网页排名- yshjava的个人博客主页Thread-5[ http://www.yshjava.cn/post/460.html]:基于JavaScript的超轻量级日期选择控件date-input - yshjava的个人博客主页Thread-3[ http://www.yshjava.cn/post/456.html]:FreeMarker中对字符串做URL转码- yshjava的个人博客主页Thread-7[ http://www.yshjava.cn/post/464.html]:FreeMarker快速入门- yshjava的个人博客主页Thread-0[ http://www.yshjava.cn/post/450.html]: mybatis3 动态SQL - yshjava的个人博客主页Thread-2[ http://www.yshjava.cn/post/454.html]: OGNL表达式原理及应用- yshjava的个人博客主页Thread-6[ http://www.yshjava.cn/post/462.html]:12款美轮美奂的jQuery图片轮播插件- yshjava的个人博客主页Thread-1[ http://www.yshjava.cn/post/452.html]: mybatis3 StatementBuilders- yshjava的个人博客主页Thread-3[ http://www.yshjava.cn/post/457.html]: yshjava的个人博客主页Thread-4[ http://www.yshjava.cn/post/459.html]:Linux系统各种压缩/解压缩命令- yshjava的个人博客主页Thread-5[ http://www.yshjava.cn/post/461.html]:程序员的职场潜意识Top10- yshjava的个人博客主页Thread-6[ http://www.yshjava.cn/post/463.html]: freemarker自定义标签范例- yshjava的个人博客主页Thread-0[ http://www.yshjava.cn/post/451.html]: mybatis3 logging日志- yshjava的个人博客主页Thread-2[ http://www.yshjava.cn/post/455.html]:基于JavaScriptWeb代码编辑器-ACE范例- yshjava的个人博客主页Thread-7[ http://www.yshjava.cn/post/465.html]:FreeMarker数据模型(DataModel)- yshjava的个人博客主页Thread-1[ http://www.yshjava.cn/post/453.html]: mybatis3 Java API - yshjava的个人博客主页

 

如果线程进行的工作是简单的、耗时较短的、线程数量偏多的,建议使用第二种方案,即对象池化技术,可以最大限度地节省资源消耗;如果线程进行的工作是繁重的、耗时较长的、线程数量偏少的,建议使用第一种方案,因为此时第二种方案在资源消耗方面已无任何优势,且其代码的简洁性不如第一种方案。

你可能感兴趣的:(java)