如今大多数的浏览器(Edge、Chrome、Firefox 等)都带有开发者工具。本篇文章简单介绍利用开发者工具辅助我们选择需要爬取的内容,方便 Scrapy 爬虫的开发。

如果你对 Scrapy 还不熟悉,可以先看这一篇文章:Scrapy-入门篇

查阅实时浏览器 DOM 的注意事项

由于开发者工具工作于实时浏览器 DOM,因此你在工具中看到的页面代码并非原始 HTML 代码,它可能被浏览器清除过一些内容或者执行了某些 Javascript 脚本。就比如在 Firefox 浏览器,它会自动为表格增加 <tbody> 标签。Scrapy 不会修改原 HTML,因此如果你的爬虫 XPath 表达式含有 <tbody>,你将不会提取到任何信息。
正因此,使用 Scrapy 时你需要注意:

  1. 当你在为 Scrapy 寻找 XPath、查阅 DOM 时,请取消激活 JavaScript。例如在 Edge 浏览器中设置方法为打开开发者工具设置(左下角三个圆点),首选项>调试程序>禁用 JavaScript。
  2. 不要使用完整的 XPath 表达式。使用相对路径,或者基于属性(比如 idclasswidth 等)进行筛选,或者其它可以助于定位的特性如 contains(@href, 'image')
  3. 不要在 XPath 表达式中使用 <tbody> 元素,除非你知道自己正在做什么。

检查元素

以英文名人名言网站 quotes.toscrape.com 为例。假如我们需要提取其中的名言,我们可以直接右击名言,选择弹出菜单中的「检查」即可打开开发者工具。此时开发者工具直接打开「元素」页面,对应的名言将高亮显示。

打开开发者工具的快捷键为F12

image.png

鼠标在「元素」中划过 HTML 块时,左侧对应的元素将高亮显示。按下「检查」按钮(快捷键 Ctrl+Shift+C,可以随时呼出开发者工具面板),可以实现鼠标划过网页时高亮显示 HTML 块,单击即可固定。此外,右键某一个 HTML 块,选择「滚动到视图」也可以轻松定位元素在网页中的位置。

我们可以手动展开或折叠 HTML 代码,右键我们想要爬取的内容,选择复制 XPath。

image.png

这样你就可以轻松定位需要爬取元素的位置。

我们可以现在打开 Scrapy shell 验证一下:

1
2
3
4
scrapy shell "https://quotes.toscrape.com/"

In [1]: response.xpath("/html/body/div/div[2]/div[1]/div[1]/span[1]/text()").getall()
Out[1]: ['“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”']

但是这个完整 XPath 表达显得不太灵活。通过开发者工具我们观察到网站中一句名言的结构是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="quote" itemscope="" itemtype="http://schema.org/CreativeWork">
<span class="text" itemprop="text">“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”</span>
<span>by <small class="author" itemprop="author">Albert Einstein</small>
<a href="/author/Albert-Einstein">(about)</a>
</span>
<div class="tags">
Tags:
<meta class="keywords" itemprop="keywords" content="change,deep-thoughts,thinking,world">
<a class="tag" href="/tag/change/page/1/">change</a>
<a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
<a class="tag" href="/tag/thinking/page/1/">thinking</a>
<a class="tag" href="/tag/world/page/1/">world</a>
</div>
</div>

一个页面就有 10 个这样的结构(10 句名言),每个结构里面都包含:两个 <span>,一个 <div>

利用一些 XPath 知识我们可以定位名言位置。在 Scrapy shell 中输入:

1
2
3
4
5
In [2]: response.xpath('//span[has-class("text")]/text()').getall()
Out[2]:
['“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”',
'“It is our choices, Harry, that show what we truly are, far more than our abilities.”',
'“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”'......

网络工具

有时候我们爬取的网页是动态网页,这些网页的许多部分是通过多次网络请求实现的。考虑这个例子: quotes.toscrape.com/scroll。与之前的网站不一样的是,这个网站的页面可以无限滚动,不需要手动点击翻页按钮。如果我们尝试在 Scrapy shell 中打开这个网页,我们会发现网站除了一些基本的框架以外啥也没有。

1
2
3
4
scrapy shell "quotes.toscrape.com/scroll"

In [1]: view(response)
Out[1]: True

image.png

我们重新在浏览器输入网址 quotes.toscrape.com/scroll 。按F12打开开发者工具,点击「网络」选项卡。刷新页面,我们将看到在这个过程中的几条网络请求。

image.png

点击名称为 scroll 的请求,点击弹出的「预览」标签,我们可以看到这个请求渲染出的 HTML 画面(是不是和之前用 view(response) 我们看到的一样?),点击「响应」标签我们可以看到该请求得到的响应内容。

观察其它的请求,发现有一些 css 和 js 类型的文件,但我们真正感兴趣的是名称为 quotes?page=1 的类型为 xhr 的文件(某些浏览器为 json)。点击这个请求,通过「标头」标签中可以知道请求的 URL 为: https://quotes.toscrape.com/api/quotes?page=1 。我们使用浏览器再新建标签页访问这个链接可以直接查看这个 json 文件的内容,这样做也便于查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"has_next": true,
"page": 1,
"quotes": [
{
"author": {
"goodreads_link": "/author/show/9810.Albert_Einstein",
"name": "Albert Einstein",
"slug": "Albert-Einstein"
},
"tags": [
"change",
"deep-thoughts",
"thinking",
"world"
],
......

观察得知:

  • URL 参数中 page=1 应该表示页面标号
  • has_next 属性应该是暗示了当前页面还有没有下一页,有则为 true。(你可以看看 https://quotes.toscrape.com/api/quotes?page=10
  • quotes 是一个包含当前页面名言的列表。

据此,我们可以编写出这样的爬虫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import scrapy
import json

class QuoteSpider(scrapy.Spider):
name = "quote_scoll"
allowed_domains = ["quotes.toscrape.com"]
page = 1
start_urls = ["https://quotes.toscrape.com/api/quotes?page=1"] # 从第一页开始

def parse(self, response):
data = json.loads(response.text)
for quote in data["quotes"]:
yield {"quote": quote["text"]}
if data["has_next"]: # 检查has_next字段值
self.page += 1
url = f"https://quotes.toscrape.com/api/quotes?page={self.page}"
yield scrapy.Request(url=url, callback=self.parse)

执行:

1
scrapy crawl quote_scoll -O quote_scoll.jsonlines

【进阶小知识】使用 cURL

在更复杂的情况中,我们可能需要增加 headercookies 才能使爬虫继续生成新的 URL。我们可以利用开发者工具中将请求导出为 cURL 的格式:在「网络」标签页中右键请求,选择「复制为 cURL」。

image.png

然后利用这个网站,将 cURL 翻译为 Scrapy 中的 Requestcurl2scrapy - cURL command -> Scrapy request translator. (michael-shub.github.io)

经过一些细枝末节的调整,我们的爬虫可以是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import scrapy
import json

from scrapy import Request

##### 翻译后的 cURL 内容 #######

url = 'https://quotes.toscrape.com/api/quotes?page=1'

headers = {
"authority": "quotes.toscrape.com",
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"cache-control": "no-cache",
"pragma": "no-cache",
"referer": "https://quotes.toscrape.com/scroll",
"sec-ch-ua": "\"Not A(Brand\";v=\"99\", \"Microsoft Edge\";v=\"121\", \"Chromium\";v=\"121\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0",
"x-requested-with": "XMLHttpRequest"
}

request = Request(
url=url,
method='GET',
dont_filter=True,
headers=headers,
)

######## END ########

class QuoteSpider(scrapy.Spider):
name = "quote_scoll"
allowed_domains = ["quotes.toscrape.com"]
page = 1

# 还记得start_requests函数吗?
def start_requests(self):
return [request]

def parse(self, response):
data = json.loads(response.text)
for quote in data["quotes"]:
yield {"quote": quote["text"]}
if data["has_next"]:
self.page += 1
url = f"https://quotes.toscrape.com/api/quotes?page={self.page}"
yield scrapy.Request(url=url, callback=self.parse)

本文参考