使用jsoup做任意网站的客户端

jsoup是一个解析网页源码的开源库,他能按照给定的规则提取出一个网页中的任意元素,和其他网页解析库不同的是,他提取网页内容的方式和css、jquery的选择器非常相似。因此如果你懂得前端的知识,只需根据以下的代码样例就可以在3分钟之内学会jsoup的用法:

            Document doc = Jsoup.connect(href).timeout(10000).get(); 
            Element masthead = doc.select("div.archive-list").first();
            Elements titleElements = masthead.select("div.archive-list-item h4 a");    
            Elements summaryElements = masthead.select("div.archive-list-item div.post-intro p");
            Elements imgElements = masthead.select("div.archive-list-item img");

没错就是3分钟,我第一次接触jsoup完全没有看官方文档,直接在网上下载了jsoup的jar包,然后找了一段jsoup的例子程序就开始用他来解析网页了,最开始我想做一个cnbeta的客户端,非常顺利,从cnbeta网页上解析得到的数据就跟是cnbeta专门为我提供的一样。

不过需要明白的是使用jsoup开发客户端并不是一个客户端开发的首选,一般是针对那些没有为你提供客户端接口的网站,一个标准的客户端接口应该解析的数据形式是json或者xml,有些网站都提供了rss的功能,rss其实就是xml格式的,所以开发一个网站的客户端可以基于一个网站的rss数据。

那么为什么还要用jsoup呢,原因有两点:1、不是所有网站都有rss;2、有的网站rss功能比较全,能够覆盖网站的大部分内容,但有的网站rss很简单,基本就是摆设。

而使用jsoup,你在网站上能看到的任何东西都可以解析出来。

但是jsoup开发网站客户端其实有个弊端,因此给自己的网站做客户端绝对不会用jsoup,而是专门写接口。

那就是一旦网站改版,原来的解析规则就失效了,客户端上可能显示不出任何数据,而rss一般很难得改一次。

因此使用jsoup开发网站客户端最好针对那些版面比较固定的网站。

