为什么htmlunit与HttpClient两者都可以爬虫、网页采集、通过网页自动写入数据,我们会推荐使用htmlunit呢?
一、网页的模拟化
首先说说HtmlUnit相对于HttpClient的最明显的一个好处,HtmlUnit更好的将一个网页封装成了一个对象,如果你非要说HttpClient返回的接口HttpResponse实际上也是存储了一个对象那也可以,但是HtmlUnit不仅保存了这个网页对象,更难能可贵的是它还存有这个网页的所有基本操作甚至事件。这就是说,我们对于操作这个网页可以像在jsp中写js一样,这是非常方便的,比如:你想某个节点的上一个节点,查找所有的按钮,查找样式为“bt-style”的所有元素,对于某些元素先进行一些改造,然后再转成String,或者我直接得到这个网页之后操作这个网页,完成一次提交都是非常方便的。这意味着你如果想分析一个网页会来的非常的容易
二、网络响应的自动化处理
HtmlUnit拥有强大的响应处理机制,我们知道:常见的404是找不到资源,100等是继续,300等是跳转...我们在使用HttpClient的时候它会把响应结果告诉我们,当然,你可以自己来判断,比如说,你发现响应码是302的时候,你就在响应头去找到新的地址并自动再跳过去,发现是100的时候就再发一次请求,你如果使用HttpClient,你可以这么去做,也可以写的比较完善,但是,HtmlUnit已经较为完整的实现了这一功能,甚至说,他还包括了页面JS的自动跳转(响应码是200,但是响应的页面就是一个JS)
三、并行控制 和串行控制
既然HtmlUnit封装了那么多的底层api和hHttpClient操作,那么它有没有给我们提供自定义各种响应策略和监听整个执行过程的方法呢?,答案是肯定的。由于HtmlUnit提供的监听和控制方法比较多,我说几个大家可能接触比较少,但很有用的方法。其他的类似于:设置CSS有效,设置不抛出JS异常,设置使用SSL安全链接,诸如此类,大家通过webClient.getOptions().set***,就可以设置了,这种大家都比较熟了。
四、强大的缓存机制
为什么第一次获取一个网页可能会比较慢,但是第二次来拿就特别快呢?在HtmlUnit源码webClient类中的loadWebResponseFromWebConnection方法中我们可以看到。
以下简单介绍下如何去分析网页及涉及的代码:
其中url可以直接浏览器访问地址直接解析页面,也可以通过分析页面请求接口(开启google浏览器F12开发者模式,刷新对应页面即可查看请求数据地址 -- >> 具体数据需要通过分享查看)
引入maven包:
<htmlunit.version>2.70.0</htmlunit.version>
<junit.version>4.13.2</junit.version>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>${htmlunit.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
@Slf4j
public class ThreadLocalClientFactory {
//单例工厂模式
private final static ThreadLocalClientFactory instance = new ThreadLocalClientFactory();
//线程的本地实例存储器,用于存储WebClient实例
private ThreadLocal<WebClient> clientThreadLocal;
public ThreadLocalClientFactory() {
clientThreadLocal = new ThreadLocal<>();
}
/**
* 获取工厂实例
*
* @return 工厂实例
*/
public static ThreadLocalClientFactory getInstance() {
return instance;
}
public WebClient crawlPageApi() {
WebClient webClient;
/**
* 如果当前线程已有WebClient实例,则直接返回该实例
* 否则重新创建一个WebClient实例并存储于当前线程的本地变量存储器
*/
if ((webClient = clientThreadLocal.get()) == null) {
webClient = new WebClient(BrowserVersion.CHROME);
clientThreadLocal.set(webClient);
log.info("为线程 [ " + Thread.currentThread().getName() + " ] 创建新的WebClient实例!");
} else {
log.info("线程 [ " + Thread.currentThread().getName() + " ] 已有WebClient实例,直接使用. . .");
}
return webClient;
}
/**
* 获取一个模拟FireFox3.6版本的WebClient实例
*
* @return 模拟FireFox3.6版本的WebClient实例
*/
public WebClient crawlPageWithAnalyseJs() {
WebClient webClient;
/**
* 如果当前线程已有WebClient实例,则直接返回该实例
* 否则重新创建一个WebClient实例并存储于当前线程的本地变量存储器
*/
if ((webClient = clientThreadLocal.get()) == null) {
webClient = new WebClient(BrowserVersion.CHROME);
webClient.getOptions().setCssEnabled(false);
webClient.getOptions().setJavaScriptEnabled(true); //需要解析js
webClient.getOptions().setThrowExceptionOnScriptError(false); //解析js出错时不抛异常
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
webClient.getOptions().setTimeout(1000);
clientThreadLocal.set(webClient);
log.info("为线程 [ " + Thread.currentThread().getName() + " ] 创建新的WebClient实例!");
} else {
log.info("线程 [ " + Thread.currentThread().getName() + " ] 已有WebClient实例,直接使用. . .");
}
return webClient;
}
public WebClient crawlPageWithoutAnalyseJs(){
WebClient webClient;
/**
* 如果当前线程已有WebClient实例,则直接返回该实例
* 否则重新创建一个WebClient实例并存储于当前线程的本地变量存储器
*/
if ((webClient = clientThreadLocal.get()) == null) {
webClient.getOptions().setCssEnabled(false);
webClient.getOptions().setJavaScriptEnabled(false);
webClient.getOptions().setTimeout(20000);
webClient.setJavaScriptErrorListener(new MyJSErrorListener());
webClient.setJavaScriptTimeout(5000);
clientThreadLocal.set(webClient);
log.info("为线程 [ " + Thread.currentThread().getName() + " ] 创建新的WebClient实例!");
} else {
log.info("线程 [ " + Thread.currentThread().getName() + " ] 已有WebClient实例,直接使用. . .");
}
return webClient;
}
}
有时候抓取某些网站即使设置webClient.getOptions().setThrowExceptionOnScriptError(false);
也不能关闭提示信息导致每次访问网页都会打印一大串无用信息,但是我们并不关心js报错问题,只关心拿下来页面的结果。
所以我们可以针对【DefaultJavaScriptErrorListener 】重写它里面的方法,把所有log输出语句全部删除,就可以不打印错误信息了
public class MyJSErrorListener extends DefaultJavaScriptErrorListener {
@Override
public void scriptException(HtmlPage page, ScriptException scriptException) {
}
@Override
public void timeoutError(HtmlPage page, long allowedTime, long executionTime) {
}
@Override
public void malformedScriptURL(HtmlPage page, String url, MalformedURLException malformedURLException) {
}
@Override
public void loadScriptError(HtmlPage page, URL scriptUrl, Exception exception) {
}
@Override
public void warn(String message, String sourceName, int line, String lineSource, int lineOffset) {
}
}
@Slf4j
public class SpiderUtils {
/**
* 获取http请求
*
* @param url
* @return
* @throws Exception
*/
public static String crawlPageApi(String url) throws Exception {
// WebClient webClient = PooledClientFactory.getInstance().getClient();
WebClient webClient = ThreadLocalClientFactory.getInstance().crawlPageApi();
//抓取网页
Page page = webClient.getPage(url);
//打印当前线程名称及网页标题
log.info(Thread.currentThread().getName() + " [ " + url + " ] : " + page.toString());
WebResponse response = page.getWebResponse();
String json = response.getContentAsString();
log.info(Thread.currentThread().getName() + " [ " + json + " ] : ");
return json;
}
/**
* 功能描述:抓取页面时并解析页面的js
*
* @param url
* @throws Exception
*/
public static HtmlPage crawlPageWithAnalyseJs(String url) throws Exception {
WebClient webClient = ThreadLocalClientFactory.getInstance().crawlPageWithAnalyseJs();
//抓取网页
HtmlPage page = webClient.getPage(url);
webClient.waitForBackgroundJavaScript(1000);
//打印当前线程名称及网页标题
System.out.println(Thread.currentThread().getName() + " [ " + url + " ] : " + page.getTitleText());
return page;
}
/**
* 功能描述:抓取页面时不解析页面的js
*
* @param url
* @throws Exception
*/
public static HtmlPage crawlPageWithoutAnalyseJs(String url) throws Exception {
WebClient webClient = ThreadLocalClientFactory.getInstance().crawlPageWithAnalyseJs();
//抓取网页
HtmlPage page = webClient.getPage(url);
//打印当前线程名称及网页标题
System.out.println(Thread.currentThread().getName() + " [ " + url + " ] : " + page.getTitleText());
return page;
}
}
目前来说,只是简单运用爬虫爬取抓取对应数据进行分析,具体的爬取规则需要根据实际情况来制定,数据量过大的时候还需要考虑通过读写分离,分库分表来解决效率问题