Scrapy的介绍与使用

Scrapy

框架概述

Scrapy是一个非常流行的基于Python的网络爬虫框架,可以用来抓取Web站点并从页面中提取结构化的数据。利用Scrapy框架,我们仅需要关注核心的网页解析框架,而不用关注其他的一些常规流程,例如发送请求,下载请求,失败重试等等,极大地简化了爬虫程序的开发。下面是Scrapy的核心组件以及它的工作流程示意图:

在Scrapy中,有如下的核心组件:

  1. Scrapy引擎(Engine):用来控制整个系统的数据处理流程
  2. 调度器(Scheduler):调度器从引擎接受请求并排序列入队列,同时接受引擎的调度请求
  3. 下载器(Downloader):负责抓取网页并将网页内容返回给爬虫程序
  4. 爬虫程序(Spiders):爬虫程序是用户自定义的用来解析网页并抓取特定URL的类,每个蜘蛛都能够处理一个域名或者一组域名。简单来说,就是用来定义特定网站的抓取和解析规则的模块
  5. 数据管道(Item Pipeline):主要负责处理爬虫程序从网页中抽取的数据条目,负责数据的清理,验证和存储。
  6. 中间件(Middleware):提供自定义的代码来扩展Scrapy的功能,包括下载器中间件以及爬虫中间件

下面是Scrapy的工作流程:

  1. 引擎询问爬虫程序需要处理哪个或者哪些网站,爬虫程序将第一个需要处理的URL返回
  2. 引擎将需要处理的URL传送给调度器,调度器负责URL的调度
  3. 引擎从调度器中获取接下来需要爬取的URL,并将其发送给下载器
  4. 当网页被下载器下载完成之后,响应内容通过下载中间件被发送到引擎当中;如果下载失败,则引擎会通知调度器记录这个URL,等待后续重试
  5. 引擎收到了下载器的响应之后,将其通过爬虫程序进行处理
  6. 爬虫程序处理响应,并返回爬取到的数据条目,此外还需要将新的URL发送给引擎
  7. 引擎将爬取到的数据条目送入数据管道,并将新的URL传送给调度器
  8. 重复以上过程,直到调度器中没有需要请求的URL

Getting Started

我们可以通过下面的案例,利用Scrapy快速创建一个爬虫项目。首先需要安装Scrapy,通过pip或者conda进行安装都可以。安装完成之后,可以执行下面的scrapy startproject命令来创建一个模板项目,其中会提供Scrapy项目所需要的基本文件。

1
2
3
pip install scrapy

scrapy startproject xxx

这里我们选择创建一个项目,名称为scrapytest。进入创建好的目录中,我们可以看到如下的项目结构:

1
2
3
4
5
6
7
8
9
10
- scrapytest
- scrapytest
- spiders
- __init__.py
- __init__.py
- items.py
- middlewares.py
- pipelines.py
- settings.py
- scrapy.cfg

可以看到这里的文件或者目录名称,与Scrapy中的相关组件可以相互对应。其中我们首先需要关注的就是爬虫程序,这里是存放在spiders目录下。根据创建项目时命令行中打印的提示,我们可以通过scrapy genspider example example.com来创建示例爬虫程序。我们这里拿一个经典新手例子,豆瓣电影Top250。创建douban爬虫程序,会生成如下的基本内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
import scrapy
from scrapy import Selector, Request
from scrapy.http import HtmlResponse
from scrapytest.items import MovieItem


class DouBanSpider(scrapy.Spider):
name = "douban"
allowed_domains = ["movie.douban.com"]
start_urls = ["https://movie.douban.com/top250"]

def parse(self, response: HtmlResponse):
pass

这里就是我们需要核心关注的地方。其中,name是这个爬虫的识别名称,必须是唯一的,在不同的爬虫必须定义不同的名字。

allow_domains是搜索的域名范围,也就是爬虫的约束区域,规定爬虫只爬取这个域名下的网页,不存在的URL会被忽略。