回到我们的话题,我们将针对jcodecraeer的《综合资讯》栏目的文章列表 (http://jcodecraeer.com/plus/list.php?tid=4)  做解析来得到文章列表并显示在一个ListView中。如果你学会了这点,解析一个网站就不成问题了,当然有些网站的解析要复杂一些,你必须先对这个网站做一些数据分析,提炼出一些数据模型。

提取数据模型

我们假设 http://jcodecraeer.com/plus/list.php?tid=4 就是一个网站(事实上它只是一个栏目),来看看我们能够提炼出什么数据模型。

这个网页上有导航栏,还有右边的一些相关文章之类的。但是我只关心文章列表,仔细观察文章列表,其实每一项都是固定的:标题、缩略图、摘要、发表时间、标签、作者、阅览数,这就是我们的数据模型,是一篇文章的模型。为了在ListView中显示文章列表,我们新建一个用于表示一篇文章的Article类:

package com.jcodecraeer.newsapp;
public class Article {
    private String title;
    private String summary;
    private String url;
    private String imageUrl;
    private String postTime;
 
    public void setTitle(String title) {
        this.title = title;
    }
    
    public String getTitle() {
        return title;
    }
    
    public void setSummary(String summary) {
        this.summary = summary;
    }
    
    public String getSummary() {
        return summary;
    }
    
    public void setUrl(String url){
        this.url=url;
    }
    
    public String getUrl(){
        return url;
    }    
    
    public void setImageUrl(String imageUrl){
        this.imageUrl = imageUrl;
    }
    
    public String getImageUrl(){
        return imageUrl;
    }        
    
    public void setPostTime(String postTime){
        this.postTime = postTime;
    }
    
    public String getPostTime(){
        return postTime;
    }    
    
}

接下来的任务就是解析网页

一个网页是由html标签组成的,要查看这些代码可以直接在网页的任何位置右键,在弹出的菜单中点击查看网页源代码。

网页的源码很多,有很多是我们不关心的,你的任务是迅速找到目标代码块,这篇文章的目的是要提取文章列表,所以我找到了文章列表相关的代码块(要熟练的找到需要一点点前端的知识):

QQ图片20141226204637.png

从上面的代码我可以肯定,文章列表位于

<div class="archive-list">

所包含的div中,而每一篇文章又是包含在

<div class="archive-list-item">

div中。因此我们先找出class="archive-list"的节点,确保后续的解析是文章相关的代码,然后再在这个节点之下解析每篇文章以及文章的数据项。

找出archive-list节点:

Jsoup连接网页:

Document doc = Jsoup.connect("http://jcodecraeer.com/plus/list.php?tid=4").timeout(10000).get();

找出archive-list节点:

Element masthead = doc.select("div.archive-list").first();

注意最后必须加上first()方法,不然得到的不是单个数据而是一组数据。select("div.archive-list")中select顾名思义是选择的意思,其中“div.archive-list”是css 选择器的写法,意思是class=“archive-list”的div,所以

doc.select("div.archive-list")的意思就是找出class=“archive-list”的所有div,doc.select("div.archive-list").first()的意思就是找出第一个class=“archive-list”的div。

获取每篇文章直接相关的html元素,比如与文章标题相关的直接元素为class="archive-list-item"的div 下面的h4 标签下面的超链接中,用Jsoup表示就是:

 Elements titleElements = masthead.select("div.archive-list-item h4 a");

上面获取了所有文章的标题相关的html元素(这里是个超链接),返回的结果是Elements类型,他是一个List类型,按照同样的道理我们再获取所有缩略图,所有摘要,所有发表时间,他们的顺序应该是一一对应的,然后根据其中任意一个的Elements数量来遍历,将这些数据一条一条的赋予上面我们定义的Article模型:

            Document doc = Jsoup.connect(href).timeout(10000).get(); 
            Element masthead = doc.select("div.archive-list").first();
            Elements titleElements = masthead.select("div.archive-list-item h4 a");    
            Elements summaryElements = masthead.select("div.archive-list-item div.post-intro p");
            Elements imgElements = masthead.select("div.archive-list-item img");   
            Elements postTimeElements = masthead.select("div.archive-list-item div.post-intro span.date");
            int count=titleElements.size();
            Log.i("count","count = " + count);        
            for(int i = 0;i < count;i++) {
                Article article = new Article();
                Element titleElement = titleElements.get(i);
                Element summaryElement = summaryElements.get(i);
                Element imgElement = imgElements.get(i);
                String url = titleElement.attr("href"); 
                url="http://www.jcodecraeer.com/" + url;
                       
                String title = titleElement.text();
                String summary = summaryElement.text();
                String imgsrc ="http://www.jcodecraeer.com/" + imgElement.attr("src");
                article.setTitle(title);
                article.setSummary(summary);
                article.setImageUrl(imgsrc);
                articleList.add(article);
            }

以标题为例,遍历的时候我们取出一篇文章的title节点:

 Element titleElement = titleElements.get(i);

然后用 String title = titleElement.text();获取标题的文字,以

 String url = titleElement.attr("href"); 
 url="http://www.jcodecraeer.com/" + url;

获取文章的超链接,注意html源码中这个超链接是相对路径,因此我们增加了网站的网址来组成完整路径。之所以可以使用

titleElement.attr("href");

是因为这是一个a标签,a标签是有href属性的。

好了,上面的这种方式在后来我发现是有问题的,因为文章标题的列表和缩略图的列表并不是一一对应的,有些文章可能没有缩略图,所以我们换一种方式,获得包含一篇文章所有信息的节点,然后针对每一个节点在for循环内部再解析出文章的每一个数据项。如果没有缩略图,该数据项为空就是了,这就不会有任何问题。

下面是经过改进后的代码,这是一个解析的过程因此我将方法命名为parseArticleList:

    public ArrayList<Article>  parseArticleList(String href, final int page){
        ArrayList<Article> articleList = new ArrayList<Article>();
        try {
            href = \_MakeURL(href, new HashMap<String, Object>(){{
                put("PageNo", page);
            }});
            Log.i("url","url = " + href);
            Document doc = Jsoup.connect(href).timeout(10000).get(); 
            Element masthead = doc.select("div.archive-list").first();
            Elements articleElements =  masthead.select("div.archive-list-item");        
            for(int i = 0; i < articleElements.size(); i++) {
                Article article = new Article();
                Element articleElement = articleElements.get(i);
                Element titleElement = articleElement.select("h4 a").first();
                Element summaryElement = articleElement.select("div.post-intro p").first();
                Element imgElement = null;
                if(articleElement.select("img").size() != 0){
                   imgElement = articleElement.select("img").first();
                }
                Element timeElement = articleElement.select(".date").first();
                String url = "http://www.jcodecraeer.com" + titleElement.attr("href"); 
                String title = titleElement.text();
                String summary = summaryElement.text();
                String imgsrc = "";
                if(imgElement != null){
                    imgsrc  ="http://www.jcodecraeer.com" + imgElement.attr("src");
                }
              
                String postTime = timeElement.text();
                article.setTitle(title);
                article.setSummary(summary);
                article.setImageUrl(imgsrc);
                article.setPostTime(postTime);
                article.setUrl(url);
                articleList.add(article);
            }
        } catch (Exception e) {
             e.printStackTrace();
        }
        
        return articleList;
    }

parseArticleList()方法返回了 ArrayList

的集合,有了它你应该知道如何在ListView中使用了吧。

分页问题

上面所讨论的仅仅是单个网页,在这个栏目下有很多页数据,因此我们还需要考虑页码的问题,如果还有更多的页,当ListView滑动到最底下自动加载下一页的数据。

实现自动加载下一页ListView我已经放在了文末给出的完整源码中,这里就不讨论了,这里要继续讨论的是如何处理好分页的问题。

首先我们要先分析这个网站文章列表的url地址,第一页的url地址为:http://jcodecraeer.com/plus/list.php?tid=4 第二页为http://jcodecraeer.com/plus/list.php?tid=4&TotalResult=454&PageNo=2,第三页为:

http://jcodecraeer.com/plus/list.php?tid=4&TotalResult=454&PageNo=3,显然,不同页之间url上的区别是仅仅PageNo参数。那么当我们加载更多页面的时候只需把请求的url的PageNo换一下就行了,然而问题是如何确定是第几页呢?

我们再看,发现每一页文章数为10,所以我们判断加载页数的依据是:

 int pageIndex = mArticleList.size() / 10 + 1;

还有一个问题,如何判断是否能够加载更多页,我们怎么确定第九页之后还有第10页呢?

if (articleList.size() < 10) {
         //已经加载完了  
} else if (articleList.size() == 10) {
      //还有更多页 
}

当然这个判断方法有缺陷,如果第9页刚刚有10条数据,而么有第10页,这种情况是可能的,但是这不是什么大问题。

  1. 这种几率很小 

  2. 即便这种情况存在,当加载10页的数据什么也没有,articleList = 0 小于10 ,进入第一个判断条件。也达到了目的。

图片的异步加载

图片的缩略图加载是需要异步的,你可以使用 universal image loader ,但在我们给出的demo中使用的是自己实现的一个ImageLoader。

运行界面

1419606579627661.png

在这篇文章中我们只是简单的完成了文章列表的展示,你还可以根据我所提供的方法实现更多的功能,除了Jsoup的使用之外,更重要的是分析一个网站,我已经采用这种方法把一个专门介绍日本爱情动作片的网站全部解析做成了客户端。事实证明这种方式是完全可行的。其实即便是网站改版,也可以很快写出新的解析规则,我们甚至可以将解析规则放到网络上,这样到网站改版的时候,就不需要变更客户端的代码使客户端照常正常运行。

Jsoup的应用远远不止于此,我们可以用Jsoup解析出整个网站,将网站的所有数据提炼出来从未到达采集网站的目的。

读完这篇文章你可能会产生一个疑问,为什么不直接把整个列表取出来放到webview中呢,其实这个问题就好比是使用html5还是原生app的问题。webview的效率和体验在短时间内难以超越原生应用。

代码下载:http://pan.baidu.com/s/1c0s5MiG