Scrapy功能非常强大,爬取效率高,相关扩展组件多,可配置和可扩展程度非常高,它几乎可以应对所有发爬网站,是目前Python中使用最广泛的爬虫框架。
Scrapy框架介绍
Scrapy是一个基于Twisted的异步处理框架,是纯Python实现的爬虫框架,其架构清晰,模块之间的耦合程度低,可扩展性极强,可以灵活完成各种需求。我们只需要定制开发几个模块就可以轻松实现一个爬虫。
架构介绍
它可以分为如下几个部分:
- Engine。引擎,处理整个系统的数据流处理、触发事务,是整个框架的核心。
- Item。项目,它定义了爬取结果的数据结构,爬取的数据会被赋值成该Item对象。
- Scheduler。调度器,接受引擎发过来的请求并将其加入队列中,在引擎再次请求的时候将请求提供给引擎。
- Downloader。下载器,下载网页内容,并将网页内容返回给蜘蛛。
- Spiders。蜘蛛,其内定义了爬取的逻辑和网页的解析规则,它主要负责解析响应并生成提取结果和新的请求。
- Item Pipeline。项目管道,负责处理由蜘蛛从网页中抽取的项目,它的主要任务是清洗、验证和存储数据。
- Downloader Middlewares。下载器中间件,位于引擎和下载器之间的钩子框架,主要处理引擎和下载器之间的请求及响应。
- Spider Middlewares。蜘蛛中间件,位于引擎和蜘蛛之间的钩子框架,主要处理蜘蛛输入的响应和输出的结果及新的请求。
数据流
Scrapy中的数据流由引擎控制,数据流的过程如下。
- Engine首先打开一个网站,找到该处理网站的Spider,并向该Spider请求第一个爬取的URL。
- Engine从Spider中获取到第一个要爬取的URL,并通过Scheduler以Request的形式调度。
- Engine想Scheduler请求下一个要爬取的URL。
- Scheduler返回下一个要爬取的URL给Engine,Engine将URL通过Downloader Middlewares转发给Downloader下载。
- 一旦页面下载完毕,Downloader生成该页面的Response,并将其通过Downloader Middlewares发送给Engine。
- Engine从下载器中接收到Response,并将其通过Spider Middlewares发送给Spider处理。
- Engine处理Response,并返回爬取到的Item及新的Request给Engine。
- Engine将Spider返回的Item给Item Pipeline,将新的Request给Scheduler。
- 重复第2步到第8步,知道Scheduler中没有更多的Request,Engine关闭该网站,爬取结束。
通过多个组件的相互写作、不同组件完成工作的不同、组件对异步处理的支持,Scrapy最大限度地利用了网络带宽,大大提高了数据爬取和处理的效率。
项目结构
Scrapy框架和pyspider不同,它是通过命令行来创建项目的,代码的编写还是需要IDE。项目创建之后,项目文件结构如下所示:
scrapy.cfg
project/
__init__.py
items.py
pipelines.py
settings.py
middlewares.py
spiders/
__init__.py
spider1.py
spider2.py
...
这里各个文件的功能描述如下:
- scrapy.cfg:它是Scrapy项目的配置文件,其内定义了项目的配置文件路径、部署相关信息等内容。
- items.py:它定义Item数据结构,所有的Item的定义都可以放这里。
- pipelines.py:它定义Itme Pipeline的实现,所有的Item Pipeline的实现都可以放这里。
- settings.py:它定义项目的全局配置。
- middlewares.py:它定义Spider Middlewares和Downloader Middlewares的实现。
- spiders:其内包含一个个Spider的实现,每个Spider都有一个文件。
创建项目
创建一个Scrapy项目,项目文件可以直接用scrapy命令生成,命令如下所示:
scrapy startproject tutorial
这个命令可以在任意文件夹运行。
创建Spider
Spider是自己定义的类,Scrapy用它来从网页里抓取内容,并解析抓取的结果。不过这个类必须集成Scrapy提供的Spider类scrpay.Spider,还要定义Spider的名称和起始请求,以及怎样处理爬取后的结果的方法。
也可以使用命令行创建一个Spider。比如要生成Quotes这个Spider,可以执行如下命令:
cd tutorial
scrapy genspider quotes quotes.toscrape.com
进入刚才创建的tutorial文件夹,然后执行genspider命令。第一个参数是Spider的名称,第二个参数是网站域名。执行完毕之后,spiders文件夹中多了一个quotes.py,它就是刚刚创建的Spider,内容如下所示:
import scrapy
class QuotesSpider(scrapy.Spider):
name = 'quotes'
allowed_domains = ['quotes.toscrape.com']
start_urls = ['http://quotes.toscrape.com/']
def parse(self, response):
pass
这里有三个属性——name、allow_domains和start_urls,还有一个方法parse。
- name,它是每个项目唯一的名字,用来区别不同荣的Spider。
- allowed_domains,它是允许爬取的域名,如果初始或后续的请求链接不是这个域名下的,则请求链接会被过滤掉。
- start_urls,它包含了Spider在启动时爬取的url列表,初始请求是由它来定义的。
- parse,它是Spider的一个方法。默认情况下,被调用时start_urls里面的链接构成的请求完成下载执行后,返回的响应就会作为唯一的参数传递给这个函数。该方法负责返回的响应、提取数据或者进一步生成要处理的请求。
创建Item
Item是保存爬取数据的容器,它的使用方法和字典类似。不过,相比字典,Item多了额外的保护机制,可以避免拼写错误或定义字段错误。
创建Item需要继承scrapy.Item类,并且定义类型为scrapy.Field的字段。观察目标网站,我们可以获取到内容由text、author、tags。
定义Item,此时将items.py修改如下:
import scrapy
class QuoteItem(scrapy.Item):
text = scrapy.Field()
author = scrapy.Field()
tags = scrapy.Field()
解析Response
parse()方法的参数response是start_urls里面的链接爬取后的结果。所以在parse()方法中,我们可以直接对response变量包含的内容进行解析,比如浏览请求结果的网页源代码,或者进一步分析源代码内容,或者找出结果中的链接而得到下一个请求。
查看(http://quotes.toscrape.com/)网页结构,每一页都有多个class为quote的区块,每个区块内都包含text、author、tags。那么我们先找出所有额quote,然后提取每一个quote中的内容。
提取的方式可以是CSS选择器或者XPath选择器。在这里我们使用CSS选择器进行选择,parse()方法的改写如下所示:
def parse(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()
这里首先利用选择器选取所有的quote,并将其赋值为quotes变量,然后利用for循环对每个quote遍历,解析每个quote的内容。
对text来说,观察到它的class为text,所以可以用.text选择器来选取,这个结果实际上是整个带标签的节点,要获取它的正文内容,可以加::text来获取。这时的结果是长度为1的列表,所以还需要用extract_first()方法来获取第一个元素。而对于tags来说,由于我们要获取所有的标签,所以用extract()方法获取整个列表即可。
使用Item
上文定义了Item,接下来就要使用它了。Item可以理解为一个字典,不过在声明的时候需要实例化。然后依次用刚才解析的结果赋值Item的每一个字段,最后将Item返回即可。
QuoteSpider的改写如下所示:
import scrapy
from tutorial.items import QuoteItem
class QuotesSpider(scrapy.Spider):
name = 'quotes'
allowed_domains = ['quotes.toscrape.com']
start_urls = ['http://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。
后续Request
上面的操作实现了从初始页面抓取内容。将刚才的页面拉到最底部,这里有一个Next按钮。查看它的源代码,可以发现它的链接是/page/2/,全链接就是:http://quotes.toscrape.com/page/2 ,通过这个链接我们就可以构造下一个请求。
构造请求时需要用到scrapy.Request。这里我们传递两个参数——url和callback,这两个参数的说明如下。
- url:它是请求链接。
- callback:它是回调函数。当指定了该回调函数的请求完成之后,获取到响应,引擎会将该响应作为参数传递给这个回调函数。回调函数进行解析或生成下一个请求,回调函数如上文的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)操作。然后再调用extract_first()方法获取内容。
第二句代码调用了urljoin()方法,urljoin()方法可以将相对URL构造成一个绝对的URL。例如,获取到的下一页地址是/page/2,urljoin()方法处理后得到的结果就是:http://quotes.toscrape.com/page/2/。
第三局代码通过url和callback变量构造了一个新的请求,回调函数callback依然使用parse()方法。这个请求完成后,响应会重新经过parse方法处理,得到第二页的解析结果,然后生成第二页的下一页,也就是第三页的请求。这样爬虫就进入了一个循环,直到最后一页。
通过几行代码,我们就轻松实现了一个抓取循环,将每个页面的结果抓取下来了。
现在,改写之后的整个Spider类如下所示:
import scrapy
from tutorial.items import QuoteItem
class QuotesSpider(scrapy.Spider):
name = 'quotes'
allowed_domains = ["quotes.toscrape.com"]
start_urls = ['http://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)
运行
接下来,进入目录,运行如下命令:
scrapy crawl quotes