start_urls是爬取的URL元祖/列表。爬虫从这里开始抓取数据,所以,第一次下载的数据将会从这些urls开始。其他子URL将会从这些起始URL中继承性生成。

parse方法接收响应,然后对响应进行解析。在这里我们主要需要将网页中的内容进行解析,解析成一个Item类的形式,以便持久化到后续的文件当中。同时我们还可以解析网页中的相关url,将其传送给核心引擎,进行下一轮的爬取。在这里,我们首先可以定义一个相关的Item类,来进行数据的承接。这个类需要继承Scrapy.Item,且需要定义在item.py文件当中。这里我们简单的利用三个属性来进行承接。

1
2
3
4
class MovieItem(scrapy.Item):
title = scrapy.Field()
rank = scrapy.Field()
subject = scrapy.Field()

之后我们就可以完成parse方法,进行网页内容的解析。相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def parse(self, response: HtmlResponse):
sel = Selector(response)
list_items = sel.css('#content > div > div.article > ol > li')
for list_item in list_items:
movie_item = MovieItem()
movie_item['title'] = list_item.css('span.title::text').extract_first()
movie_item['rank'] = list_item.css('span.rating_num::text').extract_first()
movie_item['subject'] = list_item.css('span.inq::text').extract_first()

yield movie_item

hrefs_list = sel.css("div.paginator > a::attr(href)")
for href in hrefs_list:
url = response.urljoin(href.extract())
yield Request(url=url)

这里我们首先将HtmlResponse包装成一个选择器,然后通过XPath进行解析,获取到我们需要的内容。有关XPath的具体内容这里不进行赘述,总之我们可以获取到对应的信息,将其赋值给MovieItem对象的对应属性,之后使用yield进行生成。之后我们也可以通过类似的方式获取到该网页中的相关链接,将其包装成Request对象之后,使用yield进行生成。需要注意的是,这里的yield生成的对象包括Item对象以及Request对象,核心引擎接收之后,会分别传递给数据管道以及调度器,然后分别进行不同的处理。调度器会持续接收URL,并判断是否爬取过,再决定是否发送给下载器进行爬取。

不过这里需要注意,调度器判断是否爬取是根据url的名称进行判断的,而不是通过网页内容进行判断的。可能出现不同的url对应的网页内容是相同的,这种情况会重复爬取。

这样,一个简单的爬虫程序就完成了。不过在运行之前,我们还需要调整一些相关配置。整个爬虫程序相关的设置都存放在settings.py文件中,其中定义了许多的常量,包括User-Agent、随机延迟、绕过robots协议、并发请求数量等等,这些都可以直接通过常量的定义来进行调整。

完成之后,我们就可以启动项目了。在项目目录下执行下面的命令,就可以启动爬虫项目。这个命令表示我们需要启动douban爬虫,也就是对应爬虫程序中的douban.py-o则表示将爬取的内容持久化到对应的路径中。项目执行会打印一些日志,可以使用--nolog来禁止日志打印。

1
scrapy crawl douban -o douban.csv

Scrapy保存信息的基本格式有四种,分别是jsonjsonlcsvxml

Pipeline

利用数据管道,我们可以将数据存储到更多形式的文件中,例如excel,mysql中,同时可以进行更多的数据处理。数据管道的相关代码都存放在pipelines.py中。初始生成的数据管道中会有一个默认的process_item方法,它不做任何处理直接返回Item对象。当然我们也可以自定义自己的数据管道类,进行定制化的处理。

除了process_item(self, item, spider)方法,在数据管道中还可以定义其他的方法,包括爬虫开始时执行,爬虫结束时执行的方法等,详情可以在官方文档中进行查询:Item Pipeline — Scrapy 2.8.0 documentation

数据管道的使用配置同样需要在settings.py中进行,对应常量为ITEM_PIPELINES,这是一个字典对象,Key为数据管道的全类名,Value是一个数值,表示管道执行的优先级,数字越小优先级越高,越先执行。

Middleware

