htmlunit是一款开源的Java页面分析工具,读取页面后,可以有效的使用htmlunit 分析页面上的内容。项目可以模拟浏览器运行,被誉为Java浏览器的开源实现。这个没有界面的浏览器,运行速度也是非常迅速的。
<!-- https://mvnrepository.com/artifact/net.sourceforge.htmlunit/htmlunit -->
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.42.0</version>
</dependency>
在接口中设置默认方法,子类不用实现即可调用。
public interface NewsPuller {
void pullNews();
default Document getHtmlFromUrl(String url, boolean useHtmlUnit) throws Exception {
if (!useHtmlUnit) {
return Jsoup.connect(url)
//模拟火狐浏览器
.userAgent("Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)")
.get();
}
WebClient webClient = new WebClient(BrowserVersion.CHROME); //新建一个模拟谷歌Chrome浏览器的浏览器客户端对象
webClient.getOptions().setJavaScriptEnabled(true); //很重要,启用JS
webClient.getOptions().setCssEnabled(false); //是否启用CSS, 因为不需要展现页面, 所以不需要启用
webClient.getOptions().setActiveXNative(false);
webClient.getOptions().setCssEnabled(false);
webClient.getOptions().setThrowExceptionOnScriptError(false); //当JS执行出错的时候是否抛出异常
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); //当HTTP的状态非200时是否抛出异常
//webClient.setAjaxController(new NicelyResynchronizingAjaxController());//设置支持AJAX
webClient.getOptions().setUseInsecureSSL(true);
webClient.getOptions().setTimeout(10 * 1000);
HtmlPage rootPage = null;
try {
rootPage = webClient.getPage(url);
webClient.waitForBackgroundJavaScript(10 * 1000); //异步JS执行需要耗时,所以这里线程要阻塞10秒,等待异步JS执行结束
String htmlStr = rootPage.asXml(); //直接将加载完成的页面转换成xml格式的字符串
//System.out.println(htmlStr);
return Jsoup.parse(htmlStr); //获取html文档
} finally {
webClient.close();
}
}
}
有时你想模仿一个特殊的浏览器,这可以通过WebClient构造函数的com.gargoylesoftware.htmlunit.BrowserVersion 参数实现,其中已经提供一些常见浏览器的常量,但是,你可以通过BrowserVersion 的实例说明创建你自己拥有的特殊版本。
WebClient webClient = new WebClient(BrowserVersion.CHROME); //新建一个模拟谷歌Chrome浏览器的浏览器客户端对象
指定这个BrowserVersion 会改变用户代理发送到服务器的报头,也会改变一些JavaScript 的行为。
HtmlUnit对JavaScript的支持是其最大的亮点,也是其最需要完善的地方。总的来说HtmlUnit是一款很棒的java工程,值得我们花一些时间来学习和尝试,给我们的武器库增加一件武器,也许什么时候你就会用到它。
webClient.getOptions().setJavaScriptEnabled(true); //很重要,启用JS
结合 Jsoup + HtmlUtil,爬取凤凰网新闻为例子:
@Component("ifengNewsPuller")
public class IfengNewsPuller implements NewsPuller {
private static final Logger logger = LoggerFactory.getLogger(IfengNewsPuller.class);
@Value("${news.ifeng.url}")
private String url;
@Autowired
private NewsService newsService;
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void pullNews() {
logger.info("开始拉取凤凰新闻!");
// 1. 获取首页
Document html = null;
try {
html = getHtmlFromUrl(url, false);
} catch (Exception e) {
logger.error("==============获取凤凰首页失败: {} =============", url);
e.printStackTrace();
return;
}
// 2. jsoup 获取新闻 <a> 标签
Elements newsATags = html.select("div#newsList")
.select("ul.news_list-3wjAJJJM")
.select("li")
.select("a");
// 3.从<a>标签中抽取基本信息,封装成news
HashSet<News> newsSet = new HashSet<>();
for (Element a : newsATags) {
String url = a.attr("href");
String title = a.text();
News n = new News();
n.setSource("凤凰");
n.setUrl(url);
n.setTitle(title);
n.setCreateDate(new Date());
newsSet.add(n);
}
// 4.根据新闻url访问新闻,获取新闻内容
newsSet.parallelStream().forEach(news -> {
logger.info("开始抽取凤凰新闻《{}》内容:{}", news.getTitle(), news.getUrl());
Document newsHtml = null;
try {
newsHtml = getHtmlFromUrl(news.getUrl(), false);
Elements contentElement = newsHtml.select("div.text-3zQ3cZD4");
if (contentElement.isEmpty()) {
contentElement = newsHtml.select("div.caption-3_nUnnKX h1");
}
if (contentElement.isEmpty()) {
return;
}
// 直接从头部信息获取部分数据
String time = newsHtml.head().select("meta[name=og:time ]").attr("content");
if (StringUtils.isNotBlank(time)) {
news.setNewsDate(sdf.parse(time));
}
String content = contentElement.toString();
String image = NewsUtils.getImageFromContent(content);
news.setContent(contentElement.text());
news.setImage(image);
newsService.saveNews(news);
logger.info("抽取凤凰新闻《{}》成功!", news.getTitle());
} catch (Exception e) {
logger.error("凤凰新闻抽取失败:{}", news.getUrl());
e.printStackTrace();
}
});
logger.info("凤凰新闻抽取完成!");
}
}
/**
* @Description: http工具(使用net.sourceforge.htmlunit获取完整的html页面,即完成后台js代码的运行)
* 参考1: https://www.cnblogs.com/davidwang456/articles/8693050.html
* 参考2: https://blog.csdn.net/hundan_520520/article/details/79387982
* @Author Ray
* @Date 2020/8/6 0006 13:29
* @Version 1.0
*/
public class HttpUtils {
/**
* 请求超时时间,默认20000ms
*/
private int timeout = 200000;
/**
* 等待异步JS执行时间,默认20000ms
*/
private int waitForBackgroundJavaScript = 20000;
/**
* HttpUtils 实例
*/
private static HttpUtils httpUtils;
/**
* 单例模式 - 私有化构造函数
*/
private HttpUtils() {
}
/**
* 单例模式 - 获取实例
*/
public static HttpUtils getInstance() {
if (null == httpUtils) {
httpUtils = new HttpUtils();
}
return httpUtils;
}
/**
* 将网页内容返回为解析后的文档格式
* @param html 待解析的页面
* @return 解析后的文档
* @throws Exception
*/
public static Document parseHtmlToDoc(String html) throws Exception {
return removeHtmlSpace(html);
}
/**
* 转换空格
*/
private static Document removeHtmlSpace(String str) {
Document doc = Jsoup.parse(str);
String result = doc.html().replace(" ", "");
return Jsoup.parse(result);
}
/**
* 将网页地址返回为解析后的文档格式
* @param url 待解析的页面地址
* @return 解析后的文档
* @throws Exception
*/
private static Document connectToDoc(String url) throws Exception {
return Jsoup.connect(url)
//模拟火狐浏览器
.userAgent("Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)")
.get();
}
/**
* 解析页面
* 默认解析静态页面,如果需要爬取动态数据,请调用重载方法并设置为 true
* @param url
* @return
* @throws Exception
*/
public String getHtmlPageResponse(String url) throws Exception {
return getHtmlPageResponse(url, false);
}
public String getHtmlPageResponse(String url, boolean useHtmlUnit) throws Exception {
if (!useHtmlUnit) {
return connectToDoc(url).toString();
}
WebClient webClient = new WebClient(BrowserVersion.CHROME); //新建一个模拟谷歌Chrome浏览器的浏览器客户端对象
webClient.getOptions().setJavaScriptEnabled(true); //很重要,启用JS
webClient.getOptions().setCssEnabled(false); //是否启用CSS, 因为不需要展现页面, 所以不需要启用
webClient.getOptions().setActiveXNative(false);
webClient.getOptions().setCssEnabled(false);
webClient.getOptions().setThrowExceptionOnScriptError(false); //当JS执行出错的时候是否抛出异常
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); //当HTTP的状态非200时是否抛出异常
webClient.setAjaxController(new NicelyResynchronizingAjaxController());//设置支持AJAX
webClient.getOptions().setTimeout(timeout); //设置“浏览器”的请求超时时间
webClient.setJavaScriptTimeout(timeout); //异步JS执行需要耗时, 等待异步JS执行结束
HtmlPage rootPage;
String result = "";
try {
rootPage = webClient.getPage(url); //设置链接地址
webClient.waitForBackgroundJavaScript(waitForBackgroundJavaScript); //该方法阻塞线程
result = rootPage.asXml(); //直接将加载完成的页面转换成xml格式的字符串
} finally {
webClient.close();
}
return result;
}
/**
* 获取页面文档Document对象
* 默认 false
*/
public Document getHtmlPageResponseAsDocument(String url) throws Exception {
return parseHtmlToDoc(getHtmlPageResponse(url, false));
}
/**
* 获取页面文档Document对象
* (如果为 true 等待异步JS执行)
*/
public Document getHtmlPageResponseAsDocument(String url, boolean useHtmlUnit) throws Exception {
return parseHtmlToDoc(getHtmlPageResponse(url, useHtmlUnit));
}
public int getTimeout() {
return this.timeout;
}
/**
* 设置请求超时时间
*/
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public int getWaitForBackgroundJavaScript() {
return waitForBackgroundJavaScript;
}
/**
* 设置获取完整HTML页面时等待异步JS执行的时间
*/
public void setWaitForBackgroundJavaScript(int waitForBackgroundJavaScript) {
this.waitForBackgroundJavaScript = waitForBackgroundJavaScript;
}
}
/**
* @Description: HttpUtils 工具类测试
* @Author Ray
* @Date 2020/8/6 0006 14:22
* @Version 1.0
*/
@SpringBootTest
public class HttpUtilsTest {
private static final String TEST_URL_STATIC = "https://www.baidu.com/";
private static final String TEST_URL_NOT_STATIC = "http://www.ifeng.com/";
/**
* 处理静态页面
*/
@Test
public void testGetHtmlPageResponse() {
HttpUtils httpUtils = HttpUtils.getInstance();
httpUtils.setTimeout(30000);
httpUtils.setWaitForBackgroundJavaScript(30000);
try {
String htmlPageStr = httpUtils.getHtmlPageResponse(TEST_URL_STATIC);
System.out.println(htmlPageStr);
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void testGetHtmlPageResponseAsDocument() {
HttpUtils httpUtils = HttpUtils.getInstance();
httpUtils.setTimeout(30000);
httpUtils.setWaitForBackgroundJavaScript(30000);
try {
Document document = httpUtils.getHtmlPageResponseAsDocument(TEST_URL_STATIC);
System.out.println(document);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 处理非静态页面
*/
@Test
public void testGetHtmlPageResponse2() {
HttpUtils httpUtils = HttpUtils.getInstance();
httpUtils.setTimeout(30000);
httpUtils.setWaitForBackgroundJavaScript(30000);
try {
String htmlPageStr = httpUtils.getHtmlPageResponse(TEST_URL_NOT_STATIC, true);
System.out.println(htmlPageStr);
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void testGetHtmlPageResponseAsDocument2() {
HttpUtils httpUtils = HttpUtils.getInstance();
httpUtils.setTimeout(30000);
httpUtils.setWaitForBackgroundJavaScript(30000);
try {
Document document = httpUtils.getHtmlPageResponseAsDocument(TEST_URL_NOT_STATIC, true);
System.out.println(document);
} catch (Exception e) {
e.printStackTrace();
}
}
}