Python3网络爬虫开发实战(15)Scrapy 框架的使用
文章目录
- 一、Scrapy 框架介绍
- 1.1 数据流
- 1.2 项目结构
- 1.3 Scrapy 入门
- 二、Selector 解析器
- 2.1 XPath 和 CSS 选择器
- 2.2 信息提取
- 2.3 正则提取
- 三、Spider 的使用
- 3.1 Spider 运行流程
- 3.2 Spider 类分析
- 3.3 Request
- 3.4 Response
- 四、Download Middleware 的使用
- 4.1 process_request(request, spider)
- 4.2 process_response(request, response, spider)
- 4.3 process_exception(request, exception, spider)
- 五、Spider Middleware 的使用
- 5.1 process_spider_input(response, spider)
- 5.2 process_spider_output(response, result, spider)
- 5.3 process_spider_exception(response, exception, spider)
- 5.4 process_start_requests(start_requests, spider)
- 5.5 内置Spider Middleware 简介
- 六、Item Pipeline 的使用
- 6.1 process_item(item, spider)
- 6.2 open_spider(self, spider)
- 6.3 close_spider(spider)
- 6.4 from_crawler(cls, crawler)
- 七、Extension 的使用
- 7.1 部署本地 flask 服务器
- 7.2 extensions.py
- 八、Scrapy 自动化配置
- 8.1 Scrapy 对接 Splash
- 8.2 Splash 对接 Selenium
- 8.3 关于其他
- 九、Scrapy 规则化爬虫
- 9.1 CrawlSpider
- 9.2 Item Loader
Scrapy 是一个基于 Python 开发的爬虫框架,是当前 Python 爬虫生态中最流行的爬虫框架,该框架提供了非常多爬虫相关的基础组件,架构清晰,可拓展性极强。
之前大多是基于 requests 或 aiohttp 来实现爬虫的整个逻辑的,可以发现,在整个过程中,我们需要实现爬虫相关的所有操作,例如爬取逻辑,异常处理,数据解析,数据存储等,但其实这些步骤很多都是通用或者重复的。既然如此,我们可以将这些步骤的逻辑分离出来,把其中通用的功能做成一个个基础的组件。
在抽离处基础组件之后,每次爬虫只需要在这些组件基础上加上特定的逻辑就可以实现爬取的流程了,而不用再把爬虫中每个细小的流程都实现一遍。
Scrapy 框架几乎是 Python 爬虫学习和工作过程中必须掌握的框架
一、Scrapy 框架介绍
- Scrapy Engine(引擎): 负责Spider、ItemPipeline、Downloader、Scheduler中间的通讯,信号、数据传递等。
- Item: 是一个抽象的数据结构,定义了爬取结果的数据结构,爬去的数据会被赋值成 Item 对象,每个 Item 就是一个类,类里面定义了爬取结果的数据字段,可以理解为它用来规定爬取数据的存储格式。
- Scheduler(调度器): 它负责接受引擎发送过来的Request请求,并按照一定的方式进行整理排列,入队,当引擎需要时,交还给引擎。
- Downloader(下载器):负责下载Scrapy Engine(引擎)发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spider来处理,
- Spider(爬虫):它负责处理所有Responses,从中分析提取数据,获取Item字段需要的数据,并将需要跟进的URL提交给引擎,再次进入Scheduler(调度器).
- Item Pipeline(管道):它负责处理Spider中获取到的Item,并进行进行后期处理(详细分析、过滤、存储等)的地方。
- Downloader Middlewares(下载中间件):你可以当作是一个可以自定义扩展下载功能的组件。
- Spider Middlewares(Spider中间件):你可以理解为是一个可以自定扩展和操作引擎和Spider中间通信的功能组件(比如进入Spider的Responses;和从Spider出去的Requests)
1.1 数据流
详细可以看这篇文章:Scrapy 入门教程 | 菜鸟教程 (runoob.com),这里十分生动的表示了 Scrapy 个组件的交流:
- 启动爬虫项目时,Engine 根据要爬取的目标站点找到处理该站点的 Spider,Spider 会生成最初需要爬取的页面对应的一个或多个 Request,然后发给 Engine。
- Engine 从 Spider 中获取这些 Request,然后把它们交给 Scheduler 等待被调度
- Engine 向 Scheduler 索取下一个要处理的 Request,这时候 Scheduler 根据其调度逻辑选择合适的 Request 发送给 Engine
- Engine 将 Scheduler 发来的 Request 转发给 Downloader 进行下载执行,将 Request 发送给 Downloader 的过程会经由许多定义好的 Downloader Middlewares 的处理
- Downloader 将 Request 发送给目标服务器,得到对应的 Response,然后将其返回给 Engine。将 Response 返回 Engine 的过程同样会经由许多定义好的 Downloader Middlewares 的处理。
- Engine 从 Downloader 处接收到的 Response 里包含了爬取的目标站点的内容,Engine 会将此 Response 发送给对应的 Spider 进行处理,将 Response 发送给 Spider 的过程中会经由定义好的 Spider Middlewares 的处理
- Spider 处理 Response,解析 Response 的内容,这时候 Spider 会产生一个或多个爬取结果 Item 或者后续要爬取的目标页面对应的一个或多个 Request,然后再将这些 Item 或 Request 发送给 Engine 进行处理,将 Item 或 Request 发送给 Engine 的过程会经由定义好的 Spider Middlewares 的处理
- Engine 将 Spider 发回的一个或多个 Item 转发给定义好的 Item Pipelines 进行数据处理或存储的一系列操作,将 Spider 发回的一个或多个 Request 转发给 Scheduler 等待下一次被调度。
重复第2步到第8步,直到 Scheduler 中没有更多的 Request,这时候 Engine 会关闭 Spider,整个爬取过程结束。 从整体上来看,各个组件都只专注于一个功能,组件和组件之间的耦合度非常低,也非常容易扩展。再由 Engine 将各个组件组合起来,使得各个组件各司其职,互相配合,共同完成爬取工作。另外加上 Scrapy 对异步处理的支持,Scrapy 还可以最大限度地利用网络带宽,提高数据爬取和处理的效率。
1.2 项目结构
需要先安装 Scrapy 框架,可以直接使用 pip 安装
pip install scrapy
安装完毕后,可以使用命令行来创建一个爬虫项目,这里创建一个名为 news 的项目
scrapy startproject news
执行完毕后,当前目录下就会出现一个名为 news 的文件夹,该文件夹就对应一个 Scrapy 爬虫项目,接着进入 news 文件夹,然后创建一个名称为 sina 的 Spider,
# 进入news 文件夹
cd .\news
# 创建 Spider 名称为 sina 域名为 news.sina.com.cn
scrapy genspider sina news.sina.com.cn
最终会得到如下的一个文件结构
各个文件的功能描述如下:
- scrapy.cfg: Scrapy项目的配置文件,其中定义了项目的配置文件路径、部署信息等
- items.py: 定义了Item数据结构,所有Item的定义都可以放这里
- pipelines.py: 定义了Item Pipeline的实现,所有的Item Pipeline的实现都可以放在这里
- settings.py: 定义了项目的全局配置
- middlewares.py: 定义了Downloader Middlewares和Spider Middlewares的实现
- spiders: 里面包含了一个个 Spider 的实现,每个 Spider 都对应一个 Python 文件
1.3 Scrapy 入门
这里以 Scrapy 推荐的官方练习项目为例子进行爬取,抓取的目标站点为 https://quotes.toscrape.com/
创建一个项目名为 demo 的项目,spider 命名为 example,得到spider文件 example.py 如下
# example.py
import scrapyclass ExampleSpider(scrapy.Spider):name = "example"allowed_domains = ["quotes.toscrape.com"]start_urls = ["https://quotes.toscrape.com/"]def parse(self, response):pass
name 是每个项目唯一的名字,用于区分不同的 Spider
allowed_domains 是允许爬取的域名,如果初始或者后续的请求链接不是这个域名下的,则会被过滤掉
start_urls 包含了 spider 在启动时爬取的 URL 列表,初始请求是由它来定义的
parse 是 Spider 的一个方法,在默认情况下,start_urls 里面的链接构成请求完成下载后得到一个 response,parse 方法就会调用,response 作为参数;
进入到 Items.py 文件,如下
import scrapyclass DemoItem(scrapy.Item):# define the fields for your item here like:text = scrapy.Field()author = scrapy.Field()tags = scrapy.Field()
这里 Item 类似于一个字典,但是必须使用 scrapy.Field()
来定义;对于 Response 的解析,其接口如下所示:
- url:Request URL;
- status:Response 状态码,一般情况下请求成功状态码为200;
- headers:Response Headers,是一个字典,字段是一一对应的;
- body:Response Body,这个通常就是访问页面之后得到的源代码结果了,比如里面包含的是HTML或者JSON字符串,但注意其结果是 bytes 类型。与requests模块请求后得到的响应属性content类似;
- request:Response 对应的 Request 对象;
- certificate:是twisted.internet.ssl.Certifucate类型的对象,通常代表一个SSL证书对象;
- ip_address:是一个ipaddress.IPv4Address或IPv6Address类型的对象,代表服务器的IP地址;
- urljoin:是对URL的一个处理方法,可以传入当前页面的相对URL,该方法处理后返回的就是绝对URL,urljoin 其实使用的就是: from urllib.parse import urljoin 可以去看源码;
- follow/follow_all:是一个根据URL来生成后续Request的方法,和直接构造Request不同的是,该方法接收的url可以是相对URL,不必一定是绝对URL,因为follow方法中有做url拼接的操作;
- text: 同body属性,但结果是str类型;
- encoding: Response的编码,默认是utf-8;
- selector: 根据Response的内容构造而成的Selector对象,利用它我们可以进一步调用xpath、css等方法进行结果的提取;
- xpath()方法: 传入XPath进行内容提取,等同于调用selector的xpath方法;
- css()方法: 传入CSS选择器进行内容提取,等同于调用selector的css方法;
- json()方法: 可以直接将text属性转换为JSON对象;
与 requests 的 Response 主要的不同在于其不需要再导入 lxml 或者 bs4 来进行解析,里面自带有解析的工具;在了解如何解析 Response 之后,我们可以将 example.py 修正如下:
import scrapy
from ..items import DemoItemclass ExampleSpider(scrapy.Spider):name = "example"allowed_domains = ["quotes.toscrape.com"]start_urls = ["https://quotes.toscrape.com/"]def parse(self, response, **kwargs):quotes = response.css(".quote")for quote in quotes:item = DemoItem()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
目前只获取到首页的内容,我们需要获取到下一页的内容,可以在当前页面中寻找信息构建下一个 Request,Request 的构造参数梳理如下:
- url: Request 的页面链接,即 Request URL。
- callback:Request 的回调方法,通常这个方法需要定义在 Spider 类里面,并且需要对应一个 response 参数,代表 Request 执行请求后得到的 Response 对象。如果这个 callback 参数不指定,默认会使用 Spider 类里面的 parse 方法。
- method:Request 的方法,默认是 GET,还可以设置为 POST、PUT、DELETE 等。
- meta:Request 请求携带的额外参数,利用 meta,我们可以指定任意处理参数,特定的参数经由 Scrapy 各个组件的处理,可以得到不同的效果。另外,meta 还可以用来向回调方法传递信息。
- body:Request 的内容,即 Request Body,往往 Request Body 对应的是 POST 请求,我们可以使用 FormRequest 或 JsonRequest 更方便地实现 POST 请求。
- headers:Request Headers,是字典形式。
- cookies:Request 携带的 Cookies,可以是字典或列表形式。
- encoding:Request 的编码,默认是 utf-8。
- prority:Request 优先级,默认是0,这个优先级是给 Scheduler 做 Request 调度使用的,数值越大,就越被优先调度并执行。
- dont_filter:Request 不去重,Scrapy 默认会根据 Request 的信息进行去重,使得在爬取过程中不会出现重复的请求,设置为 True 代表这个 Request 会被忽略去重操作,默认是 False。
- errback:错误处理方法,如果在请求过程中出现了错误,这个方法就会被调用。
- flags:请求的标志,可以用于记录类似的处理。
- cb_kwargs:回调方法的额外参数,可以作为字典传递。
Scrapy 还专门为 POST 请求提供了两个类 —— FormRequest 和 JsonRequest,它们都是 Request 类的子类,我们可以利用 FormRequest 的 formdata 参数传递表单内容,利用 JsonRequest 的 json 参数传递 JSON 内容,其他的参数和 Request 基本是一致的。
第一个 JsonRequest,我们可以观察到页面返回结果的 json 字段就是我们所请求时添加的 data 内容,这说明实际上是发送了 Content-Type 为 application/json 的 POST 请求,这种对应的就是发送 JSON 数据。第二个 FormRequest,我们可以观察到页面返回结果的 form 字段就是我们请求时添加的 data 内容,这说明实际上是发送了 Content-Type 为 application/x-www-form-urlencoded 的 POST 请求,这种对应的就是表单提交。这两种 POST 请求的发送方式我们需要区分清楚,并根据服务器的实际需要进行选择。
example.py 修正如下:
import scrapy
from ..items import DemoItemclass ExampleSpider(scrapy.Spider):name = "example"allowed_domains = ["quotes.toscrape.com"]start_urls = ["https://quotes.toscrape.com/"]def parse(self, response, **kwargs):quotes = response.css(".quote")for quote in quotes:item = DemoItem()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)
运行项目
scrapy crawl example
在运行完 Scrapy 后,只能在控制台上看到结果,需要保存数据,有两种方式:
其一是使用命令行,直接输出格式文件,例如 json, csv, xmlk, pickle, marshal 等等,完成这一任务不需要任何额外的代码,Scrapy 提供的 Feed Exports 可以轻松将抓取到的结果输出
# 保存为json
scrapy crawl example -o example.json
# 保存为一行json
scrapy crawl example -o example.jl # 或
scrapy crawl example -o example.jsonlines
# 保存为 csv
scrapy crawl example -o example.csv
# 保存为 xml
scrapy crawl example -o example.xml
# 保存为 pickle
scrapy crawl example -o example.pickle
# 保存为 marshal
scrapy crawl example -o example.marshal
其二是使用 Item Pipeline,如果要进行更复杂的操作,如将结果保存到数据库之中或者 对 Item 进行筛选操作;Item Pipeline 为项目管道,当 Item 生成后,它会自动被送到 Item Pipeline 处进行处理,可以使用 Item Pipeline 来做如下操作:
- 清洗 HTML 数据
- 验证爬取数据,检测爬取字段
- 查重并丢弃重复内容
- 将爬取结果保存到数据库
Pipeline 管道的基本类模版如下:
class XXXXPipeline(object):def __init__(self, a, b):self.a = aself.b = bdef process_item(self, item, spider):"""必须有!为每个项管道组件调用此方法"""pass@classmethoddef from_crawler(cls, crawler):"""如果存在,则调用此类方法以从Crawler创建管道实例。它必须返回管道的新实例。Crawler对象提供对所有Scrapy核心组件(如setting和signal)的访问;它是管道访问它们并将其功能挂钩到Scrapy的一种方式。类似于初始化a和b"""return cls(a=crawler.settings.get("a"),b=crawler.settings.get("b"),)def open_spider(self, spider):"""如果存在,这个方法是在spider打开时调用的。"""passdef close_spider(self, spider):"""如果存在,这个方法是在spider关闭时调用的。"""pass
在这里我们可以添加两个 Pipeline,首先是文本处理的 Pipeline ,还有存储数据库的 Pipeline;
from scrapy.exceptions import DropItemclass TextPipeline(object):def __init__(self):self.limit = 50def process_item(self, item, spider):if item["text"]:if len(item["text"]) > self.limit:item["text"] = item["text"][:self.limit].rstrip() + "..."return itemelse:return DropItem("Missing Text")class MongoPipeline(object):def __init__(self, connection_string, database):self.connection_string = connection_stringself.database = database@classmethoddef 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 itemdef close_spider(self, spider):self.client.close()
处理完毕后,我们还需要进入到 settings.py 中配置文件,第一个是 Mongo数据库的配置,由于 MongoPipeline 是使用 from_crawler 来进行初始化的,所以 settings.py 中需要有 MONGODB_CONNECTION_STRING
,MONGODB_DATABASE
这两个字段;其次 Pipeline 有一个先后顺序,键值越小越优先执行,修改 settings.py 内容如下:
# Crawl responsibly by identifying yourself (and your website) on the user-agent
# USER_AGENT = "demo (+http://www.yourdomain.com)"# Obey robots.txt rules
ROBOTSTXT_OBEY = TrueITEM_PIPELINES = {"爬虫项目名.pipelines.TextPipeline": 200,"爬虫项目名.pipelines.MongoPipeline": 300,
}
MONGODB_CONNECTION_STRING = "localhost"
MONGODB_DATABASE = "数据库名"
到这里就处理完毕了!开启爬虫如下:
scrapy crawl example
二、Selector 解析器
在Python3网络爬虫开发实战(3)网页数据的解析提取_etree beautifulsoup parsel-CSDN博客介绍过 Parsel 解析器,parsel 是 Python 最流行的爬虫框架 Scrapy 的底层支持;
而 Selector 在使用上和 Parsel 有一点点区别,那就是原来的 get() 和 getall() 变成了 extract_first() 和 extract();同时,Selector 是可以单独使用的
2.1 XPath 和 CSS 选择器
from scrapy import Selectorhtml = ''
selector = Selector(text=html)# css
items = selector.css('css选择器')
# xpath
items = selector.xpath('xpath选择器')
2.2 信息提取
- extract_first:从 selectorlist 对象中提取第一个 Selector 对象,然后输出其中的结果
- extract:从 selectorlist 对象中提取所有的 Selector 对象,然后以列表的形式输出其中的结果
# 提取文本
selector.css('css选择器::text()').extract_first("默认值")
selector.css('css选择器::text()').extract("默认值")
selector.xpath('xpath//text()').extract_first("默认值")
selector.xpath('xpath//text()').extract("默认值")# 提取属性
selector.css('css选择器::attr(name)').extract_first("默认值")
selector.css('css选择器::attr(href)').extract("默认值")
selector.xpath('xpath/@name()').extract_first("默认值")
selector.xpath('xpath/@href()').extract("默认值")
2.3 正则提取
- 如果选择器中是属性或者文本,那么 re 对属性或者文本进行匹配
- 如果选择器中不是属性和文本,那么 re 对该节点的 html 字符进行匹配
from parsel import Selectorhtml = ''
selector = Selector(text=html)
result = selector.css('css选择器').re('a.*')
result = selector.xpath('xpath').re('a.*')result = selector.css('css选择器').re_first('a.*')
result = selector.xpath('xpath').re_first('a.*')
三、Spider 的使用
在 Scrapy 中,网站的链接配置,抓取逻辑,解析逻辑其实都是在 Spider 中配置的,在前一节的实例中,我们发现抓取逻辑也是在 Spider 中完成的。
3.1 Spider 运行流程
Spider 定义了如何爬取某个网站的流程和解析方式,就是做了以下两件事:
- 定义爬取网站的动作
- 分析爬取下来的网页
对于 Spider 类来说,整个爬取循环如下:
- 以初始的 URL 初始化 Request 并设置回调方法,当该 Request 成功请求并返回时,将生成 Response 并将其作为参数传给该回调方法
- 在回调方法内分析返回的网页内容。返回结果可以有两种形式,一种是将解析到的有效结果返回字典或 Item 对象,下一步可直接保存或者经过处理后保存,另一种解析的下一个(如下下一页)链接,可以利用此链接构造 Request 并设置新的回调方法,返回 Request;
- 如果返回的是字典或者 Item 对象,可通过 Feed Exports 等形式存入文件,如果设置了 Pipeline,可以经由 Pipeline 处理(如过滤,修正等)并保存;
- 如果返回的是 Request,那么 Request 执行成功得到 Response 之后会再次传递给 Request 中定义的回调方法,可以再次使用选择器来分析新得到的网页内容,并根据分析的数据生成 Item;
循环进行以上几步,便完成了站点的爬取;
3.2 Spider 类分析
参考文档:Spiders - Scrapy 2.11.2文档 — Spiders — Scrapy 2.11.2 documentation
我们定义的 Spider 继承自 scrapy.Spider 类,这个类是最基本的 Spider 类,其他的 Spider 必须继承这个类;
这个类有一些基础的属性,如下:
- name:爬虫名称,是定义 Spider 名字的字符串,Spider 的名字定义了 Scrapy 如何定位并初始化 Spider,所以它必须是唯一的。 name 是 Spider 最重要的属性,而且是必须的;
- allowed_domains:允许爬取的域名,是一个可选的配置,不在此范围的链接不会被跟进爬取;
- start_urls:起始 URL 列表,当我们没有实现 start_requests 方法的,默认会从这个列表开始抓取;
- custom_settings:一个字典,是专属于本 Spider 的配置,此设置会覆盖项目全局的设置,而且此设置必须在初始化前被更新,所以它必须定义成类变量;Settings — Scrapy 2.11.2 documentation
- crawler:此属性是由 from_crawler 方法设置的,代表的是本 Spider 类对应的 Crawler 对象,Crawler 对象中包含了很多的项目组件,利用它可以获取一些项目的基本配置信息,常见的就是获取项目的设置信息,即 Settings;Core API — Scrapy 2.11.2 documentation
- settings:一个 Settings 对象,利用它我们可以直接获取项目的全局设置变量;Settings — Scrapy 2.11.2 documentation
还有一些基础的,主要的方法,如下:
- start_requests:此方法用于生成初始请求,它必须返回一个可迭代对象,此方法会默认使用 start_urls 里面的每个 URL 来构造 Request,而且 Request 是 GET 请求方式。如果我们想在启动的时候以 POST 方式访问某个站点,可以直接重写这个方法;
- parse:当 Response 没有指定回调方法时,该方法会默认被调用,它负责处理 Response,并从中提取想要的数据和下一步的请求,然后返回,该方法需要返回一个包含 Request 或 Item 的可迭代对象;
- closed:当 Spider 关闭时,该方法被调用,这里一般会定义释放资源的一些操作;
3.3 Request
Requests and Responses — Scrapy 2.11.2 documentation
在 Request 中,Request 对象实质上指的就是 scrapy.http.Request 的一个实例,它包含了 HTTP 请求的基本信息,用这个 Request 类可以构造 Request 对象发送 HTTP 请求,它会被 Engine 交给 Downloader 进行处理执行,返回一个 Response 对象;
scrapy.Requset(**kwargs)
scrapy.http.Requset(**kwargs)# Content-Type 为 application/json
scrapy.JsonRequest(**kwargs)
scrapy.http.JsonRequest(**kwargs)# Content-Type 为 application/x-www-form-urlencoded
scrapy.FormRequest(**kwargs)
scrapy.http.FormRequest(**kwargs)
Request 类的构造参数如下:
- url:Request 的页面链接,即 Request URL;
- callback:Request 的回调方法,通常这个方法需要定义在 Spider 类里面,并且需要对应一个 response 参数,代表 Request 执行请求后得到的 Response 对象,如果这个 callback 参数不指定,默认会使用 Spider 类里面的 parse 方法;
- method:Request 的方法,默认是 GET,还可以设置为 POST,PUT,DELETE 等;
- meta:Request 请求携带的额外参数,利用 meta ,我们可以指定任意处理参数,特定的参数经由 Scrapy 各个组件的处理,可以得到不同的效果,另外,meta 还可以用来向回调方法传递信息;
- body:Request 的内容,即 Request Body,往往 Request Body 对应的是 POST 请求,我们可以使用 FormRequest 或 JsonRequest 更方便地实现 POST 请求;
- headers:Request Header,是字典形式;
- cookies:Request 携带的 Cookie,可以是字典或者列表形式;
- encoding:Request 的编码,默认是 UTF-8
- prority:Request 优先级,默认是 0 ,这个优先级是给 Scheduler 做 Request 调度使用的,数值越大,就粤北优先调用执行;
- dont_filter :Request 不去重,Scrapy 默认会根据 Request 的信息进行去重,使得在爬取过程中不会出现重复请求,设置为 True 代表这个 Request 会被忽略去重操作,默认为 False;
- errback:错误处理方法,如果在请求过程中出现了错误,这个方法就会被调用;
- flags:请求的标志,可以用于记录类似的处理;
- cb_kwargs:回调方法的额外参数,可以作为字典传递;
值得注意的是,meta 参数是一个十分有用而且易扩展的参数,它可以以字典的形式传递,包含的信息不受限制,所以很多 Scrapy 的插件会基于 meta 参数做一些特殊处理,在默认情况下,Scrapy 就预留了一些特殊的 key 作为特殊处理;
Scrapy 还专门为 POST 请求提供了两个类 —— FormRequest 和 JsonRequest,它们都是 Request 类的子类,我们可以利用 FormRequest 的 formdata 参数传递表单内容,利用 JsonRequest 的 json 参数传递 JSON 内容,其他的参数和 Request 基本是一致的。
第一个 JsonRequest,我们可以观察到页面返回结果的 json 字段就是我们所请求时添加的 data 内容,这说明实际上是发送了 Content-Type 为 application/json 的 POST 请求,这种对应的就是发送 JSON 数据。第二个 FormRequest,我们可以观察到页面返回结果的 form 字段就是我们请求时添加的 data 内容,这说明实际上是发送了 Content-Type 为 application/x-www-form-urlencoded 的 POST 请求,这种对应的就是表单提交。这两种 POST 请求的发送方式我们需要区分清楚,并根据服务器的实际需要进行选择。
3.4 Response
Request 由 Downloader 执行之后,得到的就是 Response 结果了,它代表的是 HTTP 请求得到的响应结果,同样地我们可以梳理一下其可用的属性和方法,以便做解析处理使用
- url:Request URL;
- request:Response 对应的 Request 对象;
- status:Response 状态码;
- headers:Response Header,响应头;是一个字典,字段是一一对应的;
- body:Response Body,这个通常就是访问页面之后得到的源码结果了,比如里面包含的是 HTML 或者 JSON 字符串,但注意其结果是 bytes 类型;
- certificate:是 twisted.internet.ssl.Certificate 类型的对象,通常代表一个 SSL 证书对象;
- ip_address,是一个 ipaddress.IPv4Address 或 ipaddress.IPv6Address 类型的对象,代表服务器的 IP 地址;
- urljoin:是对 URL 的一个处理方法,可以传入当前页面的相对 URL,该方法处理后返回的就是绝对 URL;
- follow/follow_all:是一个根据 URL 来生成后续 Request 的方法,和直接构造 Request 不同的是,该方法接受的 url 可以是相对 URL,不必一定是绝对 URL;
另外,Response 还有几个常用的子类,如 TextResponse 和 HtmlResponse;HtmlResponse 又是 TextResponse 的子类,实际上回调方法接收的 response 参数就是一个 HtmlResponse 对象,它还有几个常用的方法或属性。
- text:同 body 属性,但结果是 str 类型;
- encoding:Response 的编码,默认是 utf-8;
- selector:根据 Response 的内容构造而成的 Selector 对象;
- xpath/css :等同于调用 selector.xpath/css 方法;
- json:可以将 text 属性转化为 JSON 对象;
四、Download Middleware 的使用
Downloader Middleware 是处于 Scrapy 的 Engine 和 Downloader 之间的处理模块。Engine 把 Scheduler 获取的 Request 发送给 Downloader 的过程中,以及 Downloader 把 Response 发送回 Engine 的过程中,Request 和 Response 都会经过 Downloader Middleware 的处理;也就是说 Downloader Middleware 在整个架构中起作用的位置是以下两个:
- Engine 从 Scheduler 获取 Request 发送给 Downloader Middleware,在 Request 被 Engine 发送给 Downloader Middleware 执行下载之前,Downloader Middleware 可以对 Request 进行修改;
- Downloader 执行 Request 后生成 Response,在 Response 被 Engine 发送给 Spider 之前,Downloader Middleware 可以对 Response 进行修改;
Downloader Middleware 在整个爬虫执行过程中能起到非常重要的作用,功能十分强大,修改 User-Agent,处理重定向,设置代理,失败重试,设置 Cookie 等功能都需要借助它来实现;
需要说明的是,Scrapy 其实已经提供了许多 Downloader Middleware,比如负责失败重试、自动重定向等功能的 Middleware,它们被 DOWNLOADER_MIDDLEWARES_BASE 变量所定义。 DOWNLOADER_MIDDLEWARES_BASE 变量的内容如下所示:
{'scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware': 100,'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware': 300,'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware': 350,'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware': 400,'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 500,'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550,'scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware': 560,'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware': 580,'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 590,'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': 600,'scrapy.downloadermiddlewares.cookies.CookiesMiddleware': 700,'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 750,'scrapy.downloadermiddlewares.stats.DownloaderStats': 850,'scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware': 900,
}
字典的键名是 Scrapy 内置的 Downloader Middleware 的名称,键值代表了调用的优先级,优先级是一个数字,数字越小代表越靠近Engine,数字越大代表越靠近 Downloader 。默认情况下,Scrapy 已经为我们开启了 DOWNLOADER_MIDDLEWARES_BASE 所定义的 Downloader Middleware,比如 RetryMiddleware 带有自动重试功能,RedirectMiddleware 带有自动处理重定向功能,这些功能默认都是开启的。
Downloader Middleware 固定内部代码如下:
class ScrapyDemoDownloaderMiddleware:# Not all methods need to be defined. If a method is not defined,# scrapy acts as if the downloader middleware does not modify the# passed objects.@classmethoddef from_crawler(cls, crawler):# This method is used by Scrapy to create your spiders.s = cls()crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)return sdef process_request(self, request, spider):# Called for each request that goes through the downloader# middleware.# Must either:# - return None: continue processing this request# - or return a Response object# - or return a Request object# - or raise IgnoreRequest: process_exception() methods of# installed downloader middleware will be calledreturn Nonedef process_response(self, request, response, spider):# Called with the response returned from the downloader.# Must either;# - return a Response object# - return a Request object# - or raise IgnoreRequestreturn responsedef process_exception(self, request, exception, spider):# Called when a download handler or a process_request()# (from other downloader middleware) raises an exception.# Must either:# - return None: continue processing this exception# - return a Response object: stops process_exception() chain# - return a Request object: stops process_exception() chainpassdef spider_opened(self, spider):spider.logger.info('Spider opened: %s' % spider.name)
每个 Downloader Middleware 都可以通过定义 process_request 和 process_reponse 方法来分别处理 Request 和 Response ,被开启的 Downloader Middleware 的 process_request 方法和 process_response 方法会根据优先级顺序调用。
process_request:由于Request是从Engine发送给Downloader的,并且优先级数字越小的Downloader Middleware越靠近Engine,所以优先级数字越小的Downloader Middleware的process_request方法越先被调用。
process_response:process_response方法则相反,由于Response是由Downloader发送给Engine的,优先级数字越大的Downloader Middleware越靠近Downloader,所以优先级数字越大的Downloader Middleware的process_response越先被调用。
如果我们想将自定义的Downloader Middleware添加到项目中,不要直接修改DOWNLOADER_MIDDLEWARES_BASE变量,Scrapy提供了另外一个设置变量DOWNLOADER_MIDDLEWARES,我们直接修改这个变量就可以添加自己定义的Downloader Middleware,以及禁用DOWNLOADER_MIDDLEWARES_BASE里面定义的Downloader Middleware了。
4.1 process_request(request, spider)
Request被Engine发送给Downloader之前,process_request方法就会被调用,也就是在Request从Scheduler里被调度出来发送到Downloader下载执行之前,我们都可以用process_request方法对Request进行处理。
参数
process_request方法的参数有两个。
- request:Request对象,即被处理的Request。
- spider:Spider对象,即此Request对应的Spider对象。
返回值
这个方法的返回值必须为None、Response对象、Request对象三者之一,或者抛出IgnoreRequest异常。返回类型不同,产生的效果也不同,下面归纳一下不同的返回情况。
- None:当返回的是None时,Scrapy将继续处理该Request,接着执行其他Downloader Middleware的process_request方法,一直到Downloader把Request执行得到Response才结束。这个过程其实就是修改Request的过程,不同的Downloader Middleware按照设置的优先级顺序依次对Request进行修改,最后送至Downloader执行。
- Response:当返回为Response对象时,更低优先级的Downloader Middleware的process_request和process_exception方法就不会被继续调用,每个Downloader Middleware的process_response方法转而被依次调用,调用完毕后,直接将Response对象发送给Spider处理。
- Request:当返回为Request对象时,更低优先级的Downloader Middleware的process_request方法会停止执行。这个Request会重新放到调度队列里,其实它就是一个全新的Request,等待被调度。如果Scheduler调度了,那么所有的Downloader Middleware的process_request方法会被重新按照顺序执行。
- IgnoreRequest:如果抛出IgnoreRequest异常,则所有的Downloader Middleware的process_exception方法会依次执行。如果没有一个方法处理这个异常,那么Request的errorback方法就会回调。如果该异常还没有被处理,那么它便会被忽略。
4.2 process_response(request, response, spider)
Downloader执行Request下载之后,会得到对应的Response。Engine便会将Response发送给Spider进行解析,在发送给Spider之前,我们都可以用process_response方法来对Response进行处理。
参数
process_response方法的参数有3个:
- request:Request对象,即此Response对应的Request。
- response:Response对象,即被处理的Response。
- spider:Spider对象,即此Response对应的Spider对象。
返回值
process_response方法的返回值必须为Request对象和Response对象两者之一。或者抛出IgnoreRequest异常。那么对不同的返回情况在下面做一下归纳。
- Request:当返回为Request对象时,更低优先级的Downloader Middleware的process_response方法不会继续调用,该Request对象会重新放到调度队列里等待被调度,相当于一个全新的Request。然后,该Request会被process_request方法顺次处理。
- Response:当返回为Response对象时,更低优先级的Downloader Middleware的process_response方法会继续被调用,对该Response对象进行处理。
- IgnoreRequest:当抛出IgnoreRequest异常时,Request的errorback方法会回调。如果该异常还没有被处理,那么它会被忽略。
4.3 process_exception(request, exception, spider)
当Downloader或process_request方法抛出异常时,例如抛出IgnoreRequest异常,process_exception方法就会被调用。
参数
process_exception方法的参数有3个。
- request:Request对象,即产生异常的Request。
- exception:Exception对象,即抛出的异常。
- spider:Spider对象,即Request对应的Spider。
返回值
方法的返回值必须为None、Response对象、Request对象三者之一。
- None:当返回值为None时,更低优先级的Downloader Middleware的process_exception会被继续顺次调用,直到所有的方法都被调用完毕。
- Response:当返回值为Response时,更低优先级的Downloader Middleware的process_exception不再被继续调用,每个Downloader Middleware的process_response方法转而被依次调用。
- Request:当返回为Request对象时,更低优先级的Downloader Middleware的process_exception也不再被继续调用,该Request对象会重新放到调度队列里面等待被调度,相当于一个全新的Request。然后,该Request又会被process_request方法顺次处理。
关于设置 header,设置代理,返回值等操作可以看:scrapy爬虫框架(四)Downloader Middleware的使用 - 乐之之 - 博客园 (cnblogs.com)
五、Spider Middleware 的使用
Spider Middleware 的作用:
- Downloader生成Reponse之后,Engine会将其发送给Spider进行解析,在Response发送给Spider之前,可以借助Spider Middleware对Response进行处理。
- Spider生成Request之后会被发送至Engine,然后Request会转发到Scheduler,在Request被发送给Engine之前,可以借助Spider Middleware对Request进行处理。
- Spider生成Item之后会被发送至Engine,然后Item会被转发到Item Pipeline,在Item被发送给Engine之前,可以借助Spider Middleware对Item进行处理。
Scrapy框架中其实已经提供了许多Spider Middleware,与Downloader Middleware类似,它们被SPIDER_MIDDLEWARES_BASE变量所定义;SPIDER_MIDDLEWARES_BASE变量的内容如下:
{'scrapy.spidermiddlewares.httperror.HttpErrorMiddleware":50,'scrapy.spidermiddlewares.offsite.OffsiteMiddleware':500,'scrapy.spidermiddlewares.referer.RefererMiddleware':700,'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware': 800,' scrapy.spidermiddlewares.depth.DepthMiddleware':900,
}
SPIDER_MIDDLEWARES_BASE里定义的Spider Middleware是默认生效的,如果我们要自定义Spider Middleware,可以和Downloader Middleware一样,创建Spider Middleware并将其加入SPIDER_MIDDLEWARES。直接修改这个变量就可以添加自己定义的Spider Middleware,以及禁用SPIDER_MIDDLEWARES_BASE里面定义的Spider Middleware。
这些Spider Middleware的调用优先级和Downloader Middleware也是类似的,数字越小的Spider Middleware是越靠近Engine的,数字越大的Spider Middleware是越靠近Spider的。
5.1 process_spider_input(response, spider)
当Response通过Spider Middleware时,process_spider_input方法被调用,处理该Response。它有两个参数。
参数
- response:Response对象,即被处理的Response。
- spider:Spider对象,即该Response对应的Spider对象。
返回值
process_spider_input应该返回None或者抛出一个异常。
- None:如果它返回None,Scrapy会继续处理该Response,调用所有其他的Spider Middleware直到Spider处理该Response。
- 异常:如果它抛出一个异常,Scarapy不会调用任何其他Spider Middleware的process_spider_input方法,并调用Request的errback方法。errback的输出将会以另一个方向被重新输入中间件,使用process_spider_output处理,当其抛出异常时则调用process_spider_exception来处理。
5.2 process_spider_output(response, result, spider)
当Spider处理Response返回结果时,process_spider_output方法被调用。它有3个参数。
参数
- response:Response对象,即生成该输出的Response。
- result:包含Request或Item对象的可迭代对象,即Spider返回的结果。
- spider:Spider对象,即结果对应的Spider对象。
返回值
process_spider_output必须返回包含Request或Item对象的可迭代对象。
5.3 process_spider_exception(response, exception, spider)
当Spider或Spider Middleware的process_spider_input方法抛出异常时,process_spider_exception方法被调用。它有3个参数。
参数
- response:Response对象,即异常被抛出时被处理的Response。
- exception:Exception对象,被抛出的异常。
- spider:Spider对象,即抛出该异常的Spider对象。
返回值
process_spider_exception必须必须返回None或者一个(包含Response或Item对象的)可迭代对象。
- None:如果它返回None,那么Scrapy将继续处理该异常,调用其他Spider Middleware中process_spider_exception方法,直到所有Spider Middleware都被调用。
- 可迭代对象(Response或Item):如果它返回的是一个可迭代对象,则其他Spider Middleware的process_spider_output方法被调用,其他的process_spider_exception不会被调用。
5.4 process_start_requests(start_requests, spider)
process_start_requests方法以Spider启动的Request为参数被调用,执行的过程类似于process_spider_output,只不过它没有相关联的Response并且必须返回Request。它有两个参数。
参数
- process_start_requests:包含Request的可迭代对象,即Start Requests。
- spider:Spider对象,即Start_Reqeusts所属的Spider。
返回值
process_start_requests方法必须返回另一个包含Request对象的可迭代对象。
5.5 内置Spider Middleware 简介
在这里我们再介绍一些scrapy框架中内置的Spider Middleware。
HttpErrorMiddleware:HttpErrorMiddleware的主要作用是过滤我们需要忽略的Response,比如状态码为200~299的会处理,500以上的不会处理。
另外,如果想要针对一些错误类型的状态码进行处理,可以修改Spider的 handle_httpstatus_list属性,也可以修改 Request meta 的 handle_httpstatus_list 属性,还可以修改全局 setttings中的HTTPERROR_ALLOWED_CODES。
OffsiteMiddleware:OffsiteMiddleware 的主要作用是过滤不符合 allowed_domains 的 Request,Spider 里面定义的allowed_domains其实就是在这个Spider Middleware 里生效的。
OffsiteMiddleware 首先遍历了 result,然后根据 dont_filter、url 和 Spider 的 allowed_domains 进行了过滤,如果不符合 allowed domains,就直接输出日志并不再返回 Request,只有符合要求的Request才会被返回并继续调用。
UrlLengthMiddleware:UrlLengthMiddleware 的主要作用是根据 Request 的URL长度对 Request 进行过滤,如果URL的长度过长,此Request就会被忽略。
UrlLengthMiddleware 利用了 process_spider_output 对 result 里面的 Request 进行过滤,如果是Request 类型并且 URL 长度超过最大限制,就会被过滤。我们可以从中了解到,如果想要根据URL的长度进行过滤,可以设置URLLENGTH LIMIT。
其详细介绍和使用可以看:scrapy爬虫框架(五)Spider Middleware - 乐之之 - 博客园 (cnblogs.com)
六、Item Pipeline 的使用
Item Pipeline即项目管道,它的调用发生在Spider产生Item之后。当Spider解析完Response,Item就会被Engine传递到Item Pipeline,被定义的Item Pipeline组件会顺次被调用,完成一连串的处理过程,比如数据清洗、存储等。
Item Pipeline的主要功能如下:
- 清洗HTML数据。
- 验证爬取数据,检查爬取字段。
- 查重并丢弃重复内容。
- 将爬取结果存储到数据库中。
6.1 process_item(item, spider)
process_item是必须实现的方法,被定义的Item Pipeline会默认调用这个方法对Item进行处理,比如进行数据处理或者将数据写入数据库等操作。
参数
process_item方法的参数有两个。
- item:Item对象,即被处理的Item。
- spider:Spider对象,即生成该Item的Spider。
返回值
process_item方法必须返回Item类型的值或者抛出一个DropItem异常。该方法的返回类型如下:
- Item:如果返回的是Item对象,那么此Item会接着被低优先级的Item Pipeline的process_item方法处理,直到所有的方法被调用完毕。
- DropItem异常:如果抛出DropItem异常,那么此Item就会被丢弃,不再进行处理。
6.2 open_spider(self, spider)
open_spider方法是在Spider开启的时候被自动调用的,在这里,我们可以做一些收尾工作,如关闭数据库连接等。其中参数spider就是被开启的Spider对象。
6.3 close_spider(spider)
close_spider方法是在Spider关闭的时候自动调用,在这里,我们可以做一些收尾工作,如关闭数据库连接等,其中参数spider就是被关闭的Spider对象。
6.4 from_crawler(cls, crawler)
from_crawler方法是一个类方法,用@classmethod标识,它接受一个参数crawler。通过crawler对象,我们可以拿到Scrapy的所有核心组件,如全局配置的每个信息。然后可以在这个方法里面创建一个Pipeline实例。参数cls就是Class,最后返回一个Class实例。
其对数据库详细的使用可以看这篇:scrapy爬虫框架(六)Item Pipeline的使用 - 乐之之 - 博客园 (cnblogs.com)
七、Extension 的使用
Scrapy提供了一个Extension机制,可以让我们添加和扩展一些自定义的功能。利用Extension我们可以注册一些处理方法并监听Scrapy运行过程中的各个信号,做到发生某个事件时执行我们自定义的方法。
Scrapy已经内置了一些Extension,如 LogStats 这个 Extension 用于记录一些基本的爬取信息,比如爬取的页面数量、提取的Item数量等。 CoreStats 这个 Extension 用于统计爬取过程中的核心统计信息,如开始爬取时间、爬取结束时间等。
和 Downloader Middleware、Spider Middleware 以及 Item Pipeline 一样,Extension 也是通过settings.py 中的配置来控制是否被启用的,是通过 EXTESION 这个配置项来实现的,例如:
EXTENSIONS={scrapy.extensions.corestats.Corestats': 500',scrapy.extensions.telnet.TelnetConsole': 501,
}
通过如上配置我们就开启了 CoreStats 和 TelnetConsole 这两个 Extension。另外我们也可以实现自定义的Extension,实现过程其实很简单,主要分为两步:
- 实现一个 Python 类,然后实现对应的处理方法,如实现一个 spider_opened 方法用于处理 Spider 开始爬取时执行的操作,可以接收一个spider参数并对其进行操作。
- 定义 from_crawler 类方法,其第一个参数是cls类对象,第二个参数是 crawler。利用 crawler 的 signals 对象将 Scrapy 的各个信号和已经定义的处理方法关联起来。
我们来尝试利用Extension实现爬取事件的消息通知。在爬取开始时、爬取到数据时、爬取结束时通知指定的服务器,将这些事件和对应的数据通过HTTP请求发送给服务器。
本节通过上节Item Pipeline的代码进行演示,主要内容如下:
import scrapy
from testItemPipeline.items import TestitempipelineItemclass MovieSpiderSpider(scrapy.Spider):name = 'movie_spider'allowed_domains = ['ssr1.scrape.center']start_url = 'http://ssr1.scrape.center'def start_requests(self):for i in range(1,11):url=self.start_url+f'/page/{i}'yield scrapy.Request(url=url,callback=self.parse_index)def parse_index(self,response):data_list = response.xpath('//div[@class="el-col el-col-18 el-col-offset-3"]//div[@class="el-card item m-t is-hover-shadow"]')for item in data_list:href = item.xpath('./div/div/div[1]/a/@href').extract_first()url = response.urljoin(href)yield scrapy.Request(url=url,callback=self.parse_detail)def parse_detail(self, response):item = TestitempipelineItem()item["name"] = response.xpath('//div[@class="el-card__body"]/div[@class="item el-row"]/div[@class="p-h el-col el-col-24 el-col-xs-16 el-col-sm-12"]/a/h2/text()').extract_first()item["categories"] = ','.join(response.xpath('//div[@class="el-card__body"]/div[@class="item el-row"]/div[@class="p-h el-col el-col-24 el-col-xs-16 el-col-sm-12"]/div[@class="categories"]/button/span/text()').extract())item["score"] = ''.join(response.xpath('//div[@class="el-card__body"]/div[@class="item el-row"]/div[@class="el-col el-col-24 el-col-xs-8 el-col-sm-4"]/p/text()').extract_first()).replace("\n","").replace(" ","")item["drama"] = ''.join(response.xpath('//div[@class="el-card__body"]/div[@class="item el-row"]/div[@class="p-h el-col el-col-24 el-col-xs-16 el-col-sm-12"]/div[@class="drama"]/p/text()').extract_first()).replace("\n","")item["directors"] = []dd = response.xpath('//div[@class="el-col el-col-18 el-col-offset-3"]//div[@class="directors el-row"]')for data in dd:directors_name = data.xpath('./div[@class="director el-col el-col-4"]/div[@class="el-card is-hover-shadow"]/div[@class="el-card__body"]/p/text()').extract_first()directors_image = data.xpath('./div[@class="director el-col el-col-4"]/div[@class="el-card is-hover-shadow"]/div[@class="el-card__body"]/img/@src').extract_first()item["directors"].append({'name': directors_name,'image': directors_image})item["actors"] = []ss = response.xpath('//div[@class="actors el-row"]//div[@class="actor el-col el-col-4"]')for data in ss:actors_image = ''.join(data.xpath('./div/div/img/@src').extract_first())actors_name = ''.join(data.xpath('./div/div/p/text()').extract_first())item["actors"].append({"name": actors_name,"image": actors_image})yield item
另外本节我们需要用到Flask来搭建一个简易的测试服务器,也需要利用requests来实现HTTP请求的发送,因此需要安装好Flask、requests和loguru这3个库,使用pip安装即可:
pip install flask requests loguru
7.1 部署本地 flask 服务器
为了方便验证,我们用Flask定义一个轻量级的服务器,用于接收POST请求并输出接收到的事件和数据,server.py的代码如下:
7.2 extensions.py
在testItemPipeline文件夹下新建一个extensions.py文件。
注意:在新建的文件夹一定要和其他组件是同一级别目录,如Spider、Item等。
接下来我们先实现几个对应的事件处理方法:
这里我们定义了一个NotificationExtension类,然后实现了3个方法,spider_opened、spider_closed和item_scraped,分别对应爬取开始、爬取结束和爬取到Item 的处理。接着调用了 requests 向刚才我们搭建的 HTTP 服务器发送了对应的事件,其中包含两个字段:一个是 event,代表事件的名称;另一个是 data,代表一些附加数据,如 Spider的名称、Item的具体内容等。
但仅仅这么定义其实还不够,现在启用这个Extension其实没有任何效果的,我们还需要将这些方法和对应的Scrapy信号关联起来,再在NotificationExtension类中添加如下类方法:
添加方法前可以先导入一下Scrapy中的signals对象:
from scrapy import signals
其中,from crawler 是一个类方法,第一个参数就是 cls 类对象,第二个参数 crawler 代表了Scrapy运行过程中全局的Crawler对象。
Crawler对象里有一个子对象叫作signals,通过调用signals对象的connect方法,我们可以将Scrapy运行过程中的某个信号和我们自定义的处理方法关联起来。这样在某个事件发生的时候,被关联的处理方法就会被调用。比如这里,connect方法第一个参数我们传入ext.spider_opened这个对象而ext是由cls类对象初始化的,所以ext.spider_opened就代表我们在NotificationExtension类中定义的spider_opened方法。connect方法的第二个参数我们传入了signals.spider_opened这个对象这就指定了spider_opened 方法可以被spider_opened信号触发。这样在Spider 开始运行的时候会产生signals.spider_opened信号,NotificationExtension类中定义的spider_opened方法就会被调用了。
完成如上定义之后,我们还需要开启这个Extension,在settings.py中添加如下内容即可。
我们成功启用了NotificationExtension这个Extension。下面我们来运行一下movie_spider:
scrapy crawl movie_spider
这时候爬取结果和Item Pipeline的使用这节的内容大致一样,不同的是日志中多了类似如下的几行:
有了这样的日志,说明成功调用了requests的post方法完成了对服务器的请求。
这时候我们回到Flask服务器,看一下控制台的输出结果:
可以看到Flask服务器成功接收到了各个事件(SPIDER OPENED、ITEM SCRAPED、SPIDEROPENED)并输出了对应的数据,这说明在 Scrapy 爬取过程中,成功调用了 Extension 并在适当的时机将数据发送到服务器了,验证成功!
我们通过一个自定义的 Extension,成功实现了 Scrapy 爬取过程中和远程服务器的通信,远程服务器收到这些事件之后就可以对事件和数据做进一步的处理了。
本节通过一个Extension的样例体会到了Extension强大又灵活的功能,以后我们想实现一些自定义的功能可以借助于Extension来实现了。而对于整个scrapy框架基础到这里也就结束了,后面对于一些不理解的地方一定要仔细琢磨认真观察,多练多思考。
八、Scrapy 自动化配置
8.1 Scrapy 对接 Splash
要实现 Scrapy 和 Splash 的对接,我们需要借助于 Scrapy-Splash 库,另外还需要一个可以正常使用的 Splash 服务;
Splash 本身就是一个 JavaScript 页面渲染服务,我们只需要将需要渲染页面的 URL 发送给 Splash 就能得到对应的 JavaScript 渲染结果,而 Scrapy-Splash 则是提供了这个过程基本功能的封装,比如 Cookie 的处理,URL 的转换等;
首先新建一个项目,名为 scrapysplashdemo,命令如下:
scrapy startproject scrapysplashdemo
进入项目,新建一个 Spider,命令如下:
scrapy genspider book spa5.scrape.center
这样便创建了初始的 Spider,然后创建一个同样的 BookItem,代码如下:
# items.py
from scrapy.item import Item, Fieldclass BookItem(Item):name = Field()tags = Field()score = Field()cover = Field()price = Field()
接下来就需要进行 Scrapy-Splash 相关的配置,可以参考 Scrapy-Splash 的配置说明:scrapy-plugins/scrapy-splash: Scrapy+Splash for JavaScript integration (github.com)
配置完毕后,可以利用 Splash 来抓取页面,可以直接生成一个 SplashRequest 对象并传递相应的参数,Scrapy 会将此请求转发给 Splash,Splash 对页面进行渲染加载,再将渲染结果传递回来。此时 Response 的内容就是渲染完成的结果了,最后交给 Spider 解析即可;
yield SplashRequest(url, self.parse_result,args = {'wait': 0.5, # 等待时间},endpoint = "render.json", # 可选参数,Splash 渲染终端splash_url = "<url>", # 可选参数,覆盖 SPLASH_URL)
这里构造了一个 SplashRequest 对象,前两个参数依然是请求的URL和回调函数。另外我们还可以通过 args 传递一些渲染参数,例如等待时间 wait 等,还可以根据 endpoint 参数指定渲染接口。更多参数可以参考文档说明:scrapy-plugins/scrapy-splash: Scrapy+Splash for JavaScript integration (github.com)
另外我们也可以生成 Request 对象,Splash 的配置通过 meta 属性配置即可,代码如下:
yield scrapy.Request(url, self.parse_result, meta={'splash': {'args': {# set rendering arguments here'html': 1,'png': 1,# 'url' is prefilled from request url# 'http_method' is set to 'POST' for POST requests# 'body' is set to request body for POST requests},# optional parameters'endpoint': 'render.json', # optional; default is render.json'splash_url': '<url>', # optional; overrides SPLASH_URL'slot_policy': scrapy_splash.SlotPolicy.PER_DOMAIN,'splash_headers': {}, # optional; a dict with headers sent to Splash'dont_process_response': True, # optional, default is False'dont_send_headers': True, # optional, default is False'magic_response': False, # optional, default is True}
})
SplashRequest 对象通过 args 来配置和 Request 对象通过 meta 来配置,两种方式达到的效果是相同的。
可以定义一个 Lua 脚本,来实现页面加载,代码如下所示:
function main(splash, args)assert(splash:go(args.url))assert(splash:wait(5))return {html = splash:html()png = splash.png()har = splash.har()}
这里实现的逻辑很简单,就是获取参数中的 url 属性并访问,然后等待 5 秒,最后把截图,html 代码,har 信息返回;接下来我们只需要在 Spider 中使用 SplashRequset 对接 Lua 脚本就好了,代码如下:
from scrapy import Spider
from scrapy_splash import SplashRequestscript = """
function main(splash, args)assert(splash:go(args.url))assert(splash:wait(5))return splash:html()
"""class BookSpider(Spider):name = 'book'allowed_domains = ['spa5.scrape.center']base_url = 'https://spa5.scrape.center'def start_requests(self):start_url = f'{self.base_url}/page/1'yield SplashRequest(start_url, callback=self.parse_index, args={'lua_source': script}, endpoint='execute')def parse_index(self, response):items = response.css('.item')for item in items:href = item.css('.top a::attr(href)').extract_first()detail_url = response.urljoin(href)yield SplashRequest(detail_url, callback=self.parse_detail, priority=2, args={'lua_source': script}, endpoint='execute')match = re.search(r'page/(\d+)', response.url)if not math: returnpage = int(match.group(1)) + 1next_url = f'{self.base_url}/page/{page}'yield SplashRequest(detail_url, callback=self.parse_detail, priority=2, args={'lua_source': script}, endpoint='execute')def parse_detail(self, response):name = response.css('.name::text').extract_first()tags = response.css('.tags button span::text').extractscore = response.css('.score::text').extract_first()price = response.css('.price span::text').extract_first()cover = response.css('.cover::attr(src)').extract_first()tags = [tag.strip() if score else None] if tags else []score = score.strip() if score else Noneitem = BookItem(name=name, tags=tags, score=score, price=price, cover=cover)yield item
接下来通过下列命令运行爬取
scrapy crawl book
8.2 Splash 对接 Selenium
Scrapy 抓取页面的方式和 requests 库类似,都是直接模拟 HTTP 请求,而 Scrapy 也不能抓取 JavaScript 动态渲染的页面。在前文中抓取 JavaScript 渲染的页面有两种方式。一种是分析 Ajax 请求,找到其对应的接口抓取,Scrapy 同样可以用此种方式抓取。另一种是直接用 Selenium 或 Splash 模拟浏览器进行抓取,我们不需要关心页面后台发生的请求,也不需要分析渲染过程,只需要关心页面最终结果即可,可见即可爬。那么,如果 Scrapy 可以对接 Selenium,那 Scrapy 就可以处理任何网站的抓取了。
本节我们来看看 Scrapy 框架如何对接 Selenium,以 PhantomJS 进行演示。我们依然抓取淘宝商品信息,抓取逻辑和前文中用 Selenium 抓取淘宝商品完全相同。
请确保 PhantomJS 和 MongoDB 已经安装好并可以正常运行,安装好 Scrapy、Selenium、PyMongo 库,安装方式可以参考第 1 章的安装说明。
首先新建项目,名为 scrapyseleniumtest,命令如下所示:
scrapy startproject scrapyseleniumtest
新建一个 Spider,命令如下所示:
scrapy genspider taobao www.taobao.com
修改 ROBOTSTXT_OBEY 为 False,如下所示:
ROBOTSTXT_OBEY = False
首先定义 Item 对象,名为 ProductItem,代码如下所示:
from scrapy import Item, Fieldclass ProductItem(Item):collection = 'products'image = Field()price = Field()deal = Field()title = Field()shop = Field()location = Field()
这里我们定义了 6 个 Field,也就是 6 个字段,跟之前的案例完全相同。然后定义了一个 collection 属性,即此 Item 保存到 MongoDB 的 Collection 名称。
初步实现 Spider 的 start_requests() 方法,如下所示:
from scrapy import Request, Spider
from urllib.parse import quote
from scrapyseleniumtest.items import ProductItemclass TaobaoSpider(Spider):name = 'taobao'allowed_domains = ['www.taobao.com']base_url = 'https://s.taobao.com/search?q='def start_requests(self):for keyword in self.settings.get('KEYWORDS'):for page in range(1, self.settings.get('MAX_PAGE') + 1):url = self.base_url + quote(keyword)yield Request(url=url, callback=self.parse, meta={'page': page}, dont_filter=True)
首先定义了一个 base_url,即商品列表的 URL,其后拼接一个搜索关键字就是该关键字在淘宝的搜索结果商品列表页面。
关键字用 KEYWORDS 标识,定义为一个列表。最大翻页页码用 MAX_PAGE 表示。它们统一定义在 setttings.py 里面,如下所示:
KEYWORDS = ['iPad']
MAX_PAGE = 100
在 start_requests() 方法里,我们首先遍历了关键字,遍历了分页页码,构造并生成 Request。由于每次搜索的 URL 是相同的,所以分页页码用 meta 参数来传递,同时设置 dont_filter 不去重。这样爬虫启动的时候,就会生成每个关键字对应的商品列表的每一页的请求了。
接下来我们需要处理这些请求的抓取。这次我们对接 Selenium 进行抓取,采用 Downloader Middleware 来实现。在 Middleware 里面的 process_request() 方法里对每个抓取请求进行处理,启动浏览器并进行页面渲染,再将渲染后的结果构造一个 HtmlResponse 对象返回。代码实现如下所示:
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from scrapy.http import HtmlResponse
from logging import getLoggerclass SeleniumMiddleware():def __init__(self, timeout=None, service_args=[]):self.logger = getLogger(__name__)self.timeout = timeoutself.browser = webdriver.PhantomJS(service_args=service_args)self.browser.set_window_size(1400, 700)self.browser.set_page_load_timeout(self.timeout)self.wait = WebDriverWait(self.browser, self.timeout)def __del__(self):self.browser.close()def process_request(self, request, spider):"""用 PhantomJS 抓取页面:param request: Request 对象:param spider: Spider 对象:return: HtmlResponse"""self.logger.debug('PhantomJS is Starting')page = request.meta.get('page', 1)try:self.browser.get(request.url)if page > 1:input = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '#mainsrp-pager div.form> input')))submit = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '#mainsrp-pager div.form> span.btn.J_Submit')))input.clear()input.send_keys(page)submit.click()self.wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, '#mainsrp-pager li.item.active> span'), str(page)))self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.m-itemlist .items .item')))return HtmlResponse(url=request.url, body=self.browser.page_source, request=request, encoding='utf-8', status=200)except TimeoutException:return HtmlResponse(url=request.url, status=500, request=request)@classmethoddef from_crawler(cls, crawler):return cls(timeout=crawler.settings.get('SELENIUM_TIMEOUT'),service_args=crawler.settings.get('PHANTOMJS_SERVICE_ARGS'))
首先我们在 __init__() 里对一些对象进行初始化,包括 PhantomJS、WebDriverWait 等对象,同时设置页面大小和页面加载超时时间。在 process_request() 方法中,我们通过 Request 的 meta 属性获取当前需要爬取的页码,调用 PhantomJS 对象的 get() 方法访问 Request 的对应的 URL。这就相当于从 Request 对象里获取请求链接,然后再用 PhantomJS 加载,而不再使用 Scrapy 里的 Downloader。
随后的处理等待和翻页的方法在此不再赘述,和前文的原理完全相同。最后,页面加载完成之后,我们调用 PhantomJS 的 page_source 属性即可获取当前页面的源代码,然后用它来直接构造并返回一个 HtmlResponse 对象。构造这个对象的时候需要传入多个参数,如 url、body 等,这些参数实际上就是它的基础属性。可以在官方文档查看 HtmlResponse 对象的结构:https://doc.scrapy.org/en/latest/topics/request-response.html,这样我们就成功利用 PhantomJS 来代替 Scrapy 完成了页面的加载,最后将 Response 返回即可。
有人可能会纳闷:为什么实现这么一个 Downloader Middleware 就可以了?之前的 Request 对象怎么办?Scrapy 不再处理了吗?Response 返回后又传递给了谁?
是的,Request 对象到这里就不会再处理了,也不会再像以前一样交给 Downloader 下载。Response 会直接传给 Spider 进行解析。
我们需要回顾一下 Downloader Middleware 的 process_request() 方法的处理逻辑,内容如下所示:
当 process_request() 方法返回 Response 对象的时候,更低优先级的 Downloader Middleware 的 process_request() 和 process_exception() 方法就不会被继续调用了,转而开始执行每个 Downloader Middleware 的 process_response() 方法,调用完毕之后直接将 Response 对象发送给 Spider 来处理。
这里直接返回了一个 HtmlResponse 对象,它是 Response 的子类,返回之后便顺次调用每个 Downloader Middleware 的 process_response() 方法。而在 process_response() 中我们没有对其做特殊处理,它会被发送给 Spider,传给 Request 的回调函数进行解析。
到现在,我们应该能了解 Downloader Middleware 实现 Selenium 对接的原理了。
在 settings.py 里,我们设置调用刚才定义的 SeleniumMiddleware、设置等待超时变量 SELENIUM_TIMEOUT、设置 PhantomJS 配置参数 PHANTOMJS_SERVICE_ARGS,如下所示:
DOWNLOADER_MIDDLEWARES = {'scrapyseleniumtest.middlewares.SeleniumMiddleware': 543,}
Response 对象就会回传给 Spider 内的回调函数进行解析。所以下一步我们就实现其回调函数,对网页来进行解析,代码如下所示:
def parse(self, response):products = response.xpath('//div[@id="mainsrp-itemlist"]//div[@class="items"][1]//div[contains(@class, "item")]')for product in products:item = ProductItem()item['price'] = ''.join(product.xpath('.//div[contains(@class, "price")]//text()').extract()).strip()item['title'] = ''.join(product.xpath('.//div[contains(@class, "title")]//text()').extract()).strip()item['shop'] = ''.join(product.xpath('.//div[contains(@class, "shop")]//text()').extract()).strip()item['image'] = ''.join(product.xpath('.//div[@class="pic"]//img[contains(@class, "img")]/@data-src').extract()).strip()item['deal'] = product.xpath('.//div[contains(@class, "deal-cnt")]//text()').extract_first()item['location'] = product.xpath('.//div[contains(@class, "location")]//text()').extract_first()yield item
在这里我们使用 XPath 进行解析,调用 response 变量的 xpath() 方法即可。首先我们传递选取所有商品对应的 XPath,可以匹配所有商品,随后对结果进行遍历,依次选取每个商品的名称、价格、图片等内容,构造并返回一个 ProductItem 对象。
最后我们实现一个 Item Pipeline,将结果保存到 MongoDB,如下所示:
import pymongoclass MongoPipeline(object):def __init__(self, mongo_uri, mongo_db):self.mongo_uri = mongo_uriself.mongo_db = mongo_db@classmethoddef from_crawler(cls, crawler):return cls(mongo_uri=crawler.settings.get('MONGO_URI'), mongo_db=crawler.settings.get('MONGO_DB'))def open_spider(self, spider):self.client = pymongo.MongoClient(self.mongo_uri)self.db = self.client[self.mongo_db]def process_item(self, item, spider):self.db[item.collection].insert(dict(item))return itemdef close_spider(self, spider):self.client.close()
此实现和前文中存储到 MongoDB 的方法完全一致,原理不再赘述。记得在 settings.py 中开启它的调用,如下所示:
ITEM_PIPELINES = {'scrapyseleniumtest.pipelines.MongoPipeline': 300,}
其中,MONGO_URI 和 MONGO_DB 的定义如下所示:
MONGO_URI = 'localhost'
MONGO_DB = 'taobao'
整个项目就完成了,执行如下命令启动抓取即可:
scrapy crawl taobao
运行结果如图所示:
再查看一下 MongoDB,结果如图所示:
这样我们便成功在 Scrapy 中对接 Selenium 并实现了淘宝商品的抓取。
本节代码地址为:https://github.com/Python3WebSpider/ScrapySeleniumTest。
我们通过改写 Downloader Middleware 的方式实现了 Selenium 的对接。但这种方法其实是阻塞式的,也就是说这样就破坏了 Scrapy 异步处理的逻辑,速度会受到影响。为了不破坏其异步加载逻辑,我们可以使用 Splash 实现。
8.3 关于其他
作者在这章还开发了两个包,第一个是 selenium 的包,介绍如下:
第二个是 pyppeteer 的包,介绍如下:
但是目前 pyppeteer 已停止维护,官方推荐使用 playwright 来继续;
九、Scrapy 规则化爬虫
通过 Scrapy,我们可以轻松地完成一个站点爬虫的编写。但如果抓取的站点量非常大,比如爬取各大媒体的新闻信息,多个 Spider 则可能包含很多重复代码。
如果我们将各个站点的 Spider 的公共部分保留下来,不同的部分提取出来作为单独的配置,如爬取规则、页面解析方式等抽离出来做成一个配置文件,那么我们在新增一个爬虫的时候,只需要实现这些网站的爬取规则和提取规则即可。
9.1 CrawlSpider
在实现通用爬虫之前我们需要先了解一下 CrawlSpider,其官方文档链接为:http://scrapy.readthedocs.io/en/latest/topics/spiders.html#crawlspider。
CrawlSpider 是 Scrapy 提供的一个通用 Spider。在 Spider 里,我们可以指定一些爬取规则来实现页面的提取,这些爬取规则由一个专门的数据结构 Rule 表示。Rule 里包含提取和跟进页面的配置,Spider 会根据 Rule 来确定当前页面中的哪些链接需要继续爬取、哪些页面的爬取结果需要用哪个方法解析等。
CrawlSpider 继承自 Spider 类。除了 Spider 类的所有方法和属性,它还提供了一个非常重要的属性和方法。
- rules,它是爬取规则属性,是包含一个或多个 Rule 对象的列表。每个 Rule 对爬取网站的动作都做了定义,CrawlSpider 会读取 rules 的每一个 Rule 并进行解析。
- parse_start_url(),它是一个可重写的方法。当 start_urls 里对应的 Request 得到 Response 时,该方法被调用,它会分析 Response 并必须返回 Item 对象或者 Request 对象。
这里最重要的内容莫过于 Rule 的定义了,它的定义和参数如下所示:
class scrapy.contrib.spiders.Rule(link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=None)
下面对其参数依次说明:
- link_extractor,是一个 Link Extractor 对象。通过它,Spider 可以知道从爬取的页面中提取哪些链接。提取出的链接会自动生成 Request。它又是一个数据结构,一般常用 LxmlLinkExtractor 对象作为参数,其定义和参数如下所示:
class scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor(allow=(), deny=(), allow_domains=(), deny_domains=(), deny_extensions=None, restrict_xpaths=(), restrict_css=(), tags=('a', 'area'), attrs=('href',), canonicalize=False, unique=True, process_value=None, strip=True)
allow 是一个正则表达式或正则表达式列表,它定义了从当前页面提取出的链接哪些是符合要求的,只有符合要求的链接才会被跟进。deny 则相反。allow_domains 定义了符合要求的域名,只有此域名的链接才会被跟进生成新的 Request,它相当于域名白名单。deny_domains 则相反,相当于域名黑名单。restrict_xpaths 定义了从当前页面中 XPath 匹配的区域提取链接,其值是 XPath 表达式或 XPath 表达式列表。restrict_css 定义了从当前页面中 CSS 选择器匹配的区域提取链接,其值是 CSS 选择器或 CSS 选择器列表。还有一些其他参数代表了提取链接的标签、是否去重、链接的处理等内容,使用的频率不高。可以参考文档的参数说明:http://scrapy.readthedocs.io/en/latest/topics/link-extractors.html#module-scrapy.linkextractors.lxmlhtml。
- callback,即回调函数,和之前定义 Request 的 callback 有相同的意义。每次从 link_extractor 中获取到链接时,该函数将会调用。该回调函数接收一个 response 作为其第一个参数,并返回一个包含 Item 或 Request 对象的列表。注意,避免使用 parse() 作为回调函数。由于 CrawlSpider 使用 parse() 方法来实现其逻辑,如果 parse() 方法覆盖了,CrawlSpider 将会运行失败。
- cb_kwargs,字典,它包含传递给回调函数的参数。
- follow,布尔值,即 True 或 False,它指定根据该规则从 response 提取的链接是否需要跟进。如果 callback 参数为 None,follow 默认设置为 True,否则默认为 False。
- process_links,指定处理函数,从 link_extractor 中获取到链接列表时,该函数将会调用,它主要用于过滤。
- process_request,同样是指定处理函数,根据该 Rule 提取到每个 Request 时,该函数都会调用,对 Request 进行处理。该函数必须返回 Request 或者 None。
以上内容便是 CrawlSpider 中的核心 Rule 的基本用法。但这些内容可能还不足以完成一个 CrawlSpider 爬虫。下面我们利用 CrawlSpider 实现新闻网站的爬取实例,来更好地理解 Rule 的用法。
9.2 Item Loader
我们了解了利用 CrawlSpider 的 Rule 来定义页面的爬取逻辑,这是可配置化的一部分内容。但是,Rule 并没有对 Item 的提取方式做规则定义。对于 Item 的提取,我们需要借助另一个模块 Item Loader 来实现。
Item Loader 提供一种便捷的机制来帮助我们方便地提取 Item。它提供的一系列 API 可以分析原始数据对 Item 进行赋值。Item 提供的是保存抓取数据的容器,而 Item Loader 提供的是填充容器的机制。有了它,数据的提取会变得更加规则化。
Item Loader 的 API 如下所示:
class scrapy.loader.ItemLoader([item, selector, response,] **kwargs)
Item Loader 的 API 返回一个新的 Item Loader 来填充给定的 Item。如果没有给出 Item,则使用 default_item_class 中的类自动实例化。另外,它传入 selector 和 response 参数来使用选择器或响应参数实例化。
下面将依次说明 Item Loader 的 API 参数。
- item,Item 对象,可以调用 add_xpath()、add_css() 或 add_value() 等方法来填充 Item 对象。
- selector,Selector 对象,用来提取填充数据的选择器。
- response,Response 对象,用于使用构造选择器的 Response。
一个比较典型的 Item Loader 实例如下:
from scrapy.loader import ItemLoader
from project.items import Productdef parse(self, response):loader = ItemLoader(item=Product(), response=response)loader.add_xpath('name', '//div[@class="product_name"]')loader.add_xpath('name', '//div[@class="product_title"]')loader.add_xpath('price', '//p[@id="price"]')loader.add_css('stock', 'p#stock]')loader.add_value('last_updated', 'today')return loader.load_item()
这里首先声明一个 Product Item,用该 Item 和 Response 对象实例化 ItemLoader,调用 add_xpath() 方法把来自两个不同位置的数据提取出来,分配给 name 属性,再用 add_xpath()、add_css()、add_value() 等方法对不同属性依次赋值,最后调用 load_item() 方法实现 Item 的解析。这种方式比较规则化,我们可以把一些参数和规则单独提取出来做成配置文件或存到数据库,即可实现可配置化。
另外,Item Loader 每个字段中都包含了一个 Input Processor(输入处理器)和一个 Output Processor(输出处理器)。Input Processor 收到数据时立刻提取数据,Input Processor 的结果被收集起来并且保存在 ItemLoader 内,但是不分配给 Item。收集到所有的数据后,load_item() 方法被调用来填充再生成 Item 对象。在调用时会先调用 Output Processor 来处理之前收集到的数据,然后再存入 Item 中,这样就生成了 Item。
下面将介绍一些内置的 Processor。
Identity
Identity 是最简单的 Processor,不进行任何处理,直接返回原来的数据。
TakeFirst
TakeFirst 返回列表的第一个非空值,类似 extract_first() 的功能,常用作 Output Processor,如下所示:
from scrapy.loader.processors import TakeFirst
processor = TakeFirst()
print(processor(['', 1, 2, 3]))
输出结果如下所示:
1
经过此 Processor 处理后的结果返回了第一个不为空的值。
Join
Join 方法相当于字符串的 join() 方法,可以把列表拼合成字符串,字符串默认使用空格分隔,如下所示:
from scrapy.loader.processors import Join
processor = Join()
print(processor(['one', 'two', 'three']))
输出结果如下所示:
one two three
它也可以通过参数更改默认的分隔符,例如改成逗号:
from scrapy.loader.processors import Join
processor = Join(',')
print(processor(['one', 'two', 'three']))
运行结果如下所示:
one,two,three
Compose
Compose 是用给定的多个函数的组合而构造的 Processor,每个输入值被传递到第一个函数,其输出再传递到第二个函数,依次类推,直到最后一个函数返回整个处理器的输出,如下所示:
from scrapy.loader.processors import Compose
processor = Compose(str.upper, lambda s: s.strip())
print(processor(' hello world'))
运行结果如下所示:
HELLO WORLD
在这里我们构造了一个 Compose Processor,传入一个开头带有空格的字符串。Compose Processor 的参数有两个:第一个是 str.upper,它可以将字母全部转为大写;第二个是一个匿名函数,它调用 strip() 方法去除头尾空白字符。Compose 会顺次调用两个参数,最后返回结果的字符串全部转化为大写并且去除了开头的空格。
MapCompose
与 Compose 类似,MapCompose 可以迭代处理一个列表输入值,如下所示:
from scrapy.loader.processors import MapCompose
processor = MapCompose(str.upper, lambda s: s.strip())
print(processor(['Hello', 'World', 'Python']))
运行结果如下所示:
['HELLO', 'WORLD', 'PYTHON']
被处理的内容是一个可迭代对象,MapCompose 会将该对象遍历然后依次处理。
SelectJmes
SelectJmes 可以查询 JSON,传入 Key,返回查询所得的 Value。不过需要先安装 jmespath 库才可以使用它,命令如下所示:
pip3 install jmespath
安装好 jmespath 之后,便可以使用这个 Processor 了,如下所示:
from scrapy.loader.processors import SelectJmes
proc = SelectJmes('foo')
processor = SelectJmes('foo')
print(processor({'foo': 'bar'}))
运行结果:
bar
以上内容便是一些常用的 Processor,在本节的实例中我们会使用 Processor 来进行数据的处理。
接下来,我们用一个实例来了解 Item Loader 的用法。
我们以中华网科技类新闻为例,来了解 CrawlSpider 和 Item Loader 的用法,再提取其可配置信息实现可配置化。官网链接为:http://tech.china.com/。我们需要爬取它的科技类新闻内容,链接为:http://tech.china.com/articles/。
我们要抓取新闻列表中的所有分页的新闻详情,包括标题、正文、时间、来源等信息。
首先新建一个 Scrapy 项目,名为 scrapyuniversal,如下所示:
scrapy startproject scrapyuniversal
创建一个 CrawlSpider,需要先制定一个模板。我们可以先看看有哪些可用模板,命令如下所示:
scrapy genspider -l
运行结果如下所示:
Available templates:basiccrawlcsvfeedxmlfeed
之前创建 Spider 的时候,我们默认使用了第一个模板 basic。这次要创建 CrawlSpider,就需要使用第二个模板 crawl,创建命令如下所示:
scrapy genspider -t crawl china tech.china.com
运行之后便会生成一个 CrawlSpider,其内容如下所示:
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Ruleclass ChinaSpider(CrawlSpider):name = 'china'allowed_domains = ['tech.china.com']start_urls = ['http://tech.china.com/']rules = (Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),)def parse_item(self, response):i = {}#i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract()#i['name'] = response.xpath('//div[@id="name"]').extract()#i['description'] = response.xpath('//div[@id="description"]').extract()return i
这次生成的 Spider 内容多了一个 rules 属性的定义。Rule 的第一个参数是 LinkExtractor,就是上文所说的 LxmlLinkExtractor,只是名称不同。同时,默认的回调函数也不再是 parse,而是 parse_item。
定义 Rule
要实现新闻的爬取,我们需要做的就是定义好 Rule,然后实现解析函数。下面我们就来一步步实现这个过程。
首先将 start_urls 修改为起始链接,代码如下所示:
start_urls = ['http://tech.china.com/articles/']
之后,Spider 爬取 start_urls 里面的每一个链接。所以这里第一个爬取的页面就是我们刚才所定义的链接。得到 Response 之后,Spider 就会根据每一个 Rule 来提取这个页面内的超链接,去生成进一步的 Request。接下来,我们就需要定义 Rule 来指定提取哪些链接。
当前页面如图所示:
这是新闻的列表页,下一步自然就是将列表中的每条新闻详情的链接提取出来。这里直接指定这些链接所在区域即可。查看源代码,所有链接都在 ID 为 left_side 的节点内,具体来说是它内部的 class 为 con_item 的节点;
此处我们可以用 LinkExtractor 的 restrict_xpaths 属性来指定,之后 Spider 就会从这个区域提取所有的超链接并生成 Request。但是,每篇文章的导航中可能还有一些其他的超链接标签,我们只想把需要的新闻链接提取出来。真正的新闻链接路径都是以 article 开头的,我们用一个正则表达式将其匹配出来再赋值给 allow 参数即可。另外,这些链接对应的页面其实就是对应的新闻详情页,而我们需要解析的就是新闻的详情信息,所以此处还需要指定一个回调函数 callback。
到现在我们就可以构造出一个 Rule 了,代码如下所示:
Rule(LinkExtractor(allow='article\/.*\.html', restrict_xpaths='//div[@id="left_side"]//div[@class="con_item"]'), callback='parse_item')
接下来,我们还要让当前页面实现分页功能,所以还需要提取下一页的链接。分析网页源码之后可以发现下一页链接是在 ID 为 pageStyle 的节点内,如图 13-22 所示。
但是,下一页节点和其他分页链接区分度不高,要取出此链接我们可以直接用 XPath 的文本匹配方式,所以这里我们直接用 LinkExtractor 的 restrict_xpaths 属性来指定提取的链接即可。另外,我们不需要像新闻详情页一样去提取此分页链接对应的页面详情信息,也就是不需要生成 Item,所以不需要加 callback 参数。另外这下一页的页面如果请求成功了就需要继续像上述情况一样分析,所以它还需要加一个 follow 参数为 True,代表继续跟进匹配分析。其实,follow 参数也可以不加,因为当 callback 为空的时候,follow 默认为 True。此处 Rule 定义为如下所示:
Rule(LinkExtractor(restrict_xpaths='//div[@id="pageStyle"]//a[contains(., "下一页")]'))
所以现在 rules 就变成了:
rules = (Rule(LinkExtractor(allow='article\/.*\.html', restrict_xpaths='//div[@id="left_side"]//div[@class="con_item"]'), callback='parse_item'),Rule(LinkExtractor(restrict_xpaths='//div[@id="pageStyle"]//a[contains(., "下一页")]'))
)
接着我们运行一下代码,命令如下:
scrapy crawl china
现在已经实现页面的翻页和详情页的抓取了,我们仅仅通过定义了两个 Rule 即实现了这样的功能,运行效果如图。
解析页面
接下来我们需要做的就是解析页面内容了,将标题、发布时间、正文、来源提取出来即可。首先定义一个 Item,如下所示:
from scrapy import Field, Itemclass NewsItem(Item):title = Field()url = Field()text = Field()datetime = Field()source = Field()website = Field()
这里的字段分别指新闻标题、链接、正文、发布时间、来源、站点名称,其中站点名称直接赋值为中华网。因为既然是通用爬虫,肯定还有很多爬虫也来爬取同样结构的其他站点的新闻内容,所以需要一个字段来区分一下站点名称。
详情页的预览图如图所示:
如果像之前一样提取内容,就直接调用 response 变量的 xpath()、css() 等方法即可。这里 parse_item() 方法的实现如下所示:
def parse_item(self, response):item = NewsItem()item['title'] = response.xpath('//h1[@id="chan_newsTitle"]/text()').extract_first()item['url'] = response.urlitem['text'] = ''.join(response.xpath('//div[@id="chan_newsDetail"]//text()').extract()).strip()item['datetime'] = response.xpath('//div[@id="chan_newsInfo"]/text()').re_first('(\d+-\d+-\d+\s\d+:\d+:\d+)')item['source'] = response.xpath('//div[@id="chan_newsInfo"]/text()').re_first(' 来源:(.*)').strip()item['website'] = ' 中华网 'yield item
这样我们就把每条新闻的信息提取形成了一个 NewsItem 对象。
这时实际上我们就已经完成了 Item 的提取。再运行一下 Spider,如下所示:
scrapy crawl china
输出内容如图所示:
现在我们就可以成功将每条新闻的信息提取出来。
不过我们发现这种提取方式非常不规整。下面我们再用 Item Loader,通过 add_xpath()、add_css()、add_value() 等方式实现配置化提取。我们可以改写 parse_item(),如下所示:
def parse_item(self, response):loader = ChinaLoader(item=NewsItem(), response=response)loader.add_xpath('title', '//h1[@id="chan_newsTitle"]/text()')loader.add_value('url', response.url)loader.add_xpath('text', '//div[@id="chan_newsDetail"]//text()')loader.add_xpath('datetime', '//div[@id="chan_newsInfo"]/text()', re='(\d+-\d+-\d+\s\d+:\d+:\d+)')loader.add_xpath('source', '//div[@id="chan_newsInfo"]/text()', re=' 来源:(.*)')loader.add_value('website', ' 中华网 ')yield loader.load_item()
这里我们定义了一个 ItemLoader 的子类,名为 ChinaLoader,其实现如下所示:
from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst, Join, Composeclass NewsLoader(ItemLoader):default_output_processor = TakeFirst()class ChinaLoader(NewsLoader):text_out = Compose(Join(), lambda s: s.strip())source_out = Compose(Join(), lambda s: s.strip())
ChinaLoader 继承了 NewsLoader 类,其内定义了一个通用的 Out Processor 为 TakeFirst,这相当于之前所定义的 extract_first() 方法的功能。我们在 ChinaLoader 中定义了 text_out 和 source_out 字段。这里使用了一个 Compose Processor,它有两个参数:第一个参数 Join 也是一个 Processor,它可以把列表拼合成一个字符串;第二个参数是一个匿名函数,可以将字符串的头尾空白字符去掉。经过这一系列处理之后,我们就将列表形式的提取结果转化为去除头尾空白字符的字符串。
代码重新运行,提取效果是完全一样的。
至此,我们已经实现了爬虫的半通用化配置。
通用配置抽取
为什么现在只做到了半通用化?如果我们需要扩展其他站点,仍然需要创建一个新的 CrawlSpider,定义这个站点的 Rule,单独实现 parse_item() 方法。还有很多代码是重复的,如 CrawlSpider 的变量、方法名几乎都是一样的。那么我们可不可以把多个类似的几个爬虫的代码共用,把完全不相同的地方抽离出来,做成可配置文件呢?
当然可以。那我们可以抽离出哪些部分?所有的变量都可以抽取,如 name、allowed_domains、start_urls、rules 等。这些变量在 CrawlSpider 初始化的时候赋值即可。我们就可以新建一个通用的 Spider 来实现这个功能,命令如下所示:
scrapy genspider -t crawl universal universal
这个全新的 Spider 名为 universal。接下来,我们将刚才所写的 Spider 内的属性抽离出来配置成一个 JSON,命名为 china.json,放到 configs 文件夹内,和 spiders 文件夹并列,代码如下所示:
{"spider": "universal","website": "中华网科技","type": "新闻","index": "http://tech.china.com/","settings": {"USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36"},"start_urls": ["http://tech.china.com/articles/"],"allowed_domains": ["tech.china.com"],"rules": "china"
}
第一个字段 spider 即 Spider 的名称,在这里是 universal。后面是站点的描述,比如站点名称、类型、首页等。随后的 settings 是该 Spider 特有的 settings 配置,如果要覆盖全局项目,settings.py 内的配置可以单独为其配置。随后是 Spider 的一些属性,如 start_urls、allowed_domains、rules 等。rules 也可以单独定义成一个 rules.py 文件,做成配置文件,实现 Rule 的分离,如下所示:
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import Rulerules = {'china': (Rule(LinkExtractor(allow='article\/.*\.html', restrict_xpaths='//div[@id="left_side"]//div[@class="con_item"]'),callback='parse_item'),Rule(LinkExtractor(restrict_xpaths='//div[@id="pageStyle"]//a[contains(., "下一页")]')))
}
这样我们将基本的配置抽取出来。如果要启动爬虫,只需要从该配置文件中读取然后动态加载到 Spider 中即可。所以我们需要定义一个读取该 JSON 文件的方法,如下所示:
from os.path import realpath, dirname
import json
def get_config(name):path = dirname(realpath(__file__)) + '/configs/' + name + '.json'with open(path, 'r', encoding='utf-8') as f:return json.loads(f.read())
定义了 get_config() 方法之后,我们只需要向其传入 JSON 配置文件的名称即可获取此 JSON 配置信息。随后我们定义入口文件 run.py,把它放在项目根目录下,它的作用是启动 Spider,如下所示:
import sys
from scrapy.utils.project import get_project_settings
from scrapyuniversal.spiders.universal import UniversalSpider
from scrapyuniversal.utils import get_config
from scrapy.crawler import CrawlerProcessdef run():name = sys.argv[1]custom_settings = get_config(name)# 爬取使用的 Spider 名称spider = custom_settings.get('spider', 'universal')project_settings = get_project_settings()settings = dict(project_settings.copy())# 合并配置settings.update(custom_settings.get('settings'))process = CrawlerProcess(settings)# 启动爬虫process.crawl(spider, **{'name': name})process.start()if __name__ == '__main__':run()
运行入口为 run()。首先获取命令行的参数并赋值为 name,name 就是 JSON 文件的名称,其实就是要爬取的目标网站的名称。我们首先利用 get_config() 方法,传入该名称读取刚才定义的配置文件。获取爬取使用的 spider 的名称、配置文件中的 settings 配置,然后将获取到的 settings 配置和项目全局的 settings 配置做了合并。新建一个 CrawlerProcess,传入爬取使用的配置。调用 crawl() 和 start() 方法即可启动爬取。
在 universal 中,我们新建一个__init__() 方法,进行初始化配置,实现如下所示:
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapyuniversal.utils import get_config
from scrapyuniversal.rules import rulesclass UniversalSpider(CrawlSpider):name = 'universal'def __init__(self, name, *args, **kwargs):config = get_config(name)self.config = configself.rules = rules.get(config.get('rules'))self.start_urls = config.get('start_urls')self.allowed_domains = config.get('allowed_domains')super(UniversalSpider, self).__init__(*args, **kwargs)def parse_item(self, response):i = {}return i
在 __init__() 方法中,start_urls、allowed_domains、rules 等属性被赋值。其中,rules 属性另外读取了 rules.py 的配置,这样就成功实现爬虫的基础配置。
接下来,执行如下命令运行爬虫:
python3 run.py china
程序会首先读取 JSON 配置文件,将配置中的一些属性赋值给 Spider,然后启动爬取。运行效果完全相同,运行结果如图所示。
现在我们已经对 Spider 的基础属性实现了可配置化。剩下的解析部分同样需要实现可配置化,原来的解析函数如下所示:
def parse_item(self, response):loader = ChinaLoader(item=NewsItem(), response=response)loader.add_xpath('title', '//h1[@id="chan_newsTitle"]/text()')loader.add_value('url', response.url)loader.add_xpath('text', '//div[@id="chan_newsDetail"]//text()')loader.add_xpath('datetime', '//div[@id="chan_newsInfo"]/text()', re='(\d+-\d+-\d+\s\d+:\d+:\d+)')loader.add_xpath('source', '//div[@id="chan_newsInfo"]/text()', re=' 来源:(.*)')loader.add_value('website', ' 中华网 ')yield loader.load_item()
我们需要将这些配置也抽离出来。这里的变量主要有 Item Loader 类的选用、Item 类的选用、Item Loader 方法参数的定义,我们可以在 JSON 文件中添加如下 item 的配置:
"item": {"class": "NewsItem","loader": "ChinaLoader","attrs": {"title": [{"method": "xpath","args": ["//h1[@id='chan_newsTitle']/text()"]}],"url": [{"method": "attr","args": ["url"]}],"text": [{"method": "xpath","args": ["//div[@id='chan_newsDetail']//text()"]}],"datetime": [{"method": "xpath","args": ["//div[@id='chan_newsInfo']/text()"],"re": "(\\d+-\\d+-\\d+\\s\\d+:\\d+:\\d+)"}],"source": [{"method": "xpath","args": ["//div[@id='chan_newsInfo']/text()"],"re": "来源:(.*)"}],"website": [{"method": "value","args": ["中华网"]}]}
}
这里定义了 class 和 loader 属性,它们分别代表 Item 和 Item Loader 所使用的类。定义了 attrs 属性来定义每个字段的提取规则,例如,title 定义的每一项都包含一个 method 属性,它代表使用的提取方法,如 xpath 即代表调用 Item Loader 的 add_xpath() 方法。args 即参数,就是 add_xpath() 的第二个参数,即 XPath 表达式。针对 datetime 字段,我们还用了一次正则提取,所以这里还可以定义一个 re 参数来传递提取时所使用的正则表达式。
我们还要将这些配置之后动态加载到 parse_item() 方法里。最后,最重要的就是实现 parse_item() 方法,如下所示:
def parse_item(self, response):item = self.config.get('item')if item:cls = eval(item.get('class'))()loader = eval(item.get('loader'))(cls, response=response)# 动态获取属性配置for key, value in item.get('attrs').items():for extractor in value:if extractor.get('method') == 'xpath':loader.add_xpath(key, *extractor.get('args'), **{'re': extractor.get('re')})if extractor.get('method') == 'css':loader.add_css(key, *extractor.get('args'), **{'re': extractor.get('re')})if extractor.get('method') == 'value':loader.add_value(key, *extractor.get('args'), **{'re': extractor.get('re')})if extractor.get('method') == 'attr':loader.add_value(key, getattr(response, *extractor.get('args')))yield loader.load_item()
这里首先获取 Item 的配置信息,然后获取 class 的配置,将其初始化,初始化 Item Loader,遍历 Item 的各个属性依次进行提取。判断 method 字段,调用对应的处理方法进行处理。如 method 为 css,就调用 Item Loader 的 add_css() 方法进行提取。所有配置动态加载完毕之后,调用 load_item() 方法将 Item 提取出来。
重新运行程序,结果如图所示。
运行结果是完全相同的。
我们再回过头看一下 start_urls 的配置。这里 start_urls 只可以配置具体的链接。如果这些链接有 100 个、1000 个,我们总不能将所有的链接全部列出来吧?在某些情况下,start_urls 也需要动态配置。我们将 start_urls 分成两种,一种是直接配置 URL 列表,一种是调用方法生成,它们分别定义为 static 和 dynamic 类型。
本例中的 start_urls 很明显是 static 类型的,所以 start_urls 配置改写如下所示:
"start_urls": {"type":"static","value": ["http://tech.china.com/articles/"]}
如果 start_urls 是动态生成的,我们可以调用方法传参数,如下所示:
"start_urls": {"type": "dynamic","method": "china","args": [5, 10]
}
这里 start_urls 定义为 dynamic 类型,指定方法为 urls_china(),然后传入参数 5 和 10,来生成第 5 到 10 页的链接。这样我们只需要实现该方法即可,统一新建一个 urls.py 文件,如下所示:
def china(start, end):for page in range(start, end + 1):yield 'http://tech.china.com/articles/index_' + str(page) + '.html'
其他站点可以自行配置。如某些链接需要用到时间戳,加密参数等,均可通过自定义方法实现。
接下来在 Spider 的 __init__() 方法中,start_urls 的配置改写如下所示:
from scrapyuniversal import urlsstart_urls = config.get('start_urls')
if start_urls:if start_urls.get('type') == 'static':self.start_urls = start_urls.get('value')elif start_urls.get('type') == 'dynamic':self.start_urls = list(eval('urls.' + start_urls.get('method'))(*start_urls.get('args', [])))
这里通过判定 start_urls 的类型分别进行不同的处理,这样我们就可以实现 start_urls 的配置了。
至此,Spider 的设置、起始链接、属性、提取方法都已经实现了全部的可配置化。
综上所述,整个项目的配置包括如下内容。
- spider,指定所使用的 Spider 的名称。
- settings,可以专门为 Spider 定制配置信息,会覆盖项目级别的配置。
- start_urls,指定爬虫爬取的起始链接。
- allowed_domains,允许爬取的站点。
- rules,站点的爬取规则。
- item,数据的提取规则。
我们实现了 Scrapy 的通用爬虫,每个站点只需要修改 JSON 文件即可实现自由配置。
本节代码地址为:https://github.com/Python3WebSpider/ScrapyUniversal。
Scrapy 通用爬虫的实现:我们将所有配置抽离出来,每增加一个爬虫,就只需要增加一个 JSON 文件配置。之后我们只需要维护这些配置文件即可。如果要更加方便的管理,可以将规则存入数据库,再对接可视化管理页面即可。