中间件分为下载中间件和爬虫中间件。中间件实质上就是一个拦截器,例如下载中间件就可以在发送Request以及接收Response之前对相应的对象进行一次处理,然后再放行。比如再发送Request之前,给请求加上相关的proxy,cookie信息等。

中间件的内容存放在middlewares.py中,每个中间件类同样有许多方法,可以达到不同的效果。中间件的配置在settings.py中对应xxx_MIDDLEWARES常量,这同样是一个字典常量,与上面的数据管道配置有相同的约定,Key为全类名,Value为数字。数字越小优先级越高。

不同页面的爬取

在上面的例子中,我们只定义了一种解析的方式。但是一种更加常见的需求是我们通过第一个页面得到了进一步的网页链接之后,需要进入新的链接,提取新的信息,此时需要使用的parse方式是不同的,这种情况下需要利用到callback方法。

举例来说,我们还想要得到每一部电影的相关信息,则需要进入详情页查看。我们首先可以通过XPath得到链接信息,但是此时并不能将Item对象进行yield,因为这个对象还没有获取到全部的信息。此时我们应该先yield一个新的Request对象,对应的url就是我们得到的新的detail链接。同时由于对于这个链接的解析需要新的方法,所以我们首先需要实现一个方法parse_detail,然后在Request对象的callback属性中指定。最后,为了最后能够将Item对象进行生成,这里还需要将对应的Item对象传入下一步的parse_detail方法中。改进代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def parse(self, response: HtmlResponse):
sel = Selector(response)
list_items = sel.css('#content > div > div.article > ol > li')
for list_item in list_items:
detail_url = list_item.css('div.info > div.hd > a::attr(href)').extract_first()

movie_item = MovieItem()
movie_item['title'] = list_item.css('span.title::text').extract_first()
movie_item['rank'] = list_item.css('span.rating_num::text').extract_first()
movie_item['subject'] = list_item.css('span.inq::text').extract_first()

yield Request(url=detail_url, callback=self.parse_detail,
cb_kwargs={'item': movie_item})

hrefs_list = sel.css("div.paginator > a::attr(href)")
for href in hrefs_list:
url = response.urljoin(href.extract())
yield Request(url=url)

def parse_detail(self, response, **kwargs):
movie_item = kwargs['item']

sel = Selector(response)
movie_item['duration'] = sel.css('span[property=v:runtime]::attr(content)').extract()
movie_item['intro'] = sel.css('span[property=v:summary]::text').extract_first()

yield movie_item

在第一步的parse方法中,我们先获取到了相关信息和详情链接,yield新的Request对象,这个对象的回调方法是parse_detail,在这个方法中我们进一步获取详情信息,同时yield完整的Item对象。

爬虫过程中的信号

在爬虫过程中还会涉及到一些信号的使用。例如我们可能会有这样的需求,就是在爬虫的开始和结束的时候进行一些操作,此时我们可以通过信号以及函数绑定的方式来完成。

首先我们需要在自定义的爬虫Spider中实现一个from_crawler方法,在其中我们需要先获取到自己的spider,然后利用信号进行函数的方法绑定,这里signals的引入方法为from scrapy import signals。在这里我们绑定了开始行为以及结束行为,分别打印相关的提示信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TestSpider(scrapy.Spider):
name = "Test"

allowed_domains = []
start_urls = []
start_url = ''

@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
spider = super(TestSpider, cls).from_crawler(crawler, *args, **kwargs)
crawler.signals.connect(spider.open_action, signal=signals.spider_opened)
crawler.signals.connect(spider.close_action, signal=signals.spider_closed)
return spider

def open_action(self, spider):
spider.logger.info('Spider open: %s', spider.name)

def close_action(self, spider):
spider.logger.info('Spider closed: %s', spider.name)

def parse(self, response: HtmlResponse):
print(response.text)

在Scrapy中还提供了一些其他的信号,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 可选信号
engine_started = object()
engine_stopped = object()
spider_opened = object()
spider_idle = object()
spider_closed = object()
spider_error = object()
request_scheduled = object()
request_dropped = object()
response_received = object()
response_downloaded = object()
item_scraped = object()
item_dropped = object()

链接提取器

通常爬虫程序应对的都是多网页的爬取,这就要求我们能够从单个网页出发,爬取出多个链接,然后递归进行对应的操作。在上面不同页面的爬取当中,我们可以通过Selector手动从其中抽取出相关的链接,而实际上,Scrapy也提供了相关的链接提取器给我们使用。Scrapy中提供了一个专门用于提取链接的类LinkExtractor,在提取大量链接或者提取规则比较复杂的时候,使用LinkExtractor更加方便。该类的引入为:from scrapy.linkextractors import LinkExtractor

该类的使用非常简单,我们只需要初始化得到一个对象之后,利用它的extract_links方法即可。该方法接收一个Response对象,然后就可以返回其中的存在的链接数组。

1
2
3
4
def parse(self, response: HtmlResponse, *args):
link_extractor = LinkExtractor()
links = link_extractor.extract_links(response)
print([link.url for link in links])

如果在初始化LinkExtractor的时候不提供任何参数,则链接抽取的规则使用默认的全部抽取。当然我们还可以定制化一些提取规则,只需要在初始化的时候提供对应参数即可,相关参数如下

  • allow:该参数接收一个正则表达式或者一个正则表达式列表,提取绝对url与正则表达式匹配的链接。如果参数为空,则提取全部链接
  • deny:该参数效果与allow正好相反,排除与正则表达式匹配的链接
  • allow_domains:接收一个域名或一个域名列表,提取到指定域的链接
  • deny_domains:与allow_domains正好相反,排除到指定域的链接
  • restrict_xpaths:接收一个XPath表达式或者一个XPath表达式列表,提取XPath选中区域下的链接
  • restrict_css:接收一个CSS表达式或者CSS表达式列表,提取选中区域中的链接
  • tags:接收一个标签或者标签列表,提取指定标签内的链接,默认为['a', 'area']
  • attrs:接收一个属性或者属性列表,提取指定属性内的链接,默认为['href']
  • procss_value:接收一个形如func(value)的回调函数。如果传递了该参数,则LinkExtractor会调用这个函数对提取的每个链接进行处理,回调函数需要返回一个字符串为最终的处理结果,如果想要抛弃所处理的链接,则需要返回None

日志管理

在Scrapy运行的时候,会打印出非常多的日志信息,Scrapy中也提供了相关方法进行日志的管理。默认情况下,所有的日志都是打印在控制台的,同时默认的日志等级是DEBUG。我们可以在setting.py中修改日志等级,例如LOG_LEVEL = 'ERROR',同时我们可以在运行命令的时候指定日志的输出位置以及日志等级:

1
scrapy crawl spiderName -s LOG_FILE=spider.log -s LOG_LEVEL=ERROR

Scrapy日志有五种等级,按照范围递增顺序排列如下:

  • CRITICAL - 严重错误
  • ERROR - 一般错误
  • WARNING - 警告信息
  • INFO - 一般信息
  • DEBUG - 调试信息

在使用Scrapy的时候,我们也可以在Scrapy中输出日志信息。在每个Spider实例中,都有一个内置的logger,我们可以通过self.logger来获取到日志记录器,然后调用相关方法进行日志输出。使用这个日志记录器输出的日志,将会与Scrapy中的日志放在一起统一管理。

1
self.logger.info("get url:%s", response.url)

参考文章

  1. Scrapy 2.8 documentation — Scrapy 2.8.0 documentation
  2. Scrapy 入门教程 | 菜鸟教程 (runoob.com)
  3. 优雅的操作scrapy爬虫的开始和结束
  4. 爬虫框架Scrapy(8)使用 LinkExtractor 提取链接
  5. scrapy 日志处理 - CrossPython - 博客园 (cnblogs.com)
  6. Scrapy日志 - Scrapy教程 (yiibai.com)

Scrapy的介绍与使用
http://example.com/2023/04/16/Scrapy的介绍与使用/
作者
EverNorif
发布于
2023年4月16日
许可协议