Scrapy爬虫
Scrapy初步认识
Scrapy模块安装
首先,我们需要先搭建好Scrapy的环境
利用conda创建一个名为Scrapy_work
的虚拟环境并且进入该环境
1 | conda create -n "Scrapy_work" |
然后使用pip安装Scrapy模块
1 | pip install Scrapy |
为了确认Scrapy模块已经安装成功,可以在Python当中测试一下能否导入Scrapy模块
1 | import scrapy |
如果正确安装的话,终端会显示Scrapy的版本
编写Scrapy爬虫
Scrapy框架结构及其工作原理
上图为Scrapy框架是组成结构
Scrapy框架中的各个组件的介绍如下所示
组件 | 描述 | 类型 |
---|---|---|
ENGINE | 引擎,框架的核心,其他所有组件在其控制下协同工作 | 内部组件 |
SCHEDULER | 调度器,负责对SPIDER提交的下载请求进行调度 | 内部组件 |
DOWNLOADER | 下载器,负责下载页面(发送HTTP请求和接收HTTP响应) | 内部组件 |
SPIDER | 爬虫,负责提取页面中的数据,并产生对新页面的下载请求 | 用户实现 |
MIDDLEWARE | 中间件,负责对Request对象和Response对象进行处理 | 可选组件 |
ITEM PIPELINE | 数据管道,负责对爬取到的数据进行处理 | 可选组件 |
对于用户来说,Spider
是最核心的组件,爬虫的开发都是围绕Spider
组件展开的
接下来是框架中的数据流
对象 | |
---|---|
REQUEST | Scrapy的HTTP请求对象 |
RESPONSE | Scrapy的HTTP响应对象 |
ITEM | 从页面中爬取的一项数据 |
数据的流动过程简述如下:
- 当
SPIDER
要爬取某URL地址的页面时,需要使用该URL构造一个Request
对象,提交给ENGINE
Request
对象随后进入SCHEDULER
按某种算法进行排队,之后的某个时刻SCHEDULER
将其出队,送往DOWNLOADER
DOWNLOADER
根据Request
对象中的URL地址发送一次HTTP请求到网站的服务器,之后用服务器返回的HTTP响应构造出一个Response
对象,其中包含页面的HTML文本Response
对象最终会被送到SPIDER
的页面解析函数处进行处理,页面解析函数可以从页面中提取数据,并且封装为ITEM
之后提交给ENGINE
,之后被送往ITEM PIPELINE
进行处理,最后由EXPORTER
以某种数据格式写入文件;除此之外,页面解析函数还可以从页面中提取链接,构造出一个新的Request
对象提交给ENGINE
Request和Response对象
Request对象
1 | class scrapy.http.Request(url[, callback, method='GET', headers, body, cookies, meta, encoding='utf-8', priority=0, dont_filter=False, errback, flags]) |
- url:请求页面的url地址
- callback:页面解析函数,
Request
对象请求的页面在下载完成之后,由该参数指定的页面解析函数被调用,如果未传递该参数,则默认调用Spider的parse
方法 - method:HTTP请求的方法,默认为
GET
- headers:HTTP请求的头部字典,dict类型
- body:HTTP请求的正文
- cookies:Cookies信息字典,dict类型
- meta:Request的元数据字典,用于给框架中其他组件传递信息
- encoding:url和body参数的编码默认为
UTF-8
,如果传入的参数是str
类型,则使用该参数进行编码 - priority:请求的优先级默认值为0,优先级高的请求优先下载
- dont_filter:如果对同一个url地址多次提交下载请求,后面的请求会被屏蔽,将此参数设置为
True
可以避免请求被过滤,强制下载 - errback:请求出现异常或者出现HTTP错误时的回调函数
- flag:一个Request请求的标记,一般用于log记录
Response对象
Response对象是用来描述一个HTTP响应的,Response
只是一个基类,根据响应内容的不同有如下子类:
- TextResponse
- HtmlResponse
- XmlResponse
当一个页面下载完成后,下载器就依据HTTP响应头部中的Content-Type
信息创建某个Response的子类对象
1 | class scrapy.http.Response(url[, status=200, headers=None, body=b'', flags=None, request=None]) |
- url:HTTP响应的url地址
- status:HTTP响应的状态码
- headers:HTTP响应的头部,类字典类型,可以用
get
或getlist
方法进行访问,例如:
1 | response.headers.get('Content-Type') |
- body:HTTP响应正文
- text:文本形式的HTTP响应正文
- encoding:HTTP响应正文的编码
- request:产生该HTTP响应的
Request
对象 - meta:即
response.request.meta
- selector:用于在
Response
中提取数据 - xpath:使用XPath选择器在
Response
中提取数据 - css:使用CSS选择器在
Response
中提取数据 - urljoin:用于构造绝对url
Spider开发流程
一般而言,实现一个Spider只需要以下几个步骤:
- 继承
scrapy.Spider
- 为
Spider
取名 - 设置起始的爬取点
- 实现页面解析函数
使用Selector提取数据
我们要从页面中提取数据需要进行HTTP文本解析,一般用以下模块进行处理:
- BeautifulSoup
- lxml
Scrapy综合以上两者的优点实现了Selector
类
在Scrapy中使用Selector对象提取页面中的数据,使用前先通过选择器选中页面中需要提取的数据,然后进行提取
Selector对象
Selector类的实现位于scrapy.selector
模块
创建对象
我们可以将页面的HTML文档字符串传递给Selector
构造器方法的text
参数
也可以使用一个Response
对象构造Selector
对象,将其传递给Selector
构造器方法的response
参数
选中数据
调用Selector
对象的XPath方法或CSS方法,可以选中文档中的某个或某些部分
XPath方法和CSS方法返回一个SelectorList
对象,其中包含每个被选中部分对应的Selector
对象
SelectorList
对象也有XPath和CSS方法,但是不同的是这里的方法是调用SelectorList
对象当中的每一个Selector
对象的XPath和CSS方法,并将结果收集到一个新的SelectorList
对象当中,然后返回给用户
提取数据
调用Selector
或SelectorList
对象的以下方法可以将选中的内容提取出来
extract()
re()
extract_first()
(SelectorList
对象专有,提取第一个Selector
对象的数据)re_first()
(SelectorList
对象专有,对第一个Selector
对象进行正则匹配)
Response内置Selector
在我们实际的开发中,一般不需要手动创建Selector
对象
因为在第一次访问一个Response
对象的selector
属性时,Response
对象内部会以自身为参数自动创建Selector
对象,并将该对象缓存,以便下次使用
为了方便,Response
对象还内置了XPath和CSS方法,它们会在内部调用内置Selector
对象的XPath和CSS方法
Xpath
Xpath即XML路径语言,它是一种用来确定XML文档中某部分位置的语言
XML文档的节点有多种类型,其中最常用的有以下几种:
- 根节点(整个文档树的根)
- 元素节点(html,body,div,p,a)
- 属性节点(href)
- 文本节点(正文)
节点之间的关系有以下几种:
- 父子
- 兄弟
- 祖先/后裔
基础语法
下表列出了XPath常用的基本语法
表达式 | 描述 |
---|---|
/ | 选中文档的根 |
. | 选中当前节点 |
… | 选中当前节点的父节点 |
ELEMENT | 选中子节点中所有的ELEMENT元素节点 |
//ELEMENT | 选中后代节点中所有ELEMENT元素节点 |
* | 选中所有元素子节点 |
text() | 选中所有文本子节点 |
@ATTR | 选中名为ATTR的属性节点 |
@* | 选中所有属性节点 |
[谓语] | 谓语用来查找某个特定的节点或者包含某个特定值的节点 |
常用函数
XPath还提供许多函数,如数字,字符串,时间,日期,统计等等,例如:
- position() 选中指定位置
- last() 选中最后一个
- string() 返回参数的字符串值
- contains(str1,str2) 判断str1中是否包含str2,返回bool值
CSS选择器
基础语法
表达式 | 描述 | 例子 |
---|---|---|
* | 选中所有元素 | * |
E | 选中E元素 | p |
E1,E2 | 选中E1和E2元素 | div,pre |
E1 E2 | 选中E1后代元素中的E2元素 | div p |
E1>E2 | 选中E1子元素中的E2元素 | div>p |
E1+E2 | 选中E1兄弟元素中的E2元素 | p+strong |
.CLASS | 选中CLASS属性包含CLASS的元素 | .info |
#ID | 选中id属性为ID的元素 | #main |
[ATTR] | 选中包含ATTR属性的元素 | [href] |
[ATTR=VALUE] | 选中包含ATTR属性且值为VALUE的元素 | [method=post] |
[ATTR~=VALUE] | 选中包含ATTR属性且值包含VALUE的元素 | [class~=clearfix] |
E:nth-child(n) E:nth-last-child(n) |
选中E元素,且该元素必须是其父元素的(倒数)第n个子元素 | a:nth-child(1) |
E:first-child E:last-child |
选中E元素,且该元素必须是其父元素的(倒数)第一个子元素 | a:first-child |
E:empty | 选中没有子元素的E元素 | div:empty |
E::text | 选中E元素的文本节点 | p::text |
使用Item封装数据
我们在存储数据的时候,经常会使用字典进行存储,但是字典会有以下的缺点:
- 无法一目了然地了解数据当中的字段,影响代码可读性
- 缺乏对字段名字的检测,容易因为笔误而出错
- 不便于携带元数据(传递给其他组件的信息)
Item和Field
Scrapy定义了两个类,用户可以用它们自定义数据类,封装爬取到的信息:
Item
基类:自定义数据类的基类Field
类:用来描述自定义数据类型包含哪些字段(如name,price等)
1 | from scrapy import Item,Field |
拓展Item子类
有时候如果需要对已经自定义的数据类进行拓展的话,可以直接继承之前的类,然后在定义中加入新的Field
使用Item Pipeline处理数据
Item Pipeline
是负责处理数据的组件,在一个项目中可以同时启用多个Item Pipeline
,按照指定次序级联起来,形成一条数据流水线
以下是几种典型应用:
- 清洗数据
- 验证数据的有效性
- 过滤掉重复的数据
- 将数据存入数据库
Item Pipeline
在创建一个Scrapy项目时,会自动生成一个pipelines.py
文件,它用来存放用户自定义的Item Pipeline
Item Pipeline
不需要继承特定的基类,只需要实现特定的方法即可
一个Item Pipeline
必须实现一个process_item(item,spider)
方法
其中item
是爬取到的一项数据,Spider
是爬取到此数据的Spider
对象
如果
process_item(item,spider)
在处理某项item时返回了一项数据,返回的数据会递送给下一级Item Pipeline
继续处理如果
process_item(item,spider)
在处理某项item时抛出了一个DropItem
异常,则该项item会被抛弃,不再递送给后面的Item Pipeline
进行处理
除了必须实现的process_item(item,spider)
之外,还有3个比较常用的方法:
open_spider(self,spider)
在Spider打开时回调该方法,常用于初始化,如连接数据库close_spider(self,spider)
在Spider关闭时回调该方法,常用于处理完所有数据之后的清理工作,如关闭数据库from_crawler(cls,crawler)
创建Item Pipeline
时回调该方法,通常通过crawler.settings
读取配置,根据配置创建Item Pipeline
对象
在Scrapy中,Item Pipeline
是可选的组件,想要启用的时候需要在settings.py
中进行配置
使用LinkExtractor提取链接
一般而言,提取链接有两种方法:
- 使用Selector
- 使用LinkExtractor
当需要提取大量链接或提取的规则比较复杂时,使用LinkExtractor更加方便
使用LinkExtractor
一般使用LinkExtractor
模块的流程如下:
- 导入
LinkExtractor
模块 - 创建一个
LinkExtractor
对象,使用一个或多个构造器参数描述提取规则 - 调用
LinkExtractor
对象的extract_links
方法,并传入一个Response
对象,该方法会依据创建对象时的提取规则去从Response
对象包含的页面中提取链接,并返回一个由绝对url构成的列表,可以用其直接构造Request
对象
描述提取规则
1 | classscrapy.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) |
在Scrapy早期的版本中也有其他的
LinkExtractor
类,但是现在它们被弃用了
allow
接收一个正则表达式或者正则表达式列表,用于提取绝对url,如果该参数为空则提取全部链接deny
接收一个正则表达式或者正则表达式列表,用于排除匹配的urlallow_domains
接收一个域名或者域名列表,提取到指定域的链接deny_domains
接收一个域名或者域名列表,排除到指定域的链接deny_extensions
在接收时需要忽略的扩展名restrict_xpaths
接收一个XPath表达式或表达式列表,提取XPath表达式选中区域下的链接restrict_css
接收一个CSS选择器或选择器列表,提取CSS选择器选中区域下的链接tag
接收一个标签或标签列表,提取指定标签内的链接attrs
接收一个属性或一个属性列表,提取指定属性内的链接canonicalize
对提取的url进行规范化处理,常设置为False
unique
是否应对提取的链接进行重复过滤process_value
接收一个形如func(value)
的回调函数,LinkExtractor
将会调用该回调函数对提取的每一个链接进行处理strip
是否从提取的属性中去除空格,因为LinkExtractor
会自动去除空格
使用Exporter导出数据
在Scrapy中,负责导出数据的组件被称为Exporter
Scrapy实现了多个Exporter
,每个Exporter
支持一种数据格式的导出,支持的数据格式如下所示:
- JSON
- JSON lines
- CSV
- XML
- Pickle
- Marshal
如果需要导出为其他数据,则需要自己实现Exporter
指定导出数据
在导出数据时,需要向爬虫提供以下信息:
- 导出文件的路径
- 导出的数据格式
在配置文件中,经常用到以下几个选项:
FEED_URL
导出文件的路径FEED_FORMAT
导出的数据格式FEED_EXPORT_ENCODING
导出文件编码
默认情况下JSON文件使用数字编码,其他使用UTF-8编码
FEED_EXPORT_FIELDS
导出数据包含的字段(默认导出所有字段),并指定次序FEED_EXPORTERS
用户定义新的Exporter字典,添加新的导出数据格式的时候使用
用户自定义的导出数据格式的代码编写此处不赘述,略
下载文件和图片
下载文件也是爬虫很常用的一种需求,例如图片,视频,文档,压缩包等等
Scrapy框架内置了两个Item Pipeline
专门用于下载文件和图片
FilesPipeline
ImagesPipeline
FilesPipeline的使用
FilesPipeline的使用步骤一般如下所示:
- 在配置文件
settings.py
中启用FilesPipeline
,通常是将其置于其他的Item Pipeline
之前 - 在配置
settings.py
时,使用FILES_STORE
指定文件的下载目录 - 在Spider解析一个包含文件下载链接的页面时,将所有需要下载文件的url地址收集到一个列表中,然后赋值给
item
的file_urls
字段(item[‘file_urls’])
FilesPipeline在处理每一项item时都会进行读取,然后对其中的每一个url进行下载
当FilesPipeline下载完所有的文件后,就会将下载的结果信息存储到另一个列表中去,下载的结果包含以下内容:
- 文件下载到本地的路径
- Checksum文件的校验和
- url文件的url地址
ImagesPipeline的使用
图片的本质也是文佳,ImagesPipeline是FilesPipeline的子类,使用上大同小异,只是配置的选项上有细微的差别
FilesPipeline | ImagesPipeline | |
---|---|---|
导入路径 | scrapy.pipelines.files.FilesPipeline | scrapy.pipelines.images.ImagesPipeline |
Item字段 | file_urls,files | image_urls,images |
下载目录 | FILES_STORE | IMAGES_STORE |
ImagesPipeline在FilesPipeline的基础上针对图片增加了一些特有的功能
- 为下载的图片生成缩略图
要开启此功能,只需要在配置文件settings.py
中设置IMAGES_THUMBS
,例如:
1 | IMAGES_THUMBS = { |
- 过滤掉尺寸较小的图片
开启该功能需要在配置文件settings.py
中设置IMAGES_MIN_WIDTH
和IMAGES_MIN_HEIGHT
,它们分别指定最小尺寸的图片的宽和高,例如:
1 | IMAGES_MIN_WIDTH = 110 |
模拟登录
目前,大部分网站都需要登录才能获取资源,所以有时候爬虫需要先模仿用户登录,再去爬取内容
登录的实质就是向服务器发送含有登录表单数据的HTTP请求(通常是POST),Scrapy提供了一个FormRequest
类,专门用于构造含有表单数据的请求
Scrapy的登录模块
FormRequest
的构造器方法有一个formdata
参数,接收字典形式的表单数据
我们还可以直接调用FormRequest
的from_response
方法,调用时需要传入一个Response
对象作为一个参数,该方法会解析Response
对象中所包含页面中的<form>
元素,帮助用户创建FormRequest
对象,并自动填入表单数据,我们只需要通过formdata
参数将账号和密码填入即可
识别验证码
目前很多网站为了防止爬虫爬取,登录的时候需要用户输入验证码,一般有以下几种方法去识别验证码:
- OCR识别
我们可以使用Python的第三方库进行识别(例如tesseract-ocr
)
- 网络平台API
也可以去验证码识别平台购买相关服务套餐,接入API
- 人工识别
在其他方法不管用的时候,我们也可以等Scrapy下载完验证码图片后,调用Image.show
方法来显示图片,然后再调用Python内置的input
函数来捕获用户的输入
Cookie登录
我们不需要去研究各个浏览器将Cookie以何种形式存储在何处,只需要调用第三方库browsercookie
即可获得Chorme和Firefox浏览器中的Cookie
爬取动态页面
我们之前讨论的都是静态页面的信息爬取,但是在实际应用中,大多数页面都是动态页面,页面中的部分内容是需要运行页面中的JavaScript脚本动态生成的,爬取相对困难
实际中更常见的是JavaScript通过HTTP请求跟网页动态交互获取数据(Ajax),然后使用数据去更新HTML页面
爬取此类网站需要先执行页面中的JavaScript代码去渲染页面,然后再进行爬取
Splash渲染引擎
Splash是Scrapy官方推荐的JavaScript渲染引擎,提供基于HTTP端口的JavaScript渲染服务,支持以下的功能:
- 为用户返回经过渲染的HTML页面或者页面截图
- 并发渲染多个页面
- 关闭图片加载,加速渲染
- 在页面中执行用户自定义的JavaScript代码
- 执行用户自定义的渲染脚本(lua)
Splash的功能丰富,包含了多个服务端点,此处仅介绍两个最常用的端点:
- render.html 提供JavaScript页面渲染服务
- execute 执行用户自定义的渲染脚本(lua),利用该端点可以在页面中执行JavaScript代码
render.html端点
服务端点 | render.html |
---|---|
请求地址 | http://localhost:8050/render.html |
请求方式 | GET / POST |
返回类型 | HTML |
render.html端点支持的参数如下表所示(仅列出常用的参数)
参数 | 是否必选 | 类型 | 描述 |
---|---|---|---|
url | 必选 | string | 需要渲染的页面的url |
timeout | 可选 | float | 渲染页面的超时时间 |
proxy | 可选 | string | 代理服务器地址 |
wait | 可选 | float | 等待页面渲染的时间 |
images | 可选 | integer | 是否下载图片,默认为1 |
js_source | 可选 | string | 用户自定义的JavaScript代码,在页面渲染前执行 |
execute端点
在爬取一些页面的时候,我们想在页面中执行一些用户自定义的JavaScript代码,可以通过利用Splash的execute端点来实现
服务端点 | execute |
---|---|
请求地址 | http://localhost:8050/execute |
请求方式 | POST |
返回类型 | HTML |
execute支持的参数如下表所示
参数 | 是否必选 | 类型 | 描述 |
---|---|---|---|
lua_source | 必选 | string | 用户自定义的lua脚本 |
timeout | 可选 | float | 渲染页面的超时时间 |
proxy | 可选 | string | 代理服务器地址 |
我们可以将execute端点的服务看作是一个可以用lua语言编程的浏览器,使用时需要传递一个用户自定义的lua脚本给Splash,该lua脚本中包含用户想要模拟的浏览器行为,例如:
- 打开某url地址的页面
- 等待页面加载或渲染
- 执行JavaScript代码
- 获取HTTP响应头部
- 获取Cookie
用户自定义的lua脚本中必须包含一个main函数作为程序的入口,main函数被调用的时候会传入一个Splash对象(lua中的对象),用户可以调用该对象上的方法操纵Splash
在Scrapy中使用Splash
如果我们需要在Scrapy中调用Splash服务,Python的scrapy-splash
库是非常好的选择
我们在安装好Python包之后,需要在settings.py
中对其进行配置
1 | # 渲染服务的url |
然后再编写代码爬取网页(以百度知道为例)
1 | import scrapy |
存入数据库
我们在爬取到数据之后,有些时候需要将其存储到数据库中
SQLite
SQLite是一个轻量级数据库,在数据量很小的情况下,它的处理速度很快,使用它足够了
SQLite数据库的文件创建和表创建在此不做赘述
在Python中,可以使用Python标准库的sqlite3
模块去访问SQLite数据库
在配置文件settings.py
时,我们需要指定所要使用的SQLite数据库,并启用和SQLite相关的Pipeline
1 | SQLITE_DB_NAME = 'scrapy.db' |
MySQL
MySQL的应用极其广泛,它是开源免费的,可以支持大型数据库,是个人用户和中小企业的技术首选
此次仍然不赘述MySQL数据库的创建和表的创建
Python可以使用mysqlclient
模块访问MySQL数据库
在配置文件settings.py
时,我们需要指定所要使用的MySQL数据库,并启用和MySQL相关的Pipeline
1 | MYSQL_DB_NAME = 'scrapy_db' |
MongoDB
MongoDB是一个面向文档的非关系型数据库,近年来得到了广泛应用
Python可以使用pymongo
模块访问MongoDB数据库
在配置文件settings.py
时,我们需要指定所要使用的MongoDB数据库,并启用和MongoDB相关的Pipeline
1 | MONGODB_URL = 'mongodb://localhost:27017' |
Redis
Redis是一个使用ANSI C编写的高性能Key-Value数据库,使用内存作为主存储
在Python中可以使用redis-py
访问Redis数据库
在配置文件settings.py
时,我们需要指定所要使用的Redis数据库,并启用和Redis相关的Pipeline
1 | REDIS_HOST = 'localhost' |
使用HTTP代理
HTTP的代理服务器可以比作客户端和服务器之间的一个信息中转站,客户端发送的HTTP请求和Web返回的HTTP响应通过代理服务器转发给对方
HttpProxyMiddleWare
Scrapy内部提供了一个下载的中间件HttpProxyMiddleWare
,专门用于给Scrapy爬虫设置代理
默认情况下,HttpProxyMiddleWare
是启用的,它会在系统的环境变量中搜索当前的系统代理(名字格式为xxx_proxy的环境变量)作为Scrapy爬虫所使用的代理
多个代理
当利用HttpProxyMiddleWare
进行代理的时候,对于一种协议(HTTP或HTTPS)的所有请求只能使用一个代理,如果想使用多个代理,可以在构造每一个Request
对象时,通过meta
参数的proxy
字段手动设置
随机代理
某些网站为了反爬虫,经常会监测IP的访问量,如果同一IP在短时间内大量访问的话,服务器会将其封禁
爬虫可以使用多个代理对此类网站进行爬取,此时的访问量会被多个代理所分摊,从而避免封禁IP
我们只需要在middlewares.py
中实现随机使用代理的算法即可
分布式爬虫
由于受到计算能力和网络带宽的限制,单个计算机上运行的爬虫在爬取的数据量较大的情况下,需要耗费较多的时间
scrapy-redis
库为Scrapy拓展了分布式爬取的功能,可以让网络中的多台计算机上同时运行爬虫程序