基于Jsoup实现的简单爬虫
Jsoup 概念
jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。
其实在这之前我解析Html一直都是使用HtmlPaser来做,在我第一次看到Jsoup的时候,我就在思考Jsoup的存在意义,既然已经有htmlPaser为什么还会Jsoup出现。经过自己动手试验了一下,我感觉到了Jsoup的魅力。
- 首先是更新频率,Htmlpaser早在06年就停止更新了,这十年间互联网技术的变化不可谓不大,htmlpaser或许现在还能勉强使用,但不保证下一刻啊。Jsoup到目前为止还在继续更新,作为一个比Htmlpaser年轻的Html解释器,或许还有很多地方不足,但在持续更新下终究会得到解决。
- 然后是实现原理, htmlpaser是基于fileter进行解析,有些类似对字符串进行正则匹配的方式进行获取。或许是我对htmlpaser还不够熟悉,在一个Paser对象进行pase一次后该paser对象将不可再使用。然而Jsoup却使用了一套自建的Dom结构,该DOM结构就是一个树形数据结构。相比htmlPaser在使用filter转换一次后将不可再次使用的问题,jsoup完全不存在这个问题,在使用getElement*方法后仅仅是获取其子树或者叶子,对原有的树并未做出任何改变。
- 然后是使用方法,而如果需要获取其子节点要么使用filter一步到位,要么就只能根据子节点层次遍历。这种不灵活的节点获取方式造成了很多垃圾代码,也使程序变得十分脆弱。然而Jsoup却使用了一套自建的Dom结构,几乎兼容javascript的dom操作方式,即可取值也可设置。更加需要说明的是jsoup有着类似jquery的dom操作方式,我相信现在绝大多数猿们都会使用jquery,这也是最吸引我的一点。
Jsoup的SSL扩展
现在很多站点都是SSL对数据传输进行加密,这也让普通的HttpConnection无法正常的获取该页面的内容,而Jsoup本身也对次没有做出相应的处理,只是留下来了一个粗糙的使用证书配置什么的方法进行解决。想了一下是否可以让Jsoup可以识别所有的SSL加密过的页面,查询了一些资料,发现可以为本地HttpsURLConnection配置一个“万能证书”,其原理是就是:
- 重置HttpsURLConnection的DefaultHostnameVerifier,使其对任意站点进行验证时都返回true
- 重置httpsURLConnection的DefaultSSLSocketFactory, 使其生成随机证书
代码实现
爬虫代码示例
package org.hanmeis.common.html;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
/**
* Created by zhao.wu on 2016/12/2.
* 该爬虫用于爬去奇书网的玄幻小说类表
*/
public class QiShuuListSpider {
//用于保存小说信息的列表
static List<NovelDir> novelDirs = new LinkedList<>();
public static void main(String[] args) throws IOException {
//解析过程
URL index = new URL("http://www.qisuu.com/soft/sort02/");
parsePage(index);
//将信息存档
FileWriter writer = new FileWriter("qishu.txt");
for (NovelDir novelDir : novelDirs) {
writer.write(novelDir.toString());
}
writer.close();
}
static void parsePage(URL url){
try {
//使用Jsoup的解析方法进行填装Dom
Document doc = Jsoups.parse(url, 1000);
//获取小说列表
Elements novelList = doc.select(".listBox li");
for (Element element : novelList) {
NovelDir dir = new NovelDir();
//获取小说作者
Element authorElement = element.select(".s a").first();
if(authorElement!=null) {
dir.setAuthor(authorElement.html());
}
//获取小说描述
Element descriElement = element.select(".u").first();
if(descriElement!=null) {
dir.setDescription(descriElement.html());
}
//获取标题、目录地址和封面
Element titleElement = element.select("a").last();
if(titleElement!=null) {
dir.setTitle(titleElement.html());
dir.setIndexUrl(titleElement.attr("abs:href"));
Element imageElement = titleElement.select("img").first();
if(imageElement!=null) {
dir.setHeadPic(imageElement.attr("src"));
}
}
System.out.println(dir);
novelDirs.add(dir);
}
//获取下一页的地址,并进行请求
Elements pageDiv = doc.select(".tspage a");
for (Element element : pageDiv) {
if(element.html().equals("下一页")){
//使用“abs:href"获取该页面的绝对地址
String path = element.attr("abs:href");
//由于该站点做了请求频率限制,过快的请求会遭到暂时屏蔽,所以要细水长流的的慢慢请求
Thread.sleep(2000);
parsePage(new URL(path));
}
}
} catch (IOException e) {
System.out.println(url);
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 小说MATE数据对象
*/
static class NovelDir{
//封面
private String headPic;
//作者
private String author;
//标题
private String title;
//目录地址
private String indexUrl;
//大概描述
private String description;
//getter, setter toString
}
}
SSL扩展代码
package org.hanmeis.common.html;
import org.jsoup.Connection;
import org.jsoup.helper.HttpConnection;
import org.jsoup.nodes.Document;
import javax.net.ssl.*;
import java.io.IOException;
import java.net.URL;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* Created by zhao.wu on 2016/11/29.
*/
public class Jsoups{
static{
try {
//重置HttpsURLConnection的DefaultHostnameVerifier,使其对任意站点进行验证时都返回true
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
//创建随机证书生成工厂
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new X509TrustManager[] { new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
} }, new SecureRandom());
//重置httpsURLConnection的DefaultSSLSocketFactory, 使其生成随机证书
HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 使用ssl的方式去获取远程html的dom,
* 该方法在功能上与Jsoup本身的转换工具一样,
* 仅仅是用来告诉代码阅读者这个方法已经对SSL进行了扩展
* @param url 需要转换的页面地址
* @param timeoutMillis 请求超市时间
* @return 该页面的dom树
* @throws IOException 请求异常或者转换异常时抛出
*/
public static Document parse(URL url, int timeoutMillis) throws IOException {
Connection con = HttpConnection.connect(url);
con.timeout(timeoutMillis);
return con.get();
}
}