基于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();
    }   
}
posted @ 2016-12-09 15:19  吴昭  阅读(130)  评论(0编辑  收藏  举报