上文提到, parse() 方法是 scrapy.Request 对象构造方法的默认回调方法。
然而,可以使用以下的代码做上文的代码相同的事情:
from pathlib import Path
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
"https://quotes.toscrape.com/page/1/",
"https://quotes.toscrape.com/page/2/",
]
def parse(self, response):
page = response.url.split("/")[-2]
filename = f"quotes-{page}.html"
Path(filename).write_bytes(response.body)
运行蜘蛛
在项目根目录(/codes/code/python/scrapy/tutorial/)运行爬虫。
运行命令格式如下:
scrapy crawl [options] <spider>
其中:
options:表示运行爬虫时的一些可选项,比如设置 Items 的保存输出文件等。
具体的所有可选项通过命令 scrapy crawl --help 查看。
spider:表示本文前面所定义的 name ,也就是项目中唯一的名称。
在项目根目录中执行以下命令来运行爬虫 quotes:
scrapy crawl quotes
爬虫成功运行的话,会输出类似于下图的情况:
执行爬虫运行命令后,项目根目录会生成两个 .html 文件。依照代码可以看出,这两个文件正好是 start_requests() 方法中的变量 urls 定义的两个 URL 的 HTTP 响应内容,文件装着的是 HTML 代码。
2. 蜘蛛的基本运行逻辑
从 start() 方法(通过 scrapy.Request 对象)发出 HTTP 请求,到通过 scrapy.Response 对象实例来接收响应,再调用 parse() 方法来处理响应内容。
响应正文往往是结构化数据,如果响应体的内容是 HTML 代码,可根据需求利用蜘蛛类的 parse() 方法的参数同时也是对象 scrapy.Response 的实例 response 变量的元素选择器来提取信息。
总结起来就是,Scrapy的工作流程遵循经典的请求-响应模式:
通过scrapy.Request发起HTTP请求
接收scrapy.Response响应对象
在parse()方法中处理响应
关于 scrapy.Request 跟 scrapy.Response 两个对象的更多解释,可参考文档:
https://docs.scrapy.net.cn/en/latest/topics/request-response.html
3. 交互式调试:Scrapy Shell 的入门与进阶
官方文档建议在Scrapy Shell 环境下学习如果提取数据,并且是最佳学习途径。
更多资料,请移步:https://docs.scrapy.net.cn/en/latest/topics/shell.html 。
如何解析结构化数据:https://docs.scrapy.net.cn/en/latest/intro/tutorial.html#extracting-data 。
基本概念
Scrapy Shell 是用于抓取给定的 URL 或文件中结构化数据的交互式控制台,用于快速测试数据提取规则。
在控制台下可以尝试诸多的 Scrapy API 快捷方式和各种对象的操作和行为来测试从而掌握这些能够在蜘蛛中用的方法和对象。
官方文档建议在 Scrapy shell(下称“交互式控制台”) 环境下学习数据提取方法,并且是最佳学习途径。
response 变量是何物
Scrapy 官方文档的教程(Tutorial)中,将介绍使用对象实例 response 提取 HTML 文本中的结构化数据。这里的 response 变量与蜘蛛代码中的 parse() 方法的同名参数变量是同一个变量,或者说都是 scrapy.Response 这个对象的实例。也就是说它们拥有的类属性和类方法是一致的。
文档中,介绍了如何使用 response 变量获取 HTML 文档中的结构化数据。
交互式控制台的用法
一、创建一个 Shell 会话
用法如下:
scrapy shell [url|file]
url 指的是远程文档的网址;
file 指的是电脑本地的文件的全路径。
也就是说,既可从远程文档中获取结构化数据,也可从本地的特定文件的内容中获取数据。
更多的选项请参阅 HELP 文档: scrapy shell --help。
官方文档的教程中,以远程 URL https://quotes.toscrape.com/page/1/ 为例,进入交互式控制台:
scrapy shell "https://quotes.toscrape.com/page/1/"
执行上面命令将会出现类似下图的界面:
二、基本使用
进入控制台即可使用之,可在 >>> 提示符后面输入想要运行的对象、属性、函数和方法等 Scrapy API。
比如:
执行 request 能够获取当前基本请求信息:
执行 response 能够获取 HTTP 响应的基本信息:
执行 response.status 获取当前响应的状态码,如 200、404 等:
200 表示当前 HTTP 请求并接收响应成功,而 response.body 则打印出响应体的文本。
response.attributes 和 request.attributes 可分别获取所有它们拥有的类属性:
Request 和 Response 对象的更多资料,请移步:https://docs.scrapy.org/en/latest/topics/request-response.html 。
三、退出当前会话
执行 quit() 函数即可退出控制台。
Scrapy 数据提取核心技术:CSS 与 XPath 选择器
Scrapy 数据提取工作原理其实不难理解,就是按用户的需求应用爬虫内置的 API 或者 Python 正则表达式把一项项数据解析出来。
比如:获取 HTML 文档中的某项内容,可以使用 Scrapy 提供的 CSS 或 XPath 选择器,两者都是爬虫系统内置的 API,也可以使用正则表达式,来获取想要的内容。
甚至可以用外部的库来解析内容,比如 BeautifulSoup。
本章节覆盖 CSS 选择器使用技巧,XPath 表达式应用案例,以及 Scrapy Shell 数据提取的调试方法详解。
什么是 CSS 和 XPath
系统对 Scrapy 数据提取内置着两套方案,一是 CSS 选择器,另一个是 XPath 表达式,它们都是 Response 对象的方法,同时也是 scrapy.Selector 对象的方法。
接下来的文章内容开始大篇幅讲解如何使用上述的两套方案:
比如:
response.xpath(query):它是 TextResponse.selector.xpath(query) 的快捷方式,利用 XPath 表达式来选择元素;
response.css(query):它是 TextResponse.selector.css(query) 的快捷方式。
它们都有各自的语法,并且才能方便地提取到结构化数据。
query 指查询的语句,语句要符合两者分别的语法。
其中 response.css() 的提取语法类似于著名且广泛使用的 JavaScript 类库 jQuery 的选择器语法。
具体的详细介绍可参考: https://www.w3.org/TR/selectors/ 。
以及官方文档: https://docs.scrapy.org/en/latest/topics/selectors.html 。
这里不多说,就举几个应用的例子。
使用 CSS 选择器提取数据
获取网页的标题选择器对象
在 HTML 元素中,有个 title 元素,它里面装的是网页的标题,也正是当前要获取的内容所在元素。
这里在控制台环境下,要获取网页 https://quotes.toscrape.com/page/1/ 的页面标题:
response.css("title")
上面的代码将获取的是关于 HTML title 元素类似于列表的选择器对象(SelectorList),想要提取出元素的内容,需要执行以下的代码:
获取单个元素
response.css("title::text").get()
::text 表示 HTML 元素的内容(不包括 HTML 标签自身),如果就这样执行,也是会获取到一个选择器对象;
.get() 则表示获取元素字符串内容,返回值不是个 Python 对象。
此方法仅仅获取一次内容,想要获取多个文本内容组成的列表,则需要使用 .getall()。
上面的代码执行结果:
获取所有匹配元素
当然,正常情况下,一个 HTML 文档中仅有一个 title 元素,本章节就举个例子,把它当作有很多。
使用 .getall() 方法,若是只有一个 HTML 的元素,那么将仅仅获取该元素的内容,并且执行结果也不是个字符串,而是由一个字符串组成的列表。
response.css("title::text").getall()
执行结果以下图:
此时要获取内容,需要在上面代码的后面加上列表的索引,因为是第一个,所以索引是 0:
response.css("title::text").getall()[0]
执行结果:
应用正则表达式来获取内容
获取 HTML 元素内容时,除了使用 get() 和 getall() 方法,还可以用更加灵活的正则表达式来提取想要的内容字符串。
response.css() 方法返回的是一个类列表的选择器列表对象,此对象有个方法 re(regex) 能用正则表达式来获取匹配的内容。
re() 方法采用的是 Python 标准的正则表达式,有兴趣了解更多可访问: https://docs.python.org/zh-cn/3/library/re.html 。
从上文可知,当前网页中的标题内容为:
Quotes to Scrape
从元素内容中获取字符个数为 6 的单词:
response.css("title::text").re(r"\w{6}")
执行结果:
['Quotes', 'Scrape']
获取标题内容前后两个单词,即不要 “to” 以及其前后的空格:
response.css("title::text").re(r"(\w+) to (\w+)")
执行结果:
['Quotes', 'Scrape']
获取标题的全部内容:
response.css("title::text").re(r".+")
执行结果:
['Quotes to Scrape']
由上述的执行结果可以得出结论:re() 方法得到一个由匹配字串组成的列表,是 Python 的普通列表,可用下标索引来访问里面的字串。
XPath 提取数据
据文档描述:XPath 表达式非常强大,它是 Scrapy 的基础选择器,就连本文前面但要的 CSS 选择器也是基于它。事实上,CSS 选择器在底层被转换为 XPath。
XPath 能选择的范围远不是 CSS 选择器能比的,它还可以通过测试是否包含一段字符串而选择其所在的元素,使用方式: response.xpath("//element[contains(选择器,'包含的文本字符串')]");比如,可以选择元素内容包含“下一页”的锚链接,此时要用 response.xpath("//a[contains(.//text(),'Next')]") 来选择,若是匹配,则返回的是包含该锚链接的选择器对象列表,而这个锚链接全貌可能是 <a href="https://domain.com/page/2">下一页</a>。
用 .xpath(query) 选择 title 元素:
response.xpath("//title")
执行结果:
用 .xpath(query) 得到 title 元素的内容:
response.path("//title/text()").get()
执行结果:
使用 XPath 选择“下一页”的锚链接:
response.xpath("//a[contains(.//text(),'Next')]")
执行结果:
使用 XPath 获取“下一页”的锚链接中的 URL:
response.xpath("//a[contains(.//text(),'Next')]/@href").get()
执行结果:
更多关于 XPath 的资料请参阅 W3C 的标准:XML Path Language (XPath) 3.1 。
在浏览器中打开当前获取的网页
上文的对于用 response.css() 用法的演示都是使用 HTML 标题元素,然而,网页上通常只有一个 title 元素。
而爬虫要抓取的数据有时是复杂的,至少比取得标题要复杂得多,可能一个查询规则会返回多个元素或/和内容,这时就要深入地研究网页的构成。
一般情况下,可以用浏览器通过网址访问该远程网页,然后查看网页源代码,再从源代码找到想要的元素和内容,最后才用 .css() 来选择全需求的内容。
如果读者不想访问远程的网址,可以在 Scrapy shell 通过执行 view(response) 在浏览器打开当前的网页,这访问的是本地的副本,不是远程的。
浏览器显示的网页:
当然,网页上的 CSS/JS 等资源还是要从远程获取的,但是这无关紧要,要的仅是网页的代码。
在 Scrapy Shell 中调试提取数据的方法
现在你已经了解了一些有关选择和提取的知识,接下来完成通过编写蜘蛛的代码从网页中提取名言和作者字符串。
在网站 https://quotes.toscrape.com/ 的 HTML 源代码中,包含名言和作者的 HTML 元素结构如下所示:
<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>
网页上有多个像这样的结构,所以只要匹配,会一次获取多个不同的名言和作者。
启动交互控制台测试一下提取数据的规则
进入 Shell:scrapy shell 'https://quotes.toscrape.com'。
刚进入的界面如下图所示:
一、从 HTML 结构中获取多个不同元素及其元素值
我们要从网页的 HTML 代码中提取出名言、作者和标签,其中,从分析 HTML 代码可知,单个独立的 div class="quote" 中有一项名言、一项作者以及若干项标签。而一个页面中有若干个 div class="quote"。
获取 div.quote,并把选择器列表赋值给变量,以备以后使用:
# 获取所有的名言 HTML 结构
quotes = response.css("div.quote")
# 获取第一个名言 HTML 结构
quote = response.css("div.quote")[0]
quotes
quote
执行结果:
获取“名言”:“名言”所在的结构是 div.quote > span.text,因此:
quote.css("span.text::text").get()
执行结果:
获取“作者”:“作者”所在的结构是 div.quote > span > small.author:
quote.css("small.author::text").get()
执行结果:
获取“标签”:“标签”所在结果是 div.quote > div.tags > a.tag:
quote.css("div.tags>a.tag::text").getall()
执行结果:
二、迭代所有的“名言”元素
for quote in response.css("div.quote"):
text = quote.css("span.text::text").get()
author = quote.css("small.author::text").get()
tags = quote.css("div.tags>a.tag::text").getall()
print(dict(text=text, author=author, tags=tags))
执行结果:
Scrapy 数据提取实战案例
本文的前面部分介绍了如何在下载的网页提取数据,这一部分将说明如何在蜘蛛代码中应用上述的提取数据的技能。
通常蜘蛛会爬取很多的网页,每个网页又提取很多数据项。这种情况下需要用到 yield 关键词,它会将每次迭代提取的数据项打印在终端上,或者输送到数据库或文件中。
现在你已经了解了一些有关选择和提取的知识,接下来完成通过编写蜘蛛的代码从网页中提取数据。
最简单的 Scrapy 爬虫实例
将原有的文件清空,然后复制粘贴以下的代码到文件中(这里是 tutorial/spiders/quotes.py):
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
"https://quotes.toscrape.com/page/1/",
"https://quotes.toscrape.com/page/2/",
]
def parse(self, response):
for quote in response.css("div.quote"):
yield {
"text": quote.css("span.text::text").get(),
"author": quote.css("small.author::text").get(),
"tags": quote.css("div.tags a.tag::text").getall(),
}
运行
scrapy crawl quotes
执行的部分结果:
停止运行
按下键盘上的 Ctrl + D 组合键即可,按一次不停止,就按多次。
这是个简单的爬虫案例,实际应用中,蜘蛛可以编写得很复杂。
那么,Scrapy 爬虫爬取的数据怎样处理储存呢?一般有两种方式,一是写入文本文件,二是存入数据库。
由于数据库要额外学习,在本教程的宗旨是快速入门,因此,容易写入和读取的文本文件成为本文使用的数据存放方式。
接下来介绍两种常见的文本数据文件格式:JSON Lines 和 JSON。
数据存储格式
最简单的提取数据的储存方式是用文件,一般序列化的数据使用 .json 和 .jsonl 两种数据导出方式的文件类型。
Scrapy 官方文档建议采用 JSONL 文件格式保存提取的数据。
JSON Lines 文件格式
JSON 行(JSON Lines)文本格式,也称为换行分隔的 JSON。JSON 行是一种存储结构化数据的方便格式,能够一次处理一条记录,每行是单独的一条数据,这与 JSON 格式明显不同。它适用于 unix 风格的文本处理工具和 Shell 管道。这是一种很好的日志文件格式。它还是在协作进程之间传递消息的灵活格式。
JSON Lines 文件内容的格式如下:
{"text": "“I like nonsense, it wakes up the brain cells. Fantasy is a necessary ingredient in living.”", "author": "Dr. Seuss", "tags": ["fantasy"]}
{"text": "“I may not have gone where I intended to go, but I think I have ended up where I needed to be.”", "author": "Douglas Adams", "tags": ["life", "navigation"]}
{"text": "“The opposite of love is not hate, it's indifference. The opposite of art is not ugliness, it's indifference. The opposite of faith is not heresy, it's indifference. And the opposite of life is not death, it's indifference.”", "author": "Elie Wiesel", "tags": ["activism", "apathy", "hate", "indifference", "inspirational", "love", "opposite", "philosophy"]}
...
JSON Lines有以下的特点:
使用 UTF-8 编码;
每行都是一个合法的 JSON 值(JSON 序列化的值,只是不包含方括号);
行分隔符是'\n',但是 '\r\n' 也会被接受;
然而处理 JSON Lines 文件的每一行时,忽略掉行分隔符,就好像没有分隔符一般,这给了用户极大的方便;
建议约定用以 .jsonl 为后缀的文件保存 JSON Lines 的内容。
建议使用 gzip 或 bzip2 等流压缩器来节省空间,从而生成 .jsonl.gz 或 .jsonl.bz2 文件。
.jsonl 文件的 MIME 类型可能是 application/jsonl,但是它至今还没有标准化。
文件内行的称呼问题:文本编辑器中称第一行为“行1”,而在 JSON Lines 文件中的第一个值称呼为“值1”。
JSON 文件格式
JSON 全称“JavaScript Object Notation”,是一种通用的数据保存格式,它以字典和列表的形式储存在文本文件中。
JSON 文件内容的格式大致如下:
[ {"text": "“I like nonsense, it wakes up the brain cells. Fantasy is a necessary ingredient in living.”", "author": "Dr. Seuss", "tags": ["fantasy"]},
{"text": "“I may not have gone where I intended to go, but I think I have ended up where I needed to be.”", "author": "Douglas Adams", "tags": ["life", "navigation"]},
{"text": "“The opposite of love is not hate, it's indifference. The opposite of art is not ugliness, it's indifference. The opposite of faith is not heresy, it's indifference. And the opposite of life is not death, it's indifference.”", "author": "Elie Wiesel", "tags": ["activism", "apathy", "hate", "indifference", "inspirational", "love", "opposite", "philosophy"]},
...
]
而 JSON Lines 文件内容的格式如下:
{"text": "“I like nonsense, it wakes up the brain cells. Fantasy is a necessary ingredient in living.”", "author": "Dr. Seuss", "tags": ["fantasy"]}
{"text": "“I may not have gone where I intended to go, but I think I have ended up where I needed to be.”", "author": "Douglas Adams", "tags": ["life", "navigation"]}
{"text": "“The opposite of love is not hate, it's indifference. The opposite of art is not ugliness, it's indifference. The opposite of faith is not heresy, it's indifference. And the opposite of life is not death, it's indifference.”", "author": "Elie Wiesel", "tags": ["activism", "apathy", "hate", "indifference", "inspirational", "love", "opposite", "philosophy"]}
从以上的举例可以看出, JSON 格式有明显的局限性,如果往文件添加数据,它将会再追加一个 JSON 格式的数据,就像如下:
[
{"text": "“I like nonsense, it wakes up the brain cells. Fantasy is a necessary ingredient in living.”", "author": "Dr. Seuss", "tags": ["fantasy"]},
{"text": "“I may not have gone where I intended to go, but I think I have ended up where I needed to be.”", "author": "Douglas Adams", "tags": ["life", "navigation"]},
...
][
{"text": "“Try not to become a man of success. Rather become a man of value.”", "author": "Albert Einstein", "tags": ["adulthood", "success", "value"]},
{"text": "“It is better to be hated for what you are than to be loved for what you are not.”", "author": "André Gide", "tags": ["life", "love"]},
...
]
毫不意外,这就破坏了 JSON 约定俗成的格式。
而 JSON Lines 数据的添加则更简单,并一如既往地易读取和写入,就是在原先的文件内容的末尾追加若干行。
由此可见,以行(一行为一个值)为单位的 JSON Lines 格式更适合读取和添加数据,而 JSON 格式更易序列化和反序列化。
导出爬虫提取的数据
准备将数据输出到文件,这里采用建议的 JSON Lines 格式。
需要用到 -o 选项。导出数据的命令格式为:scrapy crawl 蜘蛛名称 -o 文件名.文件格式,-o 选项表示将数据追加到文件中,不会覆盖原有的文件内容。
然而,如果用选项 -O(大写的字母 o) 会覆盖原文件。
具体的选项列表在 scrapy crawl --help。
导出爬取到的数据到文件 quotes.jsonl,并且以追加内容的方式:
scrapy crawl quotes -o quotes.jsonl
程序会自动识别导出的文件格式。
执行结果:
此章节运行一个爬虫例子,演示了 Scrapy 如何导出 JSON Lines 格式数据,倘若要保存到 JSON 文件,那么仅须将 -o quotes.jsonl 更换为 -o quotes.json,系统会自动识别。
爬虫自动跟踪链接
通过获取指向下一页的链接来抓取更多的数据就是跟踪链接,获得下一个要爬取的 URL。
利用响应对象从结构化文本获取下一页链接
根据浏览器工具的检查可知,页面 https://quotes.toscrape.com/ 的下一页链接文字“Next”所在的 HTML 结构如下所示:
<ul class="pager">
<li class="next">
<a href="/page/2/">Next <span aria-hidden="true">→</span></a>
</li>
</ul>
前文就介绍到交互式命令行,在里面可以测试提取数据的查询规则。
创建并进入会话:scrapy shell "https://quotes.toscrape.com/" 。
经过调试,就可以把这些规则和代码写入到蜘蛛代码文件中,让蜘蛛正确地爬取和处理数据。
比如,获取 Next 所在的锚链接的 href 属性,可执行以下的代码:
response.css("li.next a::attr(href)").get()
执行结果:
'/page/2/'
也可以使用 attrib 属性做与上述同样的事情:
response.css("li.next a").attrib["href"]
关于元素属性可参考:https://docs.scrapy.org/en/latest/topics/selectors.html#selecting-attributes 。
翻页跟踪实现
复制粘贴以下的代码到蜘蛛 quotes 代码文件中:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
"https://quotes.toscrape.com/page/1/",
]
def parse(self, response):
for quote in response.css("div.quote"):
yield {
"text": quote.css("span.text::text").get(),
"author": quote.css("small.author::text").get(),
"tags": quote.css("div.tags a.tag::text").getall(),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
代码解释:
start_urls 是一个 URL 列表,列表里面的 URL 会自动放进 Scrapy 蜘蛛请求队列中,并且会自动地下载相关的 HTML 代码,然后将响应对象的实例交给 parse() 方法,这是蜘蛛中默认的响应处理方法;
yield 在 Python 中表示脚本代码解释器执行到这一步才让返回它所代表的数据,其他时候时时处于待命状态,这不同于 return 关键词;
response.urljoin() 方法合并响应 URL 与相对 URL(这里是以 http 开头的首页 URL 与分页的相对 URL),从而组成一个绝对 URL;
若下一页的链接不是个相对 URL,那么这里就不用该方法,可直接把 .get() 方法提取到的 URL 作为第一个参数传递进 scrapy.Request() 方法中;
yield scrapy.Request(next_page, callback=self.parse) 该行代码将对每一个由下一页锚链接提取的 URL 进行爬取。
运行:
scrapy crawl quotes -o quotes.jsonl
执行上述命令将会爬取所有页面,从而提取所有页面中想要的信息。
执行结果:
数据导出的文件名为 quotes.jsonl,它的文件内容中每一行分别为一个值,值是个序列化的字典,里面包含每次提取的数据,然后一行行地写入文件。
quotes.jsonl 文件头:
文件尾:
利用 response.follow () 实现分页爬取
将下面的代码替换 tutorial/spiders/quotes.py 文件中的内容:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
"https://quotes.toscrape.com/page/1/",
]
def parse(self, response):
for quote in response.css("div.quote"):
yield {
"text": quote.css("span.text::text").get(),
"author": quote.css("span small::text").get(),
"tags": quote.css("div.tags a.tag::text").getall(),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
这里得到与上一个章节同样的结果。
与 scrapy.Request() 不同,response.follow() 方法直接支持相对 URL,不再需要调用 urljoin;
response.follow() 同样返回一个请求实例,用户必须 yield 这个请求实例;
可直接传递 href 的选择器给 response.follow() 方法的第一个参数,而非字符串 URL:
for href in response.css("ul.pager a::attr(href)"):
yield response.follow(href, callback=self.parse)
可直接用 a 元素的选择器:
for a in response.css("ul.pager a"):
yield response.follow(a, callback=self.parse)
还可用 response.follow_all() 从可迭代对象创建多个请求:
anchors = response.css("ul.pager a")
yield from response.follow_all(anchors, callback=self.parse)
最短代码做同样的事:
yield from response.follow_all(css="ul.pager a", callback=self.parse)
章节总结
本章节主要介绍了如何利用 Scrapy 的蜘蛛自动地爬取下一页并提取每一页上面的信息,并且还介绍了用不同的方式实现如何去发出跟踪每一页的请求的代码。
跟踪链接自动翻页实战
本章节要创建一个稍为复杂的新爬虫,用于爬取所有名言作者的详情页面,同时提取需要的数据,以 JSON Lines 的格式输出到文件。并且验证上文到的 Scrapy 链接跟踪和自动翻页技术。
创建蜘蛛
scrapy genspider author "https://quotes.toscrape.com/"
覆盖新创建的代码
关于新蜘蛛的代码,教程已经写好了,可直接复制粘贴到文件(tutorial/spiders/author.py)中:
import scrapy
class AuthorSpider(scrapy.Spider):
name = "author"
start_urls = ["https://quotes.toscrape.com/"]
def parse(self, response):
author_page_links = response.css(".author + a")
yield from response.follow_all(author_page_links, self.parse_author)
pagination_links = response.css("li.next a")
yield from response.follow_all(pagination_links, self.parse)
def parse_author(self, response):
def extract_with_css(query):
return response.css(query).get(default="").strip()
yield {
"name": extract_with_css("h3.author-title::text"),
"birthdate": extract_with_css(".author-born-date::text"),
"bio": extract_with_css(".author-description::text"),
}
上面的代码中:
从主页开始,它会跟踪所有指向作者详情页面的锚链接并为每个页面调用 parse_author() 回调方法;
在 parse() 方法中对页面的请求回调 parse_author() 方法,然后在该方法中提取数据,这些数据可以由管道(pipeline)交给 Items 处理;
关于“管道”和“Items” 请参阅文档获取更多的信息:
用 yield 来持续地为每次响应返回该返回的字典数据。
该方法中还有个辅助函数,该函数中的 .get() 方法添加了一个默认值,当 CSS 查询匹配不到规则时,'response.css()' 方法就返回此默认值,而不是 None。
文档教程中还指出:用户不用担心会有重复的页面被请求,因为在同一次爬取中, Scrapy 会自动过滤掉重复的内容,以及不会再次访问已经访问过的 URL。
开始爬取
scrapy crawl author -O authors.jsonl
这次用上 -O 选项,为的是再次爬取时覆盖原输出文件。
当你阅读到这里时,很大程度上你已经了解和使用 Scrapy 如何跟踪链接和回调的机制。
你也可以了解一下另外一种跟踪链接机制—— CrawlSpider 。
将附加数据传递给回调函数: https://docs.scrapy.org/en/latest/topics/request-response.html#topics-request-response-ref-request-callback-arguments 。
利用蜘蛛参数指定爬取的条件
这是文档教程中的最后一个示例,通过在蜘蛛代码中设定属性,然后在爬取命令中使用 -a 参数指定键值对。
设置好这些,蜘蛛将按 -a 参数指定的属性键值对来过滤爬取的访问,只会访问符合该属性键与值标记的 URL。
爬取命令的选项的阅读,请在终端执行:scrapy crawl --help。
修改代码文件 tutorial/spiders/quotes.py,然后用文档中的代码覆盖:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
def start_requests(self):
url = "https://quotes.toscrape.com/"
tag = getattr(self, "tag", None)
if tag is not None:
url = url + "tag/" + tag
yield scrapy.Request(url, self.parse)
def parse(self, response):
for quote in response.css("div.quote"):
yield {
"text": quote.css("span.text::text").get(),
"author": quote.css("small.author::text").get(),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page is not None:
yield response.follow(next_page, self.parse)
运行:
scrapy crawl quotes -O quotes.jsonl -a tag=humor
在以上运行的命令中:
-O 选项指定从网页提取出来的信息的输出文件,这里选用 JSON Lines 格式,如果文件扩展名为 .json,则使用 JSON 格式,这由 Scrapy 自动判断。
-a 选项指定要过滤的条件,在蜘蛛代码中指定了该属性(用 getattr() 函数,该函数的第二个参数便是),然后用赋值的方式指定 tag 属性的值。
代码中,tag = getattr(self, "tag", None) 表示:获取命令行中由-a选项指定的 tag 属性的值,然后赋这个值给变量 tag。
如果 -a 选项设为 -a tag=humor,则表示仅访问 tag 值为 humor 的 URL。
此选项要结合实际情况来使用。