使用JSON API和AJAX页面的爬虫

有时,你会发现自己在页面寻找的数据无法从 HTML 页面中找到。比如,当访问 http://localhost:9312/static/ 时(见图5.3),在页面任意位置右键单击 inspect element(1, 2),可以看到其中包含所有常见 HTML 元素的 DOM 树。但是,当你使用 scrapy shell 请求,或是在 Chrome 浏览器中右键单击 View Page Source(3, 4)时,则会发现该页面的 HTML 代码中并不包含关于房产的任何信息。那么,这些数据是从哪里来的呢?

image 2024 01 26 18 28 43 482
Figure 1. 图5.3 动态加载JSON对象时的页面请求与响应

与平常一样,遇到这类例子时,下一步操作应当是打开 Chrome 浏览器开发者工具的 Network 选项卡,来看看发生了什么。在左侧的列表中,可以看到加载本页面时 Chrome 执行的请求。在这个简单的页面中,只有 3 个请求:static/ 是刚才已经检查过的请求;jquery.min.js 用于获取一个流行的 Javascript 框架的代码;而 api.json 看起来会让我们产生兴趣。当单击该请求(6),并单击右侧的 Preview 选项卡(7)时,就会发现这里面包含了我们正在寻找的数据。实际上, http://localhost:9312/properties/api.json 包含了房产的ID和名称(8),如下所示。

[{
"id": 0,
"title": "better set unique family well"
},
... {
"id": 29,
"title": "better portered mile"
}]

这是一个非常简单的 JSON API 的示例。更复杂的 API 可能需要你登录,使用 POST 请求,或返回更有趣的数据结构。无论在哪种情况下,JSON 都是最简单的解析格式之一,因为你不需要编写任何 XPath 表达式就可以从中抽取出数据。

Python 提供了一个非常好的 JSON 解析库。当我们执行 import json 时,就可以使用 json.loads(response.body) 解析 JSON,将其转换为由 Python 原语、列表和字典组成的等效 Python 对象。

我们将第 3 章的 manual.py 拷贝过来,用于实现该功能。在本例中,这是最佳的起始选项,因为我们需要通过在 JSON 对象中找到的 ID,手动创建房产 URL 以及 Request 对象。我们将该文件重命名为 api.py,并将爬虫类重命名为 ApiSpider,name 属性修改为 api。新的 start_urls 将会是 JSON API 的 URL,如下所示。

start_urls = (
    'http://web:9312/properties/api.json',
)

如果你想执行 POST 请求,或是更复杂的操作,可以使用前一节中介绍的 start_requests() 方法。此时,Scrapy 将会打开该 URL,并调用包含以 Response 为参数的 parse() 方法。可以通过 import json,使用如下代码解析 JSON 对象。

def parse(self, response):
    base_url = "http://web:9312/properties/"
    js = json.loads(response.body)
    for item in js:
        id = item["id"]
        url = base_url + "property_%06d.html" % id
        yield Request(url, callback=self.parse_item)

前面的代码使用了 json.loads(response.body),将 Response 这个 JSON 对象解析为 Python 列表,然后迭代该列表。对于列表中的每一项,我们将 URL 的 3 个部分(base_url、property_%06d 以及 .html)组合到一起。base_url 是在前面定义的 URL 前缀。%06d 是 Python 语法中非常有用的一部分,它可以让我们结合 Python 变量创建新的字符串。在本例中,%06d 将会被变量 id 的值替换(本行结尾处 % 后面的变量)。id 将会被视为数字(%d 表示视为数字),并且如果不满 6 位,则会在前面加上 0,扩展成 6 位字符。比如,id 值为 5,%06d 将会被替换为 000005,而如果 id 为 34322,%06d 则会被替换为 034322。最终结果正是我们房产页面的有效 URL。我们使用该 URL 形成一个新的 Request 对象,并像第 3 章一样使用 yield。然后可以像平时那样使用 scrapy crawl 运行该示例。

$ scrapy crawl api
INFO: Scrapy 1.0.3 started (bot: properties)
...
DEBUG: Crawled (200) <GET ...properties/api.json>
DEBUG: Crawled (200) <GET .../property_000029.html>
...
INFO: Closing spider (finished)
INFO: Dumping Scrapy stats:
...
'downloader/request_count': 31, ...
'item_scraped_count': 30,

你可能会注意到结尾处的状态是 31 个请求——每个 Item 一个请求,以及最初的 api.json 的请求。

在响应间传参

很多情况下,在 JSON API 中会有感兴趣的信息,你可能想要将它们存储到 Item 中。在我们的示例中,为了演示这种情况,JSON API 会在给定房产信息的标题前面加上 "better"。比如,房产标题是 "Covent Garden",API 就会将标题写为 "Better Covent Garden"。假设我们想要将这些 "better" 开头的标题存储到 Items 中,要如何将信息从 parse() 方法传递到 parse_item() 方法呢?

不要感到惊讶,通过在 parse() 生成的 Request 中设置一些东西,就能实现该功能。之后,可以从 parse_item() 接收到的 Response 中取得这些信息。Request 有一个名为 meta 的字典,能够直接访问 Response。比如在我们的例子中,可以在该字典中设置标题值,以存储来自 JSON 对象的标题。

title = item["title"]
yield Request(url, meta={"title": title},callback=self.parse_item)

在 parse_item() 内部,可以使用该值替代之前使用过的 XPath 表达式。

l.add_value('title', response.meta['title'], MapCompose(unicode.strip, unicode.title))

你会发现我们不再调用 add_xpath(),而是转为调用 add_value(),这是因为我们在该字段中将不会再使用到任何 XPath 表达式。现在,可以使用 scrapy crawl 运行这个新的爬虫,并且可以在 PropertyItems 中看到来自 api.json 的标题。