在抓取一些新闻、博客类页面时,我们会遇到这样的需求:有些文章会分成几页显示,每页都是不同的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();
}
}
算不上简单,看看以后怎么优化吧。