前面介绍爬虫分类的时候,我们就对各个网络爬虫工具的优缺点进行了分析。Requests库适合进行轻量化、数据量较小、对速度不敏感的网页爬取;而要进行数据量较大、对网页爬取速度较为敏感的网站爬取,就需要使用Scrapy框架。Scrapy为什么是一个框架而不是库?如何使用这样一个性能更强但又较Requests库复杂的工具进行网站爬取?请看本文讲解。本文涵盖了
Scrapy
框架开发的几乎所有基础知识以及相关联知识,建议收藏。
Scrapy框架是一个用于爬取网站内容并进行数据提取的应用程序框架,在数据挖掘、信息处理与网站历史存档中有非常广泛的应用。之所以被称为“框架”,是由于其具有实现爬虫功能的软件结构和一系列功能组件集合。类似于工业产品中的半成品,可以根据开发者定制不同功能特性的专业网络爬虫。
Scrapy框架具有“5+2”结构,包含五个功能模块和两个中间件。分别为Engine
(引擎)、Spiders
(爬虫)、Downloader
(下载器)、Schedular
(调度器)和Item Pipelines
(管道),两个中间件Downloader Middleware
为模块间提供数据流服务。其中Engine
Downloader
Scheduler
已经实现,另外两个模块Item Piplines
Spiders
需要开发者自行编写配置。
Requests
请求数据提交请求并获取响应内容传回Engine
Requests
的优先级Item Pipline
类型Engine
、Scheduler
、Downloader
之间进行用户可配置的控制Scrapy是一个完整的框架,所以也为我们提供了命令行来进行控制,通过在控制台使用
scrapy -h
命令查看scrapy支持的命令。
scrapy [options] <command>
命令 | 说明 | 语法 |
---|---|---|
startproject | 创建一个新工程 | scrapy startproject [dir] |
genspider | 创建一个爬虫 | scrapy genspider [options] |
settings | 获取配置信息 | scrapy settings [options] |
crawl | 运行一个爬虫 | scrapy crawl |
list | 列出工程中所有爬虫 | scrapy list |
shell | 启动URL调试命令行 | scrapy shell [URL] |
在命令的必要参数前可增加选项,下面介绍部分全局选项,各命令的特殊选项输入命令后加上-h
选项可以获得
选项 | 说明 |
---|---|
–logfile=FILE | 将日志记录写入指定文件中 |
–loglevel=LEVEL | 指定日志输出等级,默认为DEBUG |
–nolog | 关闭日志输出 |
–profile=FILE | 将python cProfile 性能状态输出到文件中 |
–pidfile=FILE | 将进程ID写入到文件中 |
–set=NAME=VALUE | 重写配置 |
–pdb | 在失败后启动pdb 调试 |
使用downloader
模块下载指定的URL并输出在终端中,如果不是文本编码也会强制解码。
scrapy fetch https://www.demo.com/
在终端中使用如下命令在当前目录下生成一个名为demo的工程
scrapy startproject demo
Scrapy框架生成一个工程时,会创建一个工程目录,并在其中放置已经准备好的框架文件,开发时仅需要对这些框架文件进行编辑即可。
scrapy.cfg
配置文件,能在部署Scrapy爬虫框架时为部署服务器提供配置信息;另一个是工程同名资源目录,包含整个框架的所有代码。__init__.py
初始化脚本(无需编辑)、items.py
Items代码模板(继承类,一般不需要编辑)、middlewares.py
Middleware代码模板(继承类)、pipelines.py
Pipelines代码模板(继承类)、settings.py
Scrapy爬虫的配置文件、spiders/
Spiders代码模板目录,里面存放工程建立的爬虫使用scrapy的模板生成一个爬虫框架模板。如果当前在工程主目录中,则爬虫将会在资源目录的spiders/
目录中,否则将在当前目录中。
scrapy genspider <spider_name> <domain>
必须指定domain
参数,它将指定爬虫在特定服务器中爬取,这个允许域名将被写入生成的spider
框架中(可修改)
生成的框架如下(以名为demo1
的spider为例)
import scrapy
class Demo1Spider(scrapy.Spider):
name = 'demo1'
allowed_domains = ['www.demo.com']
start_urls = ['http://www.demo.com']
def parse(self, response):
pass
Spider
name
属性:爬虫名称allowed_domains
属性:为一个列表,设置爬虫允许访问的域名称(放置外站跳转),初始为在命令行中设置的domain
参数start_urls
属性:为一个列表,设置爬虫开始访问的URL,将以generator object
类型迭代,下面有其等价代码。该属性初始为http:///
parse
方法:对所有爬取得到的响应进行分析的方法,一开始将会返回爬取start_urls
中网页的响应,能被自身递归调用(用于得到链接继续爬取)其中,start_urlsl
属性可以用一段等价代码代替
def start_requests(self):
urls = [
'http://www.demo.com/'
]
for url in urls:
yield scrapy.Request(url=url, callback=self.parse)
关于yield
生成器,将在后面的补充知识中讲解
列出所有当前可用的爬虫。(一个爬虫符合框架的格式要求(并在spiders
目录中),就能够成为可用的爬虫)
scrapy list
运行一个爬虫
scrapy crawl <spider_name>
运行一个爬虫。
scrapy runspider <spider_name>
与crawl
命令不同的是,crawl
仅能运行在一个工程中的爬虫(工作目录在工程的外层目录下),可以对另外的模块(middlewares
pipelines
)进行配置;而runspider
可以运行一个独立的(自包含)爬虫,不需要在工程中,这个爬虫将使用其他模块的默认配置
将启动一个交互式命令行界面。
scrapy shell
将使用scrapy
框架爬取一个指定的URL,并将结果使用浏览器打开(CSS等内容由浏览器解析下载)
scrapy view <URL>
在一般情况下,我们都需要一个函数具有良好的可复用性(一个函数只做一步工作,返回值,而不是执行特定的操作),能够方便不同的函数进行调用。当一个函数产生的值需要进行迭代时,如果需要迭代的数集合非常大,则会出现诸多问题,而生成器很好地解决了这个问题。
普通迭代:直接在函数运行中即时输出结果
def sqr(n):
for i in range(n):
print(i**2)
sqr(5)
这样就能够输出想要的结果。但是,正如本段前言中所说,这段代码仅能完成生成并输出的一套操作,代码可复用性较差。如果想要这个函数的输出能够被下一步处理,则需要用到别的方式。
生成列表:由于列表是一个可迭代类型,生成一个列表可以解决这个问题
def sqr1(n):
l = []
for i in range(n):
l.append(i**2)
return l
for i in sqr(5):
print(i)
生成一个列表并输出的操作较易理解,但在n
非常大时,生成的列表数据量巨大,将占用很大一部分空间;并且,在完成整个列表的生成工作前,我们并不能输出一个结果,这将导致程序有一段较长的无响应时间,影响运行效率。
在了解普通方法及其存在的缺点后,我们一起来了解一下生成器的原理及其对应优势
生成器generator object
,就是一个不断产生值的函数,大多包含yield
语句,但它不会一次执行完成并返回值,而是具有可迭代性质。例如,在for
循环中,如果循环集合是一个生成器类型,则它不会将所有数据一并算出,**而是执行语句,直到遇到一个yield
语句,这个语句将返回一个值,作为循环变量的一个值,而后生成器函数冻结,开始运行循环体语句,直到再次运行到循环头时,这个函数被唤醒,所有变量将与它被冻结时的状态保持一致,函数将再次运行到yiled
语句并再次输出冻结……**直到函数运行结束再无遇到yield
语句,则会抛出一个错误StopIteration
(停止迭代),可以设置一个迭代结束的输出并加以处理以避免遇到这类错误。除了在可迭代变量的位置触发迭代之外,generator object
提供了一个函数.__next__()
直接进入这个生成器的下一迭代并返回值下面的两个例子展示了两种产生一个生成器的方法,后附运行顺序说明。
生成器(1):使用yield
语句产生一个生成器
def sqr3(n):
for i in range(n):
yield i ** 2
for i in sqr3(5):
print(i)
当执行到主程序的for
循环语句时,sqr3
函数将被启动并传入参数,函数内循环第一次中,返回一个值0
,生成器冻结,主函数输出这个值0
,在主程序运行到第二个循环头时,生成器再次被唤醒,变量i
为原值0
,进入生成器中的下一循环,i
变为1
,再次输出1
并冻结……直到完成所有迭代与输出,程序结束。
生成器(2):使用嵌入式循环语句产生一个生成器
def sqr2(n):
return (i**2 for i in range(n))
for i in sqr2(5):
print(i)
这个样例与上一个的工作流程相同,只不过,sqr2
返回值本身是一个generator object
生成器,在一行中包含了循环体和循环头, 较为精简,但与上面的程序功能完全相同。所以,当循环体较为简单(一个操作)时,可以使用样例2的方法,否则需要使用第一种方法。
Spider
模板Spider
,配置其具体功能Item Pipeline
,对Spider
获取内容进行进一步处理在配置并使用Scrapy
框架的过程中,我们会接触到三种数据类型:
Internet
提交的请求Spider
产生的信息class scrapy.http.Request()
Request
对象标识一个HTTP请求Spider
生成,由Downloader
执行Request
类的属性或方法属性/方法 | 说明 |
---|---|
.url |
Request对应的请求URL地址 |
.method |
与HTTP对应的请求方法 |
.headers |
HTTP请求头部信息,以字典形式组织 |
.body |
请求内容主体,为字符串类型 |
.meta |
添加的扩展信息,在Scrapy 内部模块间传递信息用 |
.copy() |
复制该请求 |
class scrapy.http.Response()
Response
对象标识一个HTTP响应Downloader
生成,由Spider
处理属性/方法 | 说明 |
---|---|
.url |
Response响应对应的URL地址 |
.status |
HTTP响应状态码 |
.headers |
Response对应的头部信息 |
.body |
Response对应的内容信息(str) |
.flags |
一组标记 |
.request |
产生Response类型对应的Request对象 |
.copy() |
复制该响应 |
class scrapy.item.Item()
Item
对象标识一个从HTML页面中提取的信息内容Spider
生成,由Item Pipeline
处理Item
类似字典类型,可以按照字典类型的方法进行操作Scrapy爬虫支持多种HTML信息提取方法,这些方法主要放置在Spider
模块下
BeautifulSoup
库,并将Response.body
作为分析材料进行文档分析)在Python 网络爬虫从0到1 (4):Beautiful Soup 4库入门详解中,我们已经介绍过Beautiful Soup
库的基本使用方法。本文我们将主要介绍另一个常用标准化信息提取工具,CSS Selector
CSS Selector
,即CSS选择器,是一种HTML文档分析和信息提取工具。由W3C
组织维护并规范,是标准化较好的文档分析工具,在Scrapy
框架下应用较为广泛。
在一个Resposne
响应对象或是Selector
选择器对象(scrapy.selector.unified.Selector
)中,可以使用.css()
方法,返回对象是一个Select
选择器对象,如果有多个符合条件的,则将返回一个列表,这也意味着,一个Selector对象是可迭代的,可使用for循环遍历每一个匹配内容。如果不指定编号,则默认返回第一个。
注意:必须使用.get()
方法才能获取到该选择器的对应内容(str
)
以下是几个示例:
# 1
next_page = response.css('li.next a::attr("href")').get()
# 2
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get()
}
选择器 | 语法 | 说明 | 样例 |
---|---|---|---|
通用选择器 | * ns|* |* | | 选择所有元素,|表示命名空间,见1 |
* 匹配所有,a|* 匹配命名空间a 下的所有元素 |
元素选择器 | element_name | 匹配指定节点名称的元素 | a 匹配所有超链接元素 |
类选择器 | .class_name | 匹配指定class 类属性的元素,见2 |
span.text 匹配所有类型为text 的span 元素 |
ID 选择器 | #id_name | 匹配一个指定ID 属性的元素,见3 |
#toc 匹配ID 为toc 的元素 |
属性选择器 | [attr] [attr=value] … | 匹配属性与匹配符符合的元素,见4 |
[href="/a"] 匹配href 属性为/a 的所有元素 |
命名空间和通用选择器符号用*
分隔,命名空间在左侧。特殊样例,|*
表示匹配不在任何命名空间中的元素,*|*
表示匹配在命名空间(无论是哪个)中的元素
类选择器前依然可以使用元素选择器,见上样例。若不加元素选择器,则会匹配任何该类元素而不考虑名称
由于在一个文档中,ID
是唯一的,所以ID
选择器最多匹配一个元素
匹配符并不只有一个,而是有一组,提供不同功能选项
匹配符 | 说明 | 样例 |
---|---|---|
[attr] | 带有某属性的元素 | [href] 表示带有href 属性的元素 |
[attr=value] | 属性等于某值的元素 | [href="/a"] 表示href 属性为/a 的元素 |
[attr~=value] | 该属性为一个列表,且一值匹配 | [a~="name"] 表示a 属性为列表,且有name 的元素 |
[attr|=value] | 表示属性值以value 或value- 开头的元素 |
[class|="a"] 表示class 属性以a 或a- 开头的元素,如a-name |
[attr&=value] | 表示属性值以value 结尾的元素 |
[href&="/s"] 表示href 属性以/s 结尾的元素 |
[attr*=value] | 表示多值属性值含value 的元素 |
[class*="name"] 表示class 属性含有name 的元素 |
~=
不同的是,*=
为多值属性,~=
表示属性值为列表span[itemprop="plain"].text
等分组选择器只有一个,,
可以将两个选择器组合在一起,以或
方式组合,即元素任意匹配一个选择器,就能被选中。
名称 | 形式 | 说明 | 样例 |
---|---|---|---|
后代组合器(Descendant combinator) | A B |
选择前一个节点A 的所有后代节点B |
div span 匹配所有div 元素下的span 元素 |
直接子代组合器(Child combinator) | A > B |
选择前一个节点A 的直接子代节点B |
div > span 匹配div 元素子代的span 元素 |
一般兄弟组合器(General sibling combinator) | A ~ B |
选择A 节点后的所有兄弟节点B |
p ~ a 匹配p 元素后面的a 兄弟元素 |
紧邻兄弟组合器(Adjacent sibling combinator) | A + B |
选择紧邻A 节点后的兄弟节点B |
p + a 匹配紧邻p 后的a 兄弟元素 |
CSS中定义了伪类,用于支持按照未被包含在文档中的状态信息来选择元素,指定了要选择的元素的特殊状态。如,a:link
表示所有未访问过的超链接。由于伪类大多描述元素交互状态,而对于爬虫来说影响较小,这里就不再赘述,有兴趣的朋友可查看伪类 - CSS层叠样式表 | MDN获得更多详细信息
伪元素用于选择匹配元素的特定部分,置于选择器最后
伪元素 | 说明 | 样例 |
---|---|---|
::text | 选中元素的文本信息(两尖括号包含内容) | span.text::text 表示获取class 属性为text 的span 元素的文本 |
::attr(“attribute”) | 选中元素的attribute 属性值 |
li.next a::attr("href") 获取类型为next 的li 元素的a 后代的href 属性值 |
::after | 表示已选中元素中的最后一个 | a::after 最后一个a 元素 |
::befor | 表示已选中元素中的第一个 | a::before 第一个a 元素 |
::first-letter | 表示选中块级元素中的第一个字符 | p::first-letter 表示p 元素内容中的第一个字符 |
::first-line | 表示选中块级元素中的第一行 | p::first-line 表示p 元素内容中的第一行 |
::selection | 表示文档中被用户高亮的部分 | ::selection 表示文档中被选中的部分 |
Pipelines
流水线处理模块由多个Pipeline
类组成。一个Pipeline
处理模块包含三个方法/函数,
方法/函数名称 | 是否必选 | 说明 |
---|---|---|
open_spider(self, spider) | 否 | 在爬虫被启动时调用的方法,一般作为流水线处理模块的初始化方法(打开文件等) |
process_item(self, item, spider) | 是 | 处理item 主要方法,接受来自spider 的 item 并处理 |
close_spider(self, spider) | 否 | 在爬虫结束时调用的方法,一般作为流水线处理模块的结束方法(关闭文件等) |
其中,process_item
函数一般将得到的item返回,方便其他流水线处理模块处理
流水线处理模块需要在settings.py
工程配置文件中注册并设置优先级,为字典格式,格式样例
ITEM_PIPELINES = {
'demo.pipelines.DemoPipeline': 300,
'demo.pipelines.QuotePipeline': 100
}
模块类名称
,后为优先级,越小越优先。
下面用一个样例复习主要知识点并了解其实际应用,这个样例向http://quotes.toscrape.com/
网站爬取某类名句以及作者,并保存在文件Quotes.json
中
创建一个工程
创建一个爬虫Quote
,domain
和start_urls
配置见下
修改爬虫框架中的代码
import scrapy
class QuotesSpider(scrapy.Spider):
name = 'Quotes'
allowed_domains = ['quotes.toscrape.com']
start_urls = ['http://quotes.toscrape.com/tag/inspirational/']
def parse(self, response, **kwargs):
# find the quotes and return the text and author
for quote in response.css('div.quote'):
yield {
# 'author': quote.xpath('span/small/text()').get(),
'author': quote.css('span small::text').get(),
'text': quote.css('span.text::text').get()
}
# find next page
next_page = response.css('li.next a::attr("href")').get()
if next_page is not None:
yield response.follow(next_page, self.parse)
下面主要讲解响应分析函数parse(self, response)
中的原理、流程
寻找页面中所有名句,并将其中文本与作者输出
首先分析网页,所有的名句都在class
属性为quote
的div
域元素下,则先选择所有该类元素。上文已经讲过,selection
类是可迭代类型,所以使用for
循环对其中名句一一分析返回。具体解析文本见CSS Selector
基本语法中的内容(包含伪元素)。
找到页面中的下一页链接并跟进爬取
分析网页,发现下一页链接在class
属性为next
的li
元素下的a
元素中的href
属性中,将其提取出,验证正确性后跟进爬取。
编辑pipelines.py
文件,为工程添加一个流水器处理模块,将内容保存在文件中
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
# useful for handling different item types with a single interface
# from itemadapter import ItemAdapter
import json
import os
class DemoPipeline:
def process_item(self, item, spider):
return item
class QuotePipeline:
def open_spider(self, spider):
self.f = open('Quotes.json', 'w')
self.f.write('[\n')
def process_item(self, item, spider):
try:
json.dump(item, self.f)
self.f.write(',\n')
except:
print('error')
return item
def close_spider(self, spider):
self.f.close()
self.f = open('Quotes.json', 'rb+')
self.f.seek(-3, os.SEEK_END)
# self.f.seek(-3, 2)
self.f.truncate()
self.f.write(b'\n]')
self.f.close()
添加的流水线处理模块名称为QuotePipeline
open_spider
,close_spider
的内容主要是文件IO以及开头与结尾的填充,process_item
使用json.dump
填充json
格式数据,收尾时为了去掉原文件的错误字符,需要使用文件截断,去掉,\n
共三个字节,可以不引用os
库,而将os.SEEK_END
常量直接替换为2
由于在配置pipeline
中使用了自定义类,需要在ITEM_PIPELINES
选项中进行设置
打开settings.py
,找到ITEM_PIPELINES
选项,进行编辑
ITEM_PIPELINES = {
'demo.pipelines.DemoPipeline': 300,
'demo.pipelines.QuotePipeline': 100
}
注册改流水线处理模块并设置优先级后,settings.py
配置就此完成
至此,配置全部完成,在终端中输入爬取命令,爬虫启动,文件保存在工程外层目录中
scrapy crawl Quotes