在抓取一些新闻、博客类页面时,我们会遇到这样的需求:有些文章会分成几页显示,每页都是不同的HTML页面,而我们最终想要的数据,肯定是一个整合好的结果。那么,如何把这些分页显示的文章整合起来呢?

这个功能在Spiderman中已经实现,使用的方式是:一旦发现分页,则进入递归下载和解析模式,直到下载完成,然后合并、保存!但是在webmagic中,所有的页面解析都是独立的,解析器没有办法去调用一个下载方法,并返回结果。替代方案是:可以在Request里保存上一次抽取的结果,然后在最后的页面把它们拼装起来。

但是这种方式并不是那么完美:这里假定了爬取的顺序,而如果第一次进入的不是第一页,而是中间某个页面呢?那么会不会存在部分抓取或者重复抓取?同时在抽取过程中,又重新递归的启动一个爬虫,也让整个爬虫的生命周期管理变得更困难。

秉承每个抽取器互相独立的目标,最后决定不要在抽取逻辑中进行处理,而放到抽取之后。每个抽取仍然相互独立,但是在最后输出时有一道“把关”,它等待知道分页的所有结果都返回后,再进行输出。

在webmagic里,Pipeline是可以嵌套的,于是就有了PagedPipeline。这里会收集所有带分页的数据,延迟直到数据整合完成,再统一输出。虽然稍微绕了点,但是保证了模块独立性(Pipeline之前的模块无须知道分页逻辑)。目前是用内存中Map实现的,如果要分布式的话,用远程存储就可以了。

这里面有几个问题:

  • 哪些分页该聚合到一起?
    可以根据页面元素抽取出一个公共ID作为key。
  • 如何知道分页有没有都抓取到?
    每个页面都列出自己还要抓取哪些页面,当待抓取集合和已抓取结合重合时,则结束并合并到一条记录中输出。
  • 页面顺序如何?
    一般根据page数字从小到大排列。
  • 结果如何拼接?
    实现一个combine方法吧。

于是设计了这么一个接口:

<!-- lang: java -->
public interface PagedModel {

    public String getPageKey();

    public Collection<String> getOtherPages();

    public String getPage();

    public PagedModel combine(PagedModel pagedModel);

}

需要分页的对象,实现这个接口就相当于回答了这几个问题。

拿网易新闻做了一个例子:

<!-- lang: java -->
@TargetUrl("http://news.163.com/\\d+/\\d+/\\d+/\\w+*.html")
public class News163 implements PagedModel, AfterExtractor {

    @ExtractByUrl("http://news\\.163\\.com/\\d+/\\d+/\\d+/(\\w+)*\\.html")
    private String pageKey;

    @ExtractByUrl(value = "http://news\\.163\\.com/\\d+/\\d+/\\d+/\\w+_(\\d+)\\.html", notNull = false)
    private String page;

    private List<String> otherPage;

    @ExtractBy("//h1[@id=\"h1title\"]/text()")
    private String title;

    @ExtractBy("//div[@id=\"epContentLeft\"]")
    private String content;

    @Override
    public String getPageKey() {
        return pageKey;
    }

    @Override
    public Collection<String> getOtherPages() {
        return otherPage;
    }

    @Override
    public String getPage() {
        if (page == null) {
            return "1";
        }
        return page;
    }

    @Override
    public PagedModel combine(PagedModel pagedModel) {
        News163 news163 = new News163();
        News163 pagedModel1 = (News163) pagedModel;
        news163.content = this.content + pagedModel1.content;
        return news163;
    }

    @Override
    public String toString() {
        return "News163{" +
                "content='" + content + '\'' +
                ", title='" + title + '\'' +
                ", otherPage=" + otherPage +
                '}';
    }

    public static void main(String[] args) {
        OOSpider.create(Site.me().addStartUrl("http://news.163.com/13/0802/05/958I1E330001124J_2.html"), News163.class)
                .clearPipeline().pipeline(new PagedPipeline()).pipeline(new ConsolePipeline()).run();
    }

    @Override
    public void afterProcess(Page page) {
        Selectable xpath = page.getHtml().xpath("//div[@class=\"ep-pages\"]//a/@href");
        otherPage = xpath.regex("http://news\\.163\\.com/\\d+/\\d+/\\d+/\\w+_(\\d+)\\.html").all();
    }
}

算不上简单,看看以后怎么优化吧。