Scrapy 入门
上一节我们介绍了 Scrapy 框架的基本架构、数据流过程和项目架构,对 Scrapy 有了初步的认识。接下来我们用 Scrapy 实现一个简单的项目,完成一遍 Scrapy 抓取流程。通过这个过程,我们可以对 Scrapy 的基本用法和原理有大体了解。
本节目标
本节要完成的目标如下。
-
创建一个 Scrapy 项目,熟悉 Scrapy 项目的创建流程。
-
编写一个 Spider 来抓取站点和处理数据,了解 Spider 的基本用法。
-
初步了解 Item Pipeline 的功能,将抓取的内容保存到 MongoDB 数据库。
-
运行 Scrapy 爬虫项目,了解 Scrapy 项目的运行流程。
这里我们以 Scrapy 推荐的官方练习项目为例进行实战演练,抓取的目标站点为 https://quotes.toscrape.com/ ,页面如图 15-3 所示。

这个站点包含了一系列名人名言,作者和标签,我们需要使用 Scrapy 将其中的内容爬取并保存下来。
准备工作
在开始之前,我们需要安装好 Scrapy 框架、MongoDB 和 PyMongo 库,具体的安装参考流程如下。
-
MongoDB: https://setup.scrape.centet/mongodb
-
PyMongo: https://setup.scrape.center/pymongo
安装好这三部分之后,我们就可以正常使用 Scrapy 命令了,同时也可以使用 PyMongo 连接 MongoDB 数据库并写人数据了。
做好如上准备工作之后,我们便可以开始本节的学习了。
创建项目
首先我们需要创建一个 Scrapy 项目,可以直接用命令生成,项目名称可以叫作 scrapytutorial,创建命令如下:
scrapy startproject scrapytutorial
运行完毕后,当前文件夹下会生成一个名为 scrapytutorial 的文件夹,文件夹结构如下所:
scrapy.cfg # Scrapy部署时的配置文件
scrapytutorial # 项目的模块,引入的时候需要从这里引入
__init__.py
items.py # Items的定义,定义爬取的数据结构
middlewares.py # Middlewares 的定义,定义爬取时的中间件
pipelines.py # Pipelines 的定义,定义数据管道
settings.py # 配置文件
spiders # 放置 Spiders 的文件夹
__init__.py
创建 Spider
Spider 是自已定义的类,Scrapy 用它来从网页里抓取内容,并解析抓取的结果。不过这个类必须继承 Scrapy 提供的 Spider 类 scrapy.Spider,还要定义 Spider 的名称和起始 Request,以及怎样处理爬取后的结果的方法。
也可以使用命令行创建一个 Spider。比如要生成 Quotes 这个 Spider,可以执行如下命令:
cd scrapytutorial
scrapy genspider quotes
进人刚才创建的 scrapytutorial 文件夹,然后执行 genspider 命令。第一个参数是 Spider 的名称,第二个参数是网站域名。执行完毕后,spiders 文件夹中多了一个 quotes.py,它就是刚刚创建的 Spider 内容如下所示:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
allowed_domains = ["quotes.toscrape.com"]
start_urls = ['https://quotes.toscrape.com/']
def parse(self, response):
pass
这个 QuotesSpider 就是刚才命令行自动创建的 Spider,它继承了 scrapy 的 Spider 类,QuotesSpider 有 3 个属性,分别为 name、allowed_domains 和 start_urls,还有一个方法 parse。
-
name 是每个项目唯一的名字,用来区分不同的 Spider
-
allowed_domains 是允许爬取的域名,如果初始或后续的请求链接不是这个域名下的:则请求链接会被过滤掉。
-
start_urls 包含了 Spider 在启动时爬取的 URL 列表,初始请求是由它来定义的。
-
parse 是 Spider 的一个方法。在默认情况下,start_urls 里面的链接构成的请求完成下载后,parse 方法就会被调用,返回的响应就会作为唯一的参数传递给 parse 方法。该方法负责解析返回的响应、提取数据或者进一步生成要处理的请求。
创建 Item
上一节我们讲过,Item 是保存爬取数据的容器,定义了爬取结果的数据结构。它的使用方法和字典类似。不过相比字典,Item 多了额外的保护机制,可以避免拼写错误或者定义字段错误。
创建 Item 需要继承 scrapy 的 Item 类,并且定义类型为 Field 的字段,这个字段就是我们要爬取的字段。
那我们需要爬哪此字段呢?观察目标网站,我们可以获取到的内容有下面几项:
-
text:文本,即每条名言的内容,是一个字符串。
-
author:作者,即每条名言的作者,是一个字符串。
-
tags:标签,即每条名言的标签,是字符串组成的列表。
这样的话,每条爬取数据就包含这 3 个字段,那么我们就可以定义对应的 Item,此时将 items.py 修改如下:
import scrapy
class QuoteItem(scrapy.Item):
text = scrapy.Field()
author = scrapy.Field()
tags = scrapy.Field()
这里我们声明了 QuoteItem,继承了 Item 类,然后使用 Field 定义了 3 个字段,接下来爬取时我们会使用到这个 Item。
解析 Response
前面我们看到,parse 方法的参数 response 是 start_urls 里面的链接爬取后的结果,即页面请求后得到的 Response,Scrapy 将其转化为了一个数据对象,里面包含了页面请求后得到的 Response Status,Body 等内容。所以在 parse 方法中,我们可以直接对 response 变量包含的内容进行解析,比如浏览请求结果的网页源代码,进一步分析源代码内容,或者找出结果中的链接而得到下一个请求。
我们可以看到网页中既有我们想要的结果,又有下一页的链接,这两部分内容我们都要进行处理。
首先看看网页结构,如图 15-4 所示。每一页都有多个 class 为 quote 的区块,每个区块内都包含 text、author、tags。那么我们先找出所有的 quote,然后提取每个 quote 中的内容。
(图省略)
我们可以使用 CSS 选择器或 XPath 选择器进行提取,这个过程我们可以直接借助 response 的 css 或 xpath 方法实现,这都是 Scrapy 给我们封装好的方法,直接调用即可。
在这里我们使用 CSS 选择器进行选择,可以将 parse 方法的内容进行如下改写:
def pares(self, response):
quotes = response.css('.quote')
for quote in quotes:
text = quote.css('.text::text').extract_first()
author = quote.css('.author::text').extract_first()
tags = quote.css('.tags .tag::text').extract()
这里首先利用 CSS 选择器选取所有的 quote 并将其赋值为 quotes 变量,然后利用 for 循环遍历每个 quote,解析每个 quote 的内容。
对 text 来说,观察到它的 class 为 text,所以可以用 .text 选择器来选取,这个结果实际上是整个带有标签的节点,要获取它的正文内容,可以加 ::text。这时的结果是长度为 1 的列表,所以还需要用 extract_first 方法来获取第一个元素。而对于 tags 来说,由于我们要获取所有的标签,所以用 extract 方法获取整个列表即可。
为了更好地理解以上内容的提取过程,我们以第一个 quote 的结果为例,看一下各个提取写法会得到怎样的提取结果。源码如下:
(省略)
不同选择器的返回结果如下:
(省略)
这里我们演示了不同提取过程的写法,其提取结果也是各不相同,比如单独调用 CSS 方法我们得到的是 Selector 对象组成的列表;调用 extract 方法会进一步从 Selector 对象里提取其内容,再加上 ::text 则会从 HTML 代码中提取出正文文本。
因此对于 text,我们只需要获取结果的第一个元素即可,所以使用 extract_first 方法,得到的就是一个字符串。而对于 tags,我们要获取所有结果组成的列表,所以使用 extract 方法,得到的就是所有标签字符组成的列表。
使用 Item
上文我们已经定义了 QuoteItem,接下来就要使用它了。
我们可以把 Item 理解为一个字典,和字典还不太相同,其本质是一个类,所以在使用的时候需要实例化。实例化之后,我们依次用刚才解析的结果赋值 Item 的每一个字段,最后将 Item 返回。
QuotesSpider 的改写如下:
import scrapy
from scrapytutorial.items import QuoteItem
class QuotesSpider(scrapy.Spider):
name = "quotes"
allowed_domains = ["quotes.toscrape.com"]
start_urls = ['https://quotes.toscrape.com']
def parse(self, response):
quotes = response.css('.quote')
for quote in quotes:
item = QuoteItem()
item['text'] = quote.css('.text::text').extract_first()
item['author'] = quote.css('.author::text').extract_first()
item['tags'] = quote.css('.tags .tag::text').extract()
yield item
如此一来,首页的所有内容就被解析出来并被赋值成了一个个 QuoteItem 了,每个 QuoteItem 就代表一条名言,包含名言的内容、作者和标签。
后续 Request
上面的操作实现了从首页抓取内容,如果运行它,我们其实已经可以从首页提取到所有 quote 信息并将其转化为一个个 QuoteItem 对象了。
但是,这样还不够,下一页的内容该如何抓取呢?这就需要我们从当前页面中找到信息来生成下一个 Request,利用同样的方式进行请求并解析就好了。那再下一页呢?也是一样的原理,我们可以在下一个页面里找到信息再构造再下一个 Request。这样循环往复迭代,从而实现整站的爬取。
我们将刚才的页面拉到最底部,如图 15-5 所示。
(省略)
这里我们发现有一个 Next 按钮:查看一下源代码,可以看到它的链接是 /page/2/,实际上全链接就是 https://quotes.toscrape.com/page/2 ,通过这个链接我们就可以构造下一个 Request 了。
构造 Request 时需要用到 scrapy 的 Request 类。这里我们传递两个参数,分别是 url 和 callback,这两个参数的说明如下。
-
url:目标页面的链接。
-
callback:回调方法,当指定了该回调方法的 Request 完成下载之后,获取Response,Engine 会将该 Response 作为参数传递给这个回调方法。回调方法进行 Response 的解析生成一个或多个 Item 或 Request,比如上文的 parse 方法就是回调方法。
由于刚才所定义的 parse 方法就是用来提取名言 text、author、tags 的方法,而下一页的结构和刚才已经解析的页面结构是一样的,所以我们可以再次使用 parse 方法来做页面解析。
接下来我们要做的就是利用选择器得到下一页链接并生成请求,在 parse 方法后追加如下的代码:
next = response.css('.pager .next a::attr(href)').extract_first()
url = response.urljoin(next)
yield scrapy.Request(url=url,callback=self.parse)
第一行代码首先通过 CSS 选择器获取下一个页面的链接,即要获取超链接 a 中的 href 属性,这里用到了 ::attr(href) 进行提取,其中 attr 代表提取节点的属性,href 则为要提取的属性名,然后再下一步调用 extract_first 方法获取内容。
第二行代码调用了 urljoin 方法,urljoin 方法可以将相对 URL 构造成一个绝对 URL。例如,获取到的下一页地址是 /page/2/,urljoin 方法处理后得到的结果就是 https://quotes.toscrape.com/page/2/ 。
第三行代码通过 url 和 callback 变量构造了一个新的 Request,回调方法 callback 依然使用 parse 方法。这个 Request 执行完成后,其对应的 Response 会重新经过 parse 方法处理,得到第二页的解析结果,然后以此类推,生成第二页的下一页,也就是第三页的请求。这样爬虫就进人了一个循环,直到最后一页。
通过几行代码,我们就轻松实现了一个抓取循环,将每个页面的结果抓取下来了。
现在,改写之后的整个 Spider 类如下所示:
import scrapy
from scrapytutorial.items import QuoteItem
class QuotesSpider(scrapy.Spider):
name = "quotes"
allowed_domains = ["quotes.toscrape.com"]
start_urls = ['https://quotes.toscrape.com']
def parse(self, response):
quotes = response.css('.quote')
for quote in quotes:
item = QuoteItem()
item['text'] = quote.css('.text::text').extract_first()
item['author'] = quote.css('.author::text').extract_first()
item['tags'] = quote.css('.tags .tag::text').extract()
yield item
next = response.css('.pager .next a::attr("href")').extract_first()
url = response.urljoin(next)
yield scrapy.Request(url=url, callback=self.parse)
可以看到整个站点的抓取逻辑就轻松完成了,不需要再去编写怎样发送 Request,不需要去关心异常处理,因为这些工作 Scrapy 都帮我们完成了,我们只需要关注 Spider 本身的抓取和提取逻辑即可。
运行
接下来就是运行项目了,进入项目目录,运行如下命令:
scrapy crawl quotes
就可以看到 Scrapy 的运行结果了:
这里只是部分运行结果,省略了一些中间的抓取结果。
首先,Scrapy 输出了当前的版本号以及正在启动的项目名称。然后输出了当前 settings.py 中一些重写后的配置。接着输出了当前所应用的 Middlewares 和 ItemPipelines。Middlewares 和 ItemPipelines 都沿用了 Scrapy 的默认配置,我们可以在 settings.py 中配置它们的开启和关闭,后文会对它们的用法进行讲解。
接下来就是输出各个页面的抓取结果了,可以看到爬虫一边解析,一边翻页,直到将所有内容抓取完毕,然后终止。
最后,Scrapy 输出了整个抓取过程的统计信息,如请求的字节数、请求次数,响应次数、完成原因等。
整个 Scrapy 程序成功运行。我们通过非常简单的代码就完成了一个站点内容的爬取,所有的名言都被我们抓取下来了。
保存到文件
运行完 Scrapy 后,我们只在控制台上看到了输出结果。如果想保存结果该怎么办呢?
要完成这个任务其实不需要任何额外的代码:Scrapy 提供的 Feed Exports 可以轻松将抓取结果输出。例如:如果我们想将上面的结果保存成 JSON 文件,那么可以执行如下命令:
scrapy crawl quotes -o quotes.json
命令运行后,项目内多了一个 quotes.json 文件,文件包含了刚才抓取的所有内容,内容是 JSON 格式。
另外我们还可以让每一个 Item 输出一行 JSON,输出后缀为 jl,为 jsonline 的缩写,命令如下所示:
scrapy crawl quotes -o quotes.jl
# 或
scrapy crawl quotes -o quotes.jsonlines
FeedExports 支持从输出格式还有很多,例如 csv、xml、pickle、marshal等,同时它支持 ftp、s3 等远程输出,另外还可以通过自定义 ItemExporter 来实现其他的输出。
例如,下面命令对应的输出分别为 csv、xml,pickle、marshal 格式以及 ftp 远程输出:
scrapy crawl quotes -o quotes.csv
scrapy crawl quotes -o quotes.xml
scrapy crawl quotes -o quotes.pickle
scrapy crawl quotes -o quotes.marshal
scrapy crawl quotes -o ftp://user:pass@ftp.example.com/path/to/quotes.csv
其中,ftp 输出需要正确配置用户名、密码、地址、输出路径,否则会报错。
通过 Scrapy 提供的 Feed Exports,我们可以轻松地将抓取结果到输出到文件中。对于一些小型项目来说这应该足够了。
如果想要更复杂的输出,如输出到数据库等,我们可以使用 Item Pipeline 来完成。
使用 Item Pipeline
如果想进行更复杂的操作,如将结果保存到 MongoDB 数据库中或者筛选某些有用的 Item,那么我们可以定义 Item Pipeline 来实现。
Item Pipeline 为项目管道。当 Item 生成后,它会自动被送到 Item Pipeline 处进行处理,我们可以用 Item Pipeline 来做如下操作:
-
清洗 HTML 数据;
-
验证爬取数据,检查爬取字段;
-
查重并丢弃重复内容;
-
将爬取结果储存到数据库;
要实现 Item Pipeline 很简单,只需要定义一个类并实现 process_item 方法即可。启用 Item Pipeline 后,Item Pipeline 会自动调用这个方法。process_item 方法必须返回包含数据的字典或 Item 对象,或者抛出 DropItem 异常。
process_item 方法有两个参数。一个参数是 item,每次 Spider 生成的 Item 都会作为参数传递过来。另一个参数是 spider,就是 Spider 的实例。
接下来,我们实现一个 Item Pipeline,筛掉 text 长度大于 50 的 Item,并将结果保存到 MongoDB。
修改项目里的 pipelines.py 文件,之前用命令行自动生成的文件内容可以删掉,增加一个 TextPipeline 类,内容如下所示:
from scrapy.exceptions import DropItem
class TextPipeline(object):
def __init__(self):
self.limit = 50
def process_item(self, item, spider):
if item['text']:
if len(item['text']) > self.limit:
item['text'] = item['text'][0:self.limit].rstrip() + '...'
return item
else:
return DropItem('Missing Text')
这段代码在构造方法里定义了限制长度为 50,实现了 process_item 方法,其参数是 item 和 spider。首先该方法判断 item 的 text 属性是否存在,如果不存在,则抛出 DropItem 异常。如果存在,再判断长度是否天于 50,如果大于,那就截断然后拼接省略号,再将 item 返回。
接下来,我们将处理后的 item 存入 MongoDB,定义另外一个 Pipeline。同样在 pipelines.py 中,我们实现另一个类 MongoPipeline,内容如下所示:
class MongoDBPipeline(object):
def __init__(self, connection_string, database):
self.connection_string = connection_string
self.database = database
@classmethod
def from_crawler(cls, crawler):
return cls(
connection_string=crawler.settings.get(
'MONGODB_CONNECTION_STRING'),
database=crawler.settings.get('MONGODB_DATABASE')
)
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.connection_string)
self.db = self.client[self.database]
def process_item(self, item, spider):
name = item.__class__.__name__
self.db[name].insert_one(dict(item))
return item
def close_spider(self, spider):
self.client.close()
MongoPipeline 类实现了另外几个 API 定义的方法。
-
from_crawler:一个类方法,用 @classmethod 标识,这个方法是以依赖注入的方式实现的,方法的参数就是 crawler。通过 crawler,我们能拿到全局配置的每个配置信息,在全局配置 settings.py 中,可以通过定义 MONGO_URI 和 MONGO_DB 来指定 MongoDB 连接需要的地址和数据库名称,拿到配置信息之后返回类对象即可。所以这个方法的定义主要是用来获取 settings.py 中的配置的。
-
open_spider:当 Spider 被开启时,这个方法被调用,主要进行了一些初始化操作。
-
close_spider:当 Spider 被关闭时,这个方法被调用,将数据库连接关闭。
最主要的 process_item 方法则执行了数据插人操作,这里直接调用 insert 方法传入 item 对象即可将数据存储到 MongoDB。
定义好 TextPipeline 和 MongoDBPipeline 这两个类后,我们需要在 settings.py 中使用它们。MongoDB 的连接信息还需要定义。
我们在 settings.py 中加人如下内容:
ITEM_PIPELINES = {
'scrapytutorial.pipelines.TextPipeline': 300,
'scrapytutorial.pipelines.MongoDBPipeline': 400,
}
MONGODB_CONNECTION_STRING = 'localhost'
MONGODB_DATABASE = 'scrapytutorial'
这里我们声明了 ITEM_PIPELINES 字典,键名是 Pipeline 的类名称,键值是调用优先级,是一个数字,数字越小则对应的 Pipeline 越先被调用,另外我们声明了 MongoDB 的连接字符串和存储的数据库名称。
再重新执行爬取,命令还是一样的:
scrapy crawl quotes
爬取结束后,我们可以看到 MongoDB 中创建了一个 scrapytutorial 的数据库和 QuoteItem 的表,内容如图 15-6 所示。
(省略图)
长的 text 已经被处理并追加了省略号,短的 text 保持不变,author 和 tags 也都相应保存到了数据中。