Xpider框架开发文档

网络爬虫

浏览数:94

2019-9-13

AD:资源代下载服务

INTRODUCTION:

为了防止每届实验室学生都重复造轮子,我将之前编写的爬虫程序封装好,做成了一个功能比较完善的框架。大家只要按照说明文档及事例定制开发即可。

【吐槽:谁都不想重复造轮子,关键是之前留下来的爬虫代码写的太乱了,可读性差,又难维护,出现个错误就得改半天。我们太难了,嘤嘤嘤。。。】

简单介绍:

Xpider一款简单灵活的爬虫框架。该框架可以很便捷的帮助你编写一个爬虫。

该文档包括Xpider的使用方式+爬虫开发的一些程序实例。

重点是:可读性好!易维护!

一、Xpider框架概述:

Xpider项目代码分为核心和扩展两部分。

  • 核心部分(Xpider-core)是一个精简的、模块化的爬虫实现。
  • 扩展部分则包括一些便利的、实用性的功能。

Scrapy作为业界最优秀的爬虫框架,优点就不用说了,但是对于普通的项目需求,这个框架就显得臃肿复杂了。Xpider目的就是将框架做的轻量级,其的架构设计参考了Scrapy,目标是尽量的模块化。同时,在实现时则应用了HttpClient、Jsoup等Java世界最成熟的工具,目标是尽量让同学们接触一些框架之外的成熟工具包。

总体架构:

Xpider的结构分为DownloaderPageProcessorSchedulerPipeline四大组件,分别对应爬虫生命周期中的下载、处理、管理和持久化等功能。

而Spider则将这几个组件组织起来,让它们可以交互,流程化的执行,可以认为Spider是一个大的容器,它也是Xpider逻辑的核心。

Xpider总体架构图如下:

Xpider的四个组件简介:

1.Downloader

Downloader负责从互联网上下载页面,以便后续处理。Xpider默认使用Apache HttpClient作为下载工具。

2.PageProcessor

PageProcessor负责解析页面,抽取有用信息,以及发现新的链接。Xpider使用Jsoup作为HTML解析工具。

PageProcessor对于每个站点每个页面都不一样,是需要使用者定制的部分。

3.Scheduler

Scheduler负责管理待抓取的URL,以及一些去重的工作。Xpider默认提供了JDK的内存队列来管理URL,并用集合来进行去重。也支持使用Redis进行分布式管理。

除非项目有一些特殊的分布式需求,否则无需自己定制Scheduler。

4.Pipeline

Pipeline负责抽取结果的处理,包括计算、持久化到文件、数据库等。Xpider默认提供了“输出到控制台”和“保存到文件”两种结果处理方案。

Pipeline定义了结果保存的方式,如果你要保存到指定数据库,则需要编写对应的Pipeline。对于一类需求一般只需编写一个Pipeline

1.2.2 用于数据流转的对象

1. Request

Request是对URL地址的一层封装,一个Request对应一个URL地址。

它是PageProcessor与Downloader交互的载体,也是PageProcessor控制Downloader唯一方式。

除了URL本身外,它还包含一个Key-Value结构的字段extra。你可以在extra中保存一些特殊的属性,然后在其他地方读取,以完成不同的功能。例如附加上一个页面的一些信息等。

2. Page

Page代表了从Downloader下载到的一个页面——可能是HTML,也可能是JSON或者其他文本格式的内容。

Page是Xpider抽取过程的核心对象,它提供一些方法可供抽取、结果保存等。在第三章的例子中,我们会详细介绍它的使用。

3. ResultItems

ResultItems相当于一个Map,它保存PageProcessor处理的结果,供Pipeline使用。它的API与Map很类似,值得注意的是它有一个字段skip,若设置为true,则不应被Pipeline处理。

1.2.3 控制爬虫运转的引擎–Spider

Spider是Xpider内部流程的核心。Downloader、PageProcessor、Scheduler、Pipeline都是Spider的一个属性,这些属性是可以自由设置的,通过设置这个属性可以实现不同的功能。Spider也是WebMagic操作的入口,它封装了爬虫的创建、启动、停止、多线程等功能。下面是一个设置各个组件,并且设置多线程和启动的例子。详细的Spider设置请看第四章《爬虫的配置、启动和终止》

public static void main(String[] args) {
    Spider.create(new GithubRepoPageProcessor())
            //从https://github.com/code4craft开始抓    
            .addUrl("https://github.com/code4craft")
            //设置Scheduler,使用Redis来管理URL队列
            .setScheduler(new RedisScheduler("localhost"))
            //设置Pipeline,将结果以json方式保存到文件
            .addPipeline(new JsonFilePipeline("D:\\data\\Xpider"))
            //开启5个线程同时执行
            .thread(5)
            //启动爬虫
            .run();
}

介绍了这么多,其实使用者不需要关心那么多,因为大部分模块Xpider已经提供了默认实现。

一般来说,对于编写一个爬虫,PageProcessor是需要编写的部分,而Spider则是创建和控制爬虫的入口。在第三章中,我们会介绍如何通过定制PageProcessor来编写一个爬虫,并通过Spider来启动。

二、框架的下载与使用

Xpider默认使用Maven管理依赖.

Xpider基于Maven进行构建,推荐使用Maven来安装Xpider。在你自己的项目(已有项目或者新建一个)中添加以下坐标即可:

<dependency>
    <groupId>us.codecraft</groupId>
    <artifactId>Xpider-core</artifactId>
    <version>0.7.3</version>
</dependency>
<dependency>
    <groupId>us.codecraft</groupId>
    <artifactId>Xpider-extension</artifactId>
    <version>0.7.3</version>
</dependency>

在你的项目中添加了Xpider的依赖之后,即可开始第一个爬虫的开发了!我们这里拿一个抓取Github信息的例子:

第一个爬虫实例:

import us.codecraft.Xpider.Page;
import us.codecraft.Xpider.Site;
import us.codecraft.Xpider.Spider;
import us.codecraft.Xpider.processor.PageProcessor;

public class GithubRepoPageProcessor implements PageProcessor {

    private Site site = Site.me().setRetryTimes(3).setSleepTime(100);

    @Override
    public void process(Page page) {
        page.addTargetRequests(page.getHtml().links().regex("(https://github\\.com/\\w+/\\w+)").all());
        page.putField("author", page.getUrl().regex("https://github\\.com/(\\w+)/.*").toString());
        page.putField("name", page.getHtml().xpath("//h1[@class='entry-title public']/strong/a/text()").toString());
        if (page.getResultItems().get("name")==null){
            //skip this page
            page.setSkip(true);
        }
        page.putField("readme", page.getHtml().xpath("//div[@id='readme']/tidyText()"));
    }

    @Override
    public Site getSite() {
        return site;
    }

    public static void main(String[] args) {
        Spider.create(new GithubRepoPageProcessor()).addUrl("https://github.com/Xknight").thread(5).run();
    }
}

三、基于Xpider开发爬虫

在Xpider里,实现一个基本的爬虫只需要编写一个类,实现PageProcessor接口即可。这个类基本上包含了抓取一个网站,你需要写的所有代码。

3.1 PageProcessor的实现

这部分我们直接通过GithubRepoPageProcessor这个例子来介绍PageProcessor的编写方式。我将PageProcessor的定制分为三个部分,分别是爬虫的配置、页面元素的抽取和链接的发现。

public class GithubRepoPageProcessor implements PageProcessor {

    // 部分一:抓取网站的相关配置,包括编码、抓取间隔、重试次数等
    private Site site = Site.me().setRetryTimes(3).setSleepTime(1000);

    @Override
    // process是定制爬虫逻辑的核心接口,在这里编写抽取逻辑
    public void process(Page page) {
        // 部分二:定义如何抽取页面信息,并保存下来
        page.putField("author", page.getUrl().regex("https://github\\.com/(\\w+)/.*").toString());
        page.putField("name", page.getHtml().xpath("//h1[@class='entry-title public']/strong/a/text()").toString());
        if (page.getResultItems().get("name") == null) {
            //skip this page
            page.setSkip(true);
        }
        page.putField("readme", page.getHtml().xpath("//div[@id='readme']/tidyText()"));

        // 部分三:从页面发现后续的url地址来抓取
        page.addTargetRequests(page.getHtml().links().regex("(https://github\\.com/[\\w\\-]+/[\\w\\-]+)").all());
    }

    @Override
    public Site getSite() {
        return site;
    }

    public static void main(String[] args) {

        Spider.create(new GithubRepoPageProcessor())
                //从"https://github.com/Xknight"开始抓
                .addUrl("https://github.com/Xknight")
                //开启5个线程抓取
                .thread(5)
                //启动爬虫
                .run();
    }
}
  •  爬虫的配置

第一部分关于爬虫的配置,包括编码、抓取间隔、超时时间、重试次数等,也包括一些模拟的参数,例如User Agent、cookie,以及代理的设置,我们会在第5章-“爬虫的配置”里进行介绍。在这里我们先简单设置一下:重试次数为3次,抓取间隔为一秒。

  • 页面元素的抽取

第二部分是爬虫的核心部分:对于下载到的Html页面,你如何从中抽取到你想要的信息?WebMagic里主要使用了三种抽取技术:XPath、正则表达式和CSS选择器。另外,对于JSON格式的内容,可使用JsonPath进行解析。

  1. XPath

    XPath本来是用于XML中获取元素的一种查询语言,但是用于Html也是比较方便的。例如:

    page.getHtml().xpath("//h1[@class='entry-title public']/strong/a/text()")

    这段代码使用了XPath,它的意思是“查找所有class属性为’entry-title public’的h1元素,并找到他的strong子节点的a子节点,并提取a节点的文本信息”。

  2. CSS选择器

  CSS选择器是与XPath类似的语言。如果大家做过前端开发,肯定知道$(‘h1.entry-title’)这种写法的含义。客观的说,它比XPath写起来要简单一些,但是如果写复杂一点的抽取规则,就相对要麻烦一点。

  3.正则表达式

  正则表达式则是一种通用的文本抽取语言。

page.addTargetRequests(page.getHtml().links().regex("(https://github\\.com/\\w+/\\w+)").all());

    这段代码就用到了正则表达式,它表示匹配所有”https://github.com/code/Xknight”这样的链接。

   4.JsonPath

    JsonPath是于XPath很类似的一个语言,它用于从Json中快速定位一条内容。WebMagic中使用的JsonPath格式可以参考这里:https://code.google.com/p/json-path/

  • 链接的发现

有了处理页面的逻辑,我们的爬虫就接近完工了!

但是现在还有一个问题:一个站点的页面是很多的,一开始我们不可能全部列举出来,于是如何发现后续的链接,是一个爬虫不可缺少的一部分。

page.addTargetRequests(page.getHtml().links().regex("(https://github\\.com/\\w+/\\w+)").all());

这段代码的分为两部分,page.getHtml().links().regex("(https://github\\.com/\\w+/\\w+)").all()用于获取所有满足”(https:/ /github\.com/\w+/\w+)”这个正则表达式的链接,page.addTargetRequests()则将这些链接加入到待抓取的队列中去。

3.2 Selectable抽取元素

Selectable相关的抽取元素链式API是Xpider的一个核心功能。使用Selectable接口,你可以直接完成页面元素的链式抽取,也无需去关心抽取的细节。

在刚才的例子中可以看到,page.getHtml()返回的是一个Html对象,它实现了Selectable接口。这个接口包含一些重要的方法,我将它分为两类:抽取部分和获取结果部分。

  • 抽取部分API:

方法 说明 示例
xpath(String xpath) 使用XPath选择 html.xpath(“//div[@class=’title’]”)
$(String selector) 使用Css选择器选择 html.$(“div.title”)
$(String selector,String attr) 使用Css选择器选择 html.$(“div.title”,”text”)
css(String selector) 功能同$(),使用Css选择器选择 html.css(“div.title”)
links() 选择所有链接 html.links()
regex(String regex) 使用正则表达式抽取 html.regex(“\<div\>(.\*?)\”)
regex(String regex,int group) 使用正则表达式抽取,并指定捕获组 html.regex(“\<div\>(.\*?)\”,1)
replace(String regex, String replacement) 替换内容 html.replace(“\”,””)

这部分抽取API返回的都是一个Selectable接口,意思是说,抽取是支持链式调用的。下面我用一个实例来讲解链式API的使用。

例如,我现在要抓取github上所有的Java项目,这些项目可以在https://github.com/search?l=Java&p=1&q=stars%3A%3E1&s=stars&type=Repositories搜索结果中看到。

为了避免抓取范围太宽,我指定只从分页部分抓取链接。这个抓取规则是比较复杂的,我会要怎么写呢?

可以先用CSS选择器提取出这个div,然后在取到所有的链接。为了保险起见,我再使用正则表达式限定一下提取出的URL的格式,那么最终的写法是这样子的:

List<String> urls = page.getHtml().css("div.pagination").links().regex(".*/search/\?l=java.*").all();

然后,我们可以把这些URL加到抓取列表中去:

List<String> urls = page.getHtml().css("div.pagination").links().regex(".*/search/\?l=java.*").all();
page.addTargetRequests(urls);
  • 获取结果的API:

当链式调用结束时,我们一般都想要拿到一个字符串类型的结果。这时候就需要用到获取结果的API了。我们知道,一条抽取规则,无论是XPath、CSS选择器或者正则表达式,总有可能抽取到多条元素。WebMagic对这些进行了统一,你可以通过不同的API获取到一个或者多个元素。

方法 说明 示例
get() 返回一条String类型的结果 String link= html.links().get()
toString() 功能同get(),返回一条String类型的结果 String link= html.links().toString()
all() 返回所有抽取结果 List links= html.links().all()
match() 是否有匹配结果 if (html.links().match()){ xxx; }

例如,我们知道页面只会有一条结果,那么可以使用selectable.get()或者selectable.toString()拿到这条结果。

这里selectable.toString()采用了toString()这个接口,是为了在输出以及和一些框架结合的时候,更加方便。因为一般情况下,我们都只需要选择一个元素!

selectable.all()则会获取到所有元素。

3.3 Pipeline保存结果

Xpider用于保存结果的组件叫做Pipeline。例如我们通过“控制台输出结果”这件事也是通过一个内置的Pipeline完成的,它叫做ConsolePipeline。那么,我现在想要把结果用Json的格式保存下来,怎么做呢?我只需要将Pipeline的实现换成”JsonFilePipeline”就可以了。

public static void main(String[] args) {
    Spider.create(new GithubRepoPageProcessor())
            //从"https://github.com/Xknight"开始抓
            .addUrl("https://github.com/Xknight")
            .addPipeline(new JsonFilePipeline("D:\\Xpider\\"))
            //开启5个线程抓取
            .thread(5)
            //启动爬虫
            .run();
}

这样子下载下来的文件就会保存在D盘的Xpider目录中了。另外,通过定制Pipeline,我们还可以实现保存结果到文件、数据库等一系列功能。

至此为止,已经完成了一个基本爬虫的编写,也具有了一些定制功能。

3.4 爬虫的配置、启动和终止

  • Spider

Spider是爬虫启动的入口。在启动爬虫之前,我们需要使用一个PageProcessor创建一个Spider对象,然后使用run()进行启动。同时Spider的其他组件(Downloader、Scheduler、Pipeline)都可以通过set方法来进行设置。

方法 说明 示例
create(PageProcessor) 创建Spider Spider.create(new GithubRepoProcessor())
addUrl(String…) 添加初始的URL spider .addUrl(“https://www.cnblogs.com/X-knight/”)
addRequest(Request…) 添加初始的Request spider .addRequest(“https://www.cnblogs.com/X-knight/”)
thread(n) 开启n个线程 spider.thread(5)
run() 启动,会阻塞当前线程执行 spider.run()
start()/runAsync() 异步启动,当前线程继续执行 spider.start()
stop() 停止爬虫 spider.stop()
test(String) 抓取一个页面进行测试 spider .test(“https://www.cnblogs.com/X-knight/”)
addPipeline(Pipeline) 添加一个Pipeline,一个Spider可以有多个Pipeline spider .addPipeline(new ConsolePipeline())
setScheduler(Scheduler) 设置Scheduler,一个Spider只能有个一个Scheduler spider.setScheduler(new RedisScheduler())
setDownloader(Downloader) 设置Downloader,一个Spider只能有个一个Downloader spider .setDownloader(new SeleniumDownloader())
get(String) 同步调用,并直接取得结果 ResultItems result = spider .get(“https://www.cnblogs.com/X-knight/”)
getAll(String…) 同步调用,并直接取得一堆结果 List<ResultItems> results = spider .getAll(“https://www.cnblogs.com/X-knight/”, “https://www.cnblogs.com/X-knight/*****”)
  • Site

对站点本身的一些配置信息,例如编码、HTTP头、超时时间、重试策略等、代理等,都可以通过设置Site对象来进行配置。

方法 说明 示例
setCharset(String) 设置编码 site.setCharset(“utf-8”)
setUserAgent(String) 设置UserAgent site.setUserAgent(“Spider”)
setTimeOut(int) 设置超时时间,单位是毫秒 site.setTimeOut(3000)
setRetryTimes(int) 设置重试次数 site.setRetryTimes(3)
setCycleRetryTimes(int) 设置循环重试次数 site.setCycleRetryTimes(3)
addCookie(String,String) 添加一条cookie site.addCookie(“dotcomt_user”,”Xknight”)
setDomain(String) 设置域名,需设置域名后,addCookie才可生效 site.setDomain(“github.com”)
addHeader(String,String) 添加一条addHeader site.addHeader(“Referer”,”https://github.com“)
setHttpProxy(HttpHost) 设置Http代理 site.setHttpProxy(new HttpHost(“127.0.0.1”,8080))

循环重试cycleRetry机制会将下载失败的url重新放入队列尾部重试,直到达到重试次数,以保证不因为某些网络原因漏抓页面。

3.5 爬虫的监控

利用爬虫的监控功能,你可以查看爬虫的执行情况——已经下载了多少页面、还有多少页面、启动了多少线程等信息。该功能通过JMX实现,你可以使用Jconsole等JMX工具查看本地或者远程的爬虫信息。

如果你完全不会JMX也没关系,因为它的使用相对简单,本章会比较详细的讲解使用方法。如果要弄明白其中原理,你可能需要一些JMX的知识,推荐阅读:JMX整理

注意: 如果你自己定义了Scheduler,那么需要用这个类实现MonitorableScheduler接口,才能查看“LeftPageCount”和“TotalPageCount”这两条信息。

  •  为项目添加监控

添加监控非常简单,获取一个SpiderMonitor的单例SpiderMonitor.instance(),并将你想要监控的Spider注册进去即可。你可以注册多个Spider到SpiderMonitor中。

public class MonitorExample {

    public static void main(String[] args) throws Exception {

        Spider oschinaSpider = Spider.create(new OschinaBlogPageProcessor())
                .addUrl("http://my.oschina.net/flashsword/blog");
        Spider githubSpider = Spider.create(new GithubRepoPageProcessor())
                .addUrl("https://github.com/Xknight");

        SpiderMonitor.instance().register(oschinaSpider);
        SpiderMonitor.instance().register(githubSpider);
        oschinaSpider.start();
        githubSpider.start();
    }
}
  • 查看监控信息

Xpider的监控使用JMX提供控制,你可以使用任何支持JMX的客户端来进行连接。我们这里以JDK自带的JConsole为例。我们首先启动Xpider的一个Spider,并添加监控代码。然后我们通过JConsole来进行查看。

启动程序,然后在命令行输入jconsole(windows下是在DOS下输入jconsole.exe)即可启动JConsole。

这里我们选择启动Xpider的本地进程,连接后选择“MBean”,点开“Xpider”,就能看到所有已经监控的Spider信息了!

这里我们也可以选择“操作”,在操作里可以选择启动-start()和终止爬虫-stop(),这会直接调用对应Spider的start()和stop()方法,来达到基本控制的目的。

3.6 配置代理

为了防止频繁访问网站而导致IP被封,爬虫无法运行的情况发生,加入了APIProxyProvider。因为相对于Site的“配置”,ProxyProvider定位更多是一个“组件”,所以代理不再从Site设置,而是由HttpClientDownloader设置。

API 说明
HttpClientDownloader.setProxyProvider(ProxyProvider proxyProvider) 设置代理

ProxyProvider有一个默认实现:SimpleProxyProvider。它是一个基于简单Round-Robin的、没有失败检查的ProxyProvider。可以配置任意个候选代理,每次会按顺序挑选一个代理使用。它适合用在自己搭建的比较稳定的代理的场景。

代理示例:

  1. 设置单一的普通HTTP代理为101.101.101.101的8888端口,并设置密码为”username”,”password”
  HttpClientDownloader httpClientDownloader = new HttpClientDownloader();
  httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(new Proxy("101.101.101.101",8888,"username","password")));
  spider.setDownloader(httpClientDownloader);
  1. 设置代理池,其中包括101.101.101.101和102.102.102.102两个IP,没有密码
HttpClientDownloader httpClientDownloader = new HttpClientDownloader();
httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(
new Proxy("101.101.101.101",8888)
,new Proxy("102.102.102.102",8888)));

3.7 处理非HTTP GET请求

一般来说,爬虫只会抓取信息展示类的页面,所以基本只会处理HTTP GET方法的数据。但是对于某些场景,模拟POST等方法也是需要的。

这里采用在Request对象上添加Method和requestBody来实现。

Request request = new Request("http://xxx/path");
request.setMethod(HttpConstant.Method.POST);
request.setRequestBody(HttpRequestBody.json("{'id':1}","utf-8"));

HttpRequestBody内置了几种初始化方式,支持最常见的表单提交、json提交等方式。

API 说明
HttpRequestBody.form(Map\<string,object> params, String encoding) 使用表单提交的方式
HttpRequestBody.json(String json, String encoding) 使用JSON的方式,json是序列化后的结果
HttpRequestBody.xml(String xml, String encoding) 设置xml的方式,xml是序列化后的结果
HttpRequestBody.custom(byte[] body, String contentType, String encoding) 设置自定义的requestBody

四、Xpider的注解功能

Xpider支持使用独有的注解风格编写一个爬虫,引入Xpider-extension包即可使用此功能。

在注解模式下,使用一个简单对象加上注解,可以用极少的代码量就完成一个爬虫的编写。对于简单的爬虫,这样写既简单又容易理解,并且管理起来也很方便。

注解模式的开发方式是这样的:

  1. 首先定义你需要抽取的数据,并编写类。
  2. 在类上写明@TargetUrl注解,定义对哪些URL进行下载和抽取。
  3. 在类的字段上加上@ExtractBy注解,定义这个字段使用什么方式进行抽取。
  4. 定义结果的存储方式。

下面我们仍然以前面中github的例子,来编写一个同样功能的爬虫,来讲解注解功能的使用。

@TargetUrl("https://github.com/\\w+/\\w+")
@HelpUrl("https://github.com/\\w+")
public class GithubRepo {

    @ExtractBy(value = "//h1[@class='entry-title public']/strong/a/text()", notNull = true)
    private String name;

    @ExtractByUrl("https://github\\.com/(\\w+)/.*")
    private String author;

    @ExtractBy("//div[@id='readme']/tidyText()")
    private String readme;

    public static void main(String[] args) {
        OOSpider.create(Site.me().setSleepTime(1000)
                , new ConsolePageModelPipeline(), GithubRepo.class)
                .addUrl("https://github.com/Xknight").thread(5).run();
    }
}

最终编写好的爬虫是这样子的,是不是更加简单?

4.1 编写Model类

同第三章的例子一样,我们这里抽取一个github项目的名称、作者和简介三个信息,所以我们定义了一个Model类。

public class GithubRepo {

    private String name;

    private String author;

    private String readme;

}

这里省略了getter和setter方法。

在抽取最后,我们会得到这个类的一个或者多个实例,这就是爬虫的结果。

4.2 TargetUrl与HelpUrl

在第二步,我们仍然要定义如何发现URL。这里我们要先引入两个概念:@TargetUrl@HelpUrl

  • TargetUrl与HelpUrl

HelpUrl/TargetUrl是一个非常有效的爬虫开发模式,TargetUrl是我们最终要抓取的URL,最终想要的数据都来自这里;而HelpUrl则是为了发现这个最终URL,我们需要访问的页面。几乎所有垂直爬虫的需求,都可以归结为对这两类URL的处理:

  • 对于博客页,HelpUrl是列表页,TargetUrl是文章页。
  • 对于论坛,HelpUrl是帖子列表,TargetUrl是帖子详情。
  • 对于电商网站,HelpUrl是分类列表,TargetUrl是商品详情。

在这个例子中,TargetUrl是最终的项目页,而HelpUrl则是项目搜索页,它会展示所有项目的链接。

有了这些知识,我们就为这个例子定义URL格式:

@TargetUrl("https://github.com/\\w+/\\w+")
@HelpUrl("https://github.com/\\w+")
public class GithubRepo {
    ……
}
TargetUrl中的自定义正则表达式

这里我们使用的是正则表达式来规定URL范围。可能细心的朋友,会知道.是正则表达式的保留字符,那么这里是不是写错了呢?其实是这里为了方便,WebMagic自己定制的适合URL的正则表达式,主要由两点改动:

  • 将URL中常用的字符.默认做了转义,变成了\.
  • 将”*”替换成了”.*”,直接使用可表示通配符。

例如,https://github.com/*在这里是一个合法的表达式,它表示https://github.com/下的所有URL。

在WebMagic中,从TargetUrl页面得到的URL,只要符合TargetUrl的格式,也是会被下载的。所以即使不指定HelpUrl也是可以的——例如某些博客页总会有“下一篇”链接,这种情况下无需指定HelpUrl。

sourceRegion

TargetUrl还支持定义sourceRegion,这个参数是一个XPath表达式,指定了这个URL从哪里得到——不在sourceRegion的URL不会被抽取。

4.3 使用ExtractBy进行抽取

@ExtractBy是一个用于抽取元素的注解,它描述了一种抽取规则。

  • 初识ExtractBy注解

@ExtractBy注解主要作用于字段,它表示“使用这个抽取规则,将抽取到的结果保存到这个字段中”。例如:

@ExtractBy("//div[@id='readme']/text()")
private String readme;

这里”//div[@id=’readme’]/text()”是一个XPath表示的抽取规则,而抽取到的结果则会保存到readme字段中。

  •  使用其他抽取方式

除了XPath,我们还可以使用其他抽取方式来进行抽取,包括CSS选择器、正则表达式和JsonPath,在注解中指明type之后即可。

@ExtractBy(value = "div.BlogContent", type = ExtractBy.Type.Css)
private String content;
  • notnull

@ExtractBy包含一个notNull属性,如果熟悉mysql的同学一定能明白它的意思:此字段不允许为空。如果为空,这条抽取到的结果会被丢弃。对于一些页面的关键性属性(例如文章的标题等),设置notnulltrue,可以有效的过滤掉无用的页面。

notNull默认为false

  • ExtractByUrl

@ExtractByUrl是一个单独的注解,它的意思是“从URL中进行抽取”。它只支持正则表达式作为抽取规则。

4.4 在类上使用ExtractBy

在之前的注解模式中,我们一个页面只对应一条结果。如果一个页面有多个抽取的记录呢?例如在“QQ美食”的列表页面http://meishi.qq.com/beijing/c/all,我想要抽取所有商户名和优惠信息,该怎么办呢?

在类上使用@ExtractBy注解可以解决这个问题。

在类上使用这个注解的意思很简单:使用这个结果抽取一个区域,让这块区域对应一个结果。

@ExtractBy(value = "//ul[@id=\"promos_list2\"]/li",multi = true)
public class QQMeishi {
    ……
}

对应的,在这个类中的字段上再使用@ExtractBy的话,则是从这个区域而不是整个页面进行抽取。如果这个时候仍想要从整个页面抽取,则可以设置source = RawHtml

@TargetUrl("http://meishi.qq.com/beijing/c/all[\\-p2]*")
@ExtractBy(value = "//ul[@id=\"promos_list2\"]/li",multi = true)
public class QQMeishi {

    @ExtractBy("//div[@class=info]/a[@class=title]/h4/text()")
    private String shopName;

    @ExtractBy("//div[@class=info]/a[@class=title]/text()")
    private String promo;

    public static void main(String[] args) {
        OOSpider.create(Site.me(), new ConsolePageModelPipeline(), QQMeishi.class).addUrl("http://meishi.qq.com/beijing/c/all").thread(4).run();
    }

}

4.5 结果的类型转换

【类型转换(Formatter机制)】因为抽取到的内容总是String,而我们想要的内容则可能是其他类型。Formatter机制可以将抽取到的内容,自动转换成一些基本类型,而无需手动使用代码进行转换。

例如:

@ExtractBy("//ul[@class='pagehead-actions']/li[1]//a[@class='social-count js-social-count']/text()")
private int star;
  • 自动转换支持的类型

自动转换支持所有基本类型和装箱类型。

基本类型 装箱类型
int Integer
long Long
double Double
float Float
short Short
char Character
byte Byte
boolean Boolean

另外,还支持java.util.Date类型的转换。但是在转换时,需要指定Date的格式。格式按照JDK的标准来定义,具体规范可以看这里:http://java.sun.com/docs/books/tutorial/i18n/format/simpleDateFormat.html

@Formatter("yyyy-MM-dd HH:mm")
@ExtractBy("//div[@class='BlogStat']/regex('\\d+-\\d+-\\d+\\s+\\d+:\\d+')")
private Date date;
  • 显式指定转换类型

一般情况下,Formatter会根据字段类型进行转换,但是特殊情况下,我们会需要手动指定类型。这主要发生在字段是List类型的时候。

@Formatter(value = "",subClazz = Integer.class)
@ExtractBy(value = "//div[@class='id']/text()", multi = true)
private List<Integer> ids;
  • 自定义Formatter(TODO)

实际上,除了自动类型转换之外,Formatter还可以做一些结果的后处理的事情。例如,我们有一种需求场景,需要将抽取的结果作为结果的一部分,拼接上一部分字符串来使用。在这里,我们定义了一个StringTemplateFormatter

public class StringTemplateFormatter implements ObjectFormatter<String> {

    private String template;

    @Override
    public String format(String raw) throws Exception {
        return String.format(template, raw);
    }

    @Override
    public Class<String> clazz() {
        return String.class;
    }

    @Override
    public void initParam(String[] extra) {
        template = extra[0];
    }
}

那么,我们就能在抽取之后,做一些简单的操作了!

@Formatter(value = "author is %s",formatter = StringTemplateFormatter.class)
@ExtractByUrl("https://github\\.com/(\\w+)/.*")
private String author;

4.6 一个完整的流程

到之前为止,我们了解了URL和抽取相关API,一个爬虫已经基本编写完成了。

@TargetUrl("https://github.com/\\w+/\\w+")
@HelpUrl("https://github.com/\\w+")
public class GithubRepo {

    @ExtractBy(value = "//h1[@class='entry-title public']/strong/a/text()", notNull = true)
    private String name;

    @ExtractByUrl("https://github\\.com/(\\w+)/.*")
    private String author;

    @ExtractBy("//div[@id='readme']/tidyText()")
    private String readme;
}
  • 爬虫的创建和启动

注解模式的入口是OOSpider,它继承了Spider类,提供了特殊的创建方法,其他的方法是类似的。创建一个注解模式的爬虫需要一个或者多个Model类,以及一个或者多个PageModelPipeline——定义处理结果的方式。

public static OOSpider create(Site site, PageModelPipeline pageModelPipeline, Class... pageModels);
  • PageModelPipeline

注解模式下,处理结果的类叫做PageModelPipeline,通过实现它,你可以自定义自己的结果处理方式。

public interface PageModelPipeline<T> {

    public void process(T t, Task task);

}

PageModelPipeline与Model类是对应的,多个Model可以对应一个PageModelPipeline。除了创建时,你还可以通过:

public OOSpider addPageModel(PageModelPipeline pageModelPipeline, Class... pageModels)

方法,在添加一个Model的同时,可以添加一个PageModelPipeline。

  • 结语

好了,现在我们来完成这个例子:

@TargetUrl("https://github.com/\\w+/\\w+")
@HelpUrl("https://github.com/\\w+")
public class GithubRepo {

    @ExtractBy(value = "//h1[@class='entry-title public']/strong/a/text()", notNull = true)
    private String name;

    @ExtractByUrl("https://github\\.com/(\\w+)/.*")
    private String author;

    @ExtractBy("//div[@id='readme']/tidyText()")
    private String readme;

    public static void main(String[] args) {
        OOSpider.create(Site.me().setSleepTime(1000)
                , new ConsolePageModelPipeline(), GithubRepo.class)
                .addUrl("https://github.com/Xknight").thread(5).run();
    }
}

4.7 AfterExtractor

有的时候,注解模式无法满足所有需求,我们可能还需要写代码完成一些事情,这个时候就要用到AfterExtractor接口了。

public interface AfterExtractor {

    public void afterProcess(Page page);
}

afterProcess方法会在抽取结束,字段都初始化完毕之后被调用,可以处理一些特殊的逻辑。例如这个例子使用Jfinal ActiveRecord持久化爬到的博客

//TargetUrl的意思是只有以下格式的URL才会被抽取出生成model对象
//这里对正则做了一点改动,'.'默认是不需要转义的,而'*'则会自动被替换成'.*',因为这样描述URL看着舒服一点...
//继承jfinal中的Model
//实现AfterExtractor接口可以在填充属性后进行其他操作
@TargetUrl("http://my.oschina.net/flashsword/blog/*")
public class OschinaBlog extends Model<OschinaBlog> implements AfterExtractor {

    //用ExtractBy注解的字段会被自动抽取并填充
    //默认是xpath语法
    @ExtractBy("//title")
    private String title;

    //可以定义抽取语法为Css、Regex等
    @ExtractBy(value = "div.BlogContent", type = ExtractBy.Type.Css)
    private String content;

    //multi标注的抽取结果可以是一个List
    @ExtractBy(value = "//div[@class='BlogTags']/a/text()", multi = true)
    private List<String> tags;

    @Override
    public void afterProcess(Page page) {
        //jfinal的属性其实是一个Map而不是字段,没关系,填充进去就是了
        this.set("title", title);
        this.set("content", content);
        this.set("tags", StringUtils.join(tags, ","));
        //保存
        save();
    }

    public static void main(String[] args) {
        C3p0Plugin c3p0Plugin = new C3p0Plugin("jdbc:mysql://127.0.0.1/blog?characterEncoding=utf-8", "blog", "password");
        c3p0Plugin.start();
        ActiveRecordPlugin activeRecordPlugin = new ActiveRecordPlugin(c3p0Plugin);
        activeRecordPlugin.addMapping("blog", OschinaBlog.class);
        activeRecordPlugin.start();
        //启动webmagic
        OOSpider.create(Site.me().addStartUrl("http://my.oschina.net/flashsword/blog/145796"), OschinaBlog.class).run();
    }
}
  • 结语

注解模式现在算是介绍结束了,注解模式其实是完全基于Xpider-core中的PageProcessorPipeline扩展实现的。

5. 组件的定制

在第一章里,我们提到了Xpider的组件是可以灵活定制的。

你可以根据自己需求实现你自己想要的功能。

在Spider类里,PageProcessorDownloaderSchedulerPipeline四个组件都是Spider的字段。除了PageProcessor是在Spider创建的时候已经指定,DownloaderSchedulerPipeline都可以通过Spider的setter方法来进行配置和更改。

方法 说明 示例
setScheduler() 设置Scheduler spipder.setScheduler(new FileCacheQueueScheduler(“D:\data\webmagic”))
setDownloader() 设置Downloader spipder.setDownloader(new SeleniumDownloader()))
addPipeline() 设置Pipeline,一个Spider可以有多个Pipeline spipder.addPipeline(new FilePipeline())

在这一章,我们会讲到如何定制这些组件,完成我们想要的功能。

5.1 使用和定制Pipeline

Pileline是抽取结束后,进行处理的部分,它主要用于抽取结果的保存,也可以定制Pileline可以实现一些通用的功能。在这一节中,我们会对Pipeline进行介绍,并用两个例子来讲解如何定制Pipeline。

  • 6.1.1 Pipeline介绍

Pipeline的接口定义如下:

public interface Pipeline {

    // ResultItems保存了抽取结果,它是一个Map结构,
    // 在page.putField(key,value)中保存的数据,可以通过ResultItems.get(key)获取
    public void process(ResultItems resultItems, Task task);

}

可以看到,Pipeline其实就是将PageProcessor抽取的结果,继续进行了处理的,其实在Pipeline中完成的功能,你基本上也可以直接在PageProcessor实现,那么为什么会有Pipeline?有几个原因:

  1. 为了模块分离。“页面抽取”和“后处理、持久化”是爬虫的两个阶段,将其分离开来,一个是代码结构比较清晰,另一个是以后也可能将其处理过程分开,分开在独立的线程以至于不同的机器执行。
  2. Pipeline的功能比较固定,更容易做成通用组件。每个页面的抽取方式千变万化,但是后续处理方式则比较固定,例如保存到文件、保存到数据库这种操作,这些对所有页面都是通用的。WebMagic中就已经提供了控制台输出、保存到文件、保存为JSON格式的文件几种通用的Pipeline。

在Xpider里,一个Spider可以有多个Pipeline,使用Spider.addPipeline()即可增加一个Pipeline。这些Pipeline都会得到处理,例如你可以使用

spider.addPipeline(new ConsolePipeline()).addPipeline(new FilePipeline())

实现输出结果到控制台,并且保存到文件的目标。

  • 将结果输出到控制台

在介绍PageProcessor时,我们使用了GithubRepoPageProcessor作为例子,其中某一段代码中,我们将结果进行了保存:

public void process(Page page) {
    page.addTargetRequests(page.getHtml().links().regex("(https://github\\.com/\\w+/\\w+)").all());
    page.addTargetRequests(page.getHtml().links().regex("(https://github\\.com/\\w+)").all());
    //保存结果author,这个结果会最终保存到ResultItems中
    page.putField("author", page.getUrl().regex("https://github\\.com/(\\w+)/.*").toString());
    page.putField("name", page.getHtml().xpath("//h1[@class='entry-title public']/strong/a/text()").toString());
    if (page.getResultItems().get("name")==null){
        //设置skip之后,这个页面的结果不会被Pipeline处理
        page.setSkip(true);
    }
    page.putField("readme", page.getHtml().xpath("//div[@id='readme']/tidyText()"));
}

现在我们想将结果保存到控制台,要怎么做呢?ConsolePipeline可以完成这个工作:

public class ConsolePipeline implements Pipeline {

    @Override
    public void process(ResultItems resultItems, Task task) {
        System.out.println("get page: " + resultItems.getRequest().getUrl());
        //遍历所有结果,输出到控制台,上面例子中的"author"、"name"、"readme"都是一个key,其结果则是对应的value
        for (Map.Entry<String, Object> entry : resultItems.getAll().entrySet()) {
            System.out.println(entry.getKey() + ":\t" + entry.getValue());
        }
    }
}

参考这个例子,你就可以定制自己的Pipeline了——从ResultItems中取出数据,再按照你希望的方式处理即可。

  •  将结果保存到MySQL

这里先介绍一个demo项目:jobhunter。它是一个集成了Spring,使用WebMagic抓取招聘信息,并且使用Mybatis持久化到Mysql的例子。我们会用这个项目来介绍如果持久化到Mysql。

在Java里,我们有很多方式将数据保存到MySQL,例如jdbc、dbutils、spring-jdbc、MyBatis等工具。这些工具都可以完成同样的事情,只不过功能和使用复杂程度不一样。如果使用jdbc,那么我们只需要从ResultItems取出数据,进行保存即可。

如果我们会使用ORM框架来完成持久化到MySQL的工作,就会面临一个问题:这些框架一般都要求保存的内容是一个定义好结构的对象,而不是一个key-value形式的ResultItems。以MyBatis为例,我们使用MyBatis-Spring可以定义这样一个DAO:

public interface JobInfoDAO {

    @Insert("insert into JobInfo (`title`,`salary`,`company`,`description`,`requirement`,`source`,`url`,`urlMd5`) values (#{title},#{salary},#{company},#{description},#{requirement},#{source},#{url},#{urlMd5})")
    public int add(LieTouJobInfo jobInfo);
}

我们要做的,就是实现一个Pipeline,将ResultItems和LieTouJobInfo对象结合起来。

注解模式

注解模式下,Xpider内置了一个PageModelPipeline

public interface PageModelPipeline<T> {

    //这里传入的是处理好的对象
    public void process(T t, Task task);

}

这时,我们可以很优雅的定义一个JobInfoDaoPipeline,来实现这个功能:

@Component("JobInfoDaoPipeline")
public class JobInfoDaoPipeline implements PageModelPipeline<LieTouJobInfo> {

    @Resource
    private JobInfoDAO jobInfoDAO;

    @Override
    public void process(LieTouJobInfo lieTouJobInfo, Task task) {
        //调用MyBatis DAO保存结果
        jobInfoDAO.add(lieTouJobInfo);
    }
}

基本Pipeline模式

至此,结果保存就已经完成了!那么如果我们使用原始的Pipeline接口,要怎么完成呢?其实答案也很简单,如果你要保存一个对象,那么就需要在抽取的时候,将它保存为一个对象:

public void process(Page page) {
    page.addTargetRequests(page.getHtml().links().regex("(https://github\\.com/\\w+/\\w+)").all());
    page.addTargetRequests(page.getHtml().links().regex("(https://github\\.com/\\w+)").all());
    GithubRepo githubRepo = new GithubRepo();
    githubRepo.setAuthor(page.getUrl().regex("https://github\\.com/(\\w+)/.*").toString());
    githubRepo.setName(page.getHtml().xpath("//h1[@class='entry-title public']/strong/a/text()").toString());
    githubRepo.setReadme(page.getHtml().xpath("//div[@id='readme']/tidyText()").toString());
    if (githubRepo.getName() == null) {
        //skip this page
        page.setSkip(true);
    } else {
        page.putField("repo", githubRepo);
    }
}

在Pipeline中,只要使用

GithubRepo githubRepo = (GithubRepo)resultItems.get("repo");

就可以获取这个对象了。

PageModelPipeline实际上也是通过原始的Pipeline来实现的,它将与PageProcessor进行了整合,在保存时,使用类名作为key,而对象则是value,具体实现见:ModelPipeline

  • Xpider已经提供的几个Pipeline

Xpider中已经提供了将结果输出到控制台、保存到文件和JSON格式保存的几个Pipeline:

说明 备注
ConsolePipeline 输出结果到控制台 抽取结果需要实现toString方法
FilePipeline 保存结果到文件 抽取结果需要实现toString方法
JsonFilePipeline JSON格式保存结果到文件  
ConsolePageModelPipeline (注解模式)输出结果到控制台  
FilePageModelPipeline (注解模式)保存结果到文件  
JsonFilePageModelPipeline (注解模式)JSON格式保存结果到文件 想要持久化的字段需要有getter方法

5.2 使用和定制Scheduler

Scheduler是Xpider中进行URL管理的组件。一般来说,Scheduler包括两个作用:

  1. 对待抓取的URL队列进行管理。
  2. 对已抓取的URL进行去重。

Xpider内置了几个常用的Scheduler。如果你只是在本地执行规模比较小的爬虫,那么基本无需定制Scheduler,但是了解一下已经提供的几个Scheduler还是有意义的。

说明 备注
DuplicateRemovedScheduler 抽象基类,提供一些模板方法 继承它可以实现自己的功能
QueueScheduler 使用内存队列保存待抓取URL  
PriorityScheduler 使用带有优先级的内存队列保存待抓取URL 耗费内存较QueueScheduler更大,但是当设置了request.priority之后,只能使用PriorityScheduler才可使优先级生效
FileCacheQueueScheduler 使用文件保存抓取URL,可以在关闭程序并下次启动时,从之前抓取到的URL继续抓取 需指定路径,会建立.urls.txt和.cursor.txt两个文件
RedisScheduler 使用Redis保存抓取队列,可进行多台机器同时合作抓取 需要安装并启动redis

在最新版本里,对Scheduler的内部实现进行了重构,去重部分被单独抽象成了一个接口:DuplicateRemover,从而可以为同一个Scheduler选择不同的去重方式,以适应不同的需要,目前提供了两种去重方式。

说明
HashSetDuplicateRemover 使用HashSet来进行去重,占用内存较大
BloomFilterDuplicateRemover 使用BloomFilter来进行去重,占用内存较小,但是可能漏抓页面

所有默认的Scheduler都使用HashSetDuplicateRemover来进行去重,(除开RedisScheduler是使用Redis的set进行去重)。如果你的URL较多,使用HashSetDuplicateRemover会比较占用内存,所以也可以尝试以下BloomFilterDuplicateRemover1,使用方式:

spider.setScheduler(new QueueScheduler()
.setDuplicateRemover(new BloomFilterDuplicateRemover(10000000)) //10000000是估计的页面数量
)

5.3 使用和定制Downloader

Xpider的默认Downloader基于HttpClient。一般来说,你无须自己实现Downloader,不过HttpClientDownloader也预留了几个扩展点,以满足不同场景的需求。

另外,你可能希望通过其他方式来实现页面下载,例如使用SeleniumDownloader来渲染动态页面。

六、抓取前端渲染的页面

随着AJAX技术不断的普及,以及现在AngularJS这种Single-page application框架的出现,现在js渲染出的页面越来越多。对于爬虫来说,这种页面是比较讨厌的:仅仅提取HTML内容,往往无法拿到有效的信息。那么如何处理这种页面呢?总的来说有两种做法:

  1. 在抓取阶段,在爬虫中内置一个浏览器内核,执行js渲染页面后,再抓取。这方面对应的工具有SeleniumHtmlUnit或者PhantomJs。但是这些工具都存在一定的效率问题,同时也不是那么稳定。好处是编写规则同静态页面一样。
  2. 因为js渲染页面的数据也是从后端拿到,而且基本上都是AJAX获取,所以分析AJAX请求,找到对应数据的请求,也是比较可行的做法。而且相对于页面样式,这种接口变化可能性更小。缺点就是找到这个请求,并进行模拟,是一个相对困难的过程,也需要相对多的分析经验。

对比两种方式,我的观点是,对于一次性或者小规模的需求,用第一种方式省时省力。但是对于长期性的、大规模的需求,还是第二种会更靠谱一些。对于一些站点,甚至还有一些js混淆的技术,这个时候,第一种的方式基本是万能的,而第二种就会很复杂了。

对于第一种方法,Xpider-selenium就是这样的一个尝试,它定义了一个Downloader,在下载页面时,就是用浏览器内核进行渲染。selenium的配置比较复杂,而且跟平台和版本有关,没有太稳定的方案。感兴趣的可以看我这篇博客:使用Selenium来抓取动态加载的页面

这里我主要介绍第二种方法,希望到最后你会发现:原来解析一个前端渲染的页面,也没有那么复杂。这里我们以AngularJS中文社区http://angularjs.cn/为例。

1 如何判断前端渲染

判断页面是否为js渲染的方式比较简单,在浏览器中直接查看源码(Windows下Ctrl+U,Mac下command+alt+u),如果找不到有效的信息,则基本可以肯定为js渲染。

这个例子中,在页面中的标题“有孚计算机网络-前端攻城师”在源码中无法找到,则可以断定是js渲染,并且这个数据是AJAX得到。

2 分析请求

下面我们进入最难的一部分:找到这个数据请求。这一步能帮助我们的工具,主要是浏览器中查看网络请求的开发者工具。

以Chome为例,我们打开“开发者工具”(Windows下是F12,Mac下是command+alt+i),然后重新刷新页面(也有可能是下拉页面,总之是所有你认为可能触发新数据的操作),然后记得保留现场,把请求一个个拿来分析吧!

这一步需要一点耐心,但是也并不是无章可循。首先能帮助我们的是上方的分类筛选(All、Document等选项)。如果是正常的AJAX,在XHR标签下会显示,而JSONP请求会在Scripts标签下,这是两个比较常见的数据类型。

然后你可以根据数据大小来判断一下,一般结果体积较大的更有可能是返回数据的接口。剩下的,基本靠经验了,例如这里这个”latest?p=1&s=20″一看就很可疑…

对于可疑的地址,这时候可以看一下响应体是什么内容了。这里在开发者工具看不清楚,我们把URLhttp://angularjs.cn/api/article/latest?p=1&s=20复制到地址栏,重新请求一次(如果用Chrome推荐装个jsonviewer,查看AJAX结果很方便)。查看结果,看来我们找到了想要的。

同样的办法,我们进入到帖子详情页,找到了具体内容的请求:http://angularjs.cn/api/article/A0y2

3 编写程序

回想一下之前列表+目标页的例子,会发现我们这次的需求,跟之前是类似的,只不过换成了AJAX方式-AJAX方式的列表,AJAX方式的数据,而返回数据变成了JSON。那么,我们仍然可以用上次的方式,分为两种页面来进行编写:

  1. 数据列表

    在这个列表页,我们需要找到有效的信息,来帮助我们构建目标AJAX的URL。这里我们看到,这个_id应该就是我们想要的帖子的id,而帖子的详情请求,就是由一些固定URL加上这个id组成。所以在这一步,我们自己手动构造URL,并加入到待抓取队列中。这里我们使用JsonPath这种选择语言来选择数据(webmagic-extension包中提供了JsonPathSelector来支持它)。

 if (page.getUrl().regex(LIST_URL).match()) {
     //这里我们使用JSONPATH这种选择语言来选择数据
     List<String> ids = new JsonPathSelector("$.data[*]._id").selectList(page.getRawText());
     if (CollectionUtils.isNotEmpty(ids)) {
         for (String id : ids) {
             page.addTargetRequest("http://angularjs.cn/api/article/"+id);
         }
     }
 }

目标数据

有了URL,实际上解析目标数据就非常简单了,因为JSON数据是完全结构化的,所以省去了我们分析页面,编写XPath的过程。这里我们依然使用JsonPath来获取标题和内容。

 page.putField("title", new JsonPathSelector("$.data.title").select(page.getRawText()));
 page.putField("content", new JsonPathSelector("$.data.content").select(page.getRawText()));

这个例子完整的代码请看AngularJSProcessor.java

4 总结

在这个例子中,我们分析了一个比较经典的动态页面的抓取过程。实际上,动态页面抓取,最大的区别在于:它提高了链接发现的难度。我们对比一下两种开发模式:

  1. 后端渲染的页面

    下载辅助页面=>发现链接=>下载并分析目标HTML

  2. 前端渲染的页面

    发现辅助数据=>构造链接=>下载并分析目标AJAX

对于不同的站点,这个辅助数据可能是在页面HTML中已经预先输出,也可能是通过AJAX去请求,甚至可能是多次数据请求的过程,但是这个模式基本是固定的。

但是这些数据请求的分析比起页面分析来说,仍然是要复杂得多,所以这其实是动态页面抓取的难点。

本节这个例子希望做到的是,在分析出请求后,为这类爬虫的编写提供一个可遵循的模式,即发现辅助数据=>构造链接=>下载并分析目标AJAX这个模式。

PS:

Xpider之后会将Json的支持增加到链式API中,以后你可以使用:

page.getJson().jsonPath("$.name").get();

这样的方式来解析AJAX请求了。

同时也支持

page.getJson().removePadding("callback").jsonPath("$.name").get();

这样的方式来解析JSONP请求。

七、对目标网站的更新的实时监控[未封装]

该部分内容,是之前用Python实现的,还未封装成模块,该更新。可参考https://www.cnblogs.com/X-knight/protected/p/11369294.html

 

作者:X-knight|勋爵