自学Python第十六天-Scrapy框架创建爬虫
Scrapy 框架是 python 开发的一个快速,高层次的屏幕抓取和 web 抓取框架,用于抓取 web 站点并从页面中提取结构化的数据。它采用了
Twisted
异步网络框架,可以大大的增加下载速度。
Scrapy的用途广泛,可以用于数据挖掘、监测和自动化测试。Scrapy 吸引人的地方在于它是一个框架,任何人都可以根据需求方便的修改。并且它也提供了多种类型爬虫的基类,如 BaseSpider、sitemap 爬虫等。
运行原理
普通的爬虫工作流程大概是:
上面的流程可以改写成以下结构:
Scrapy
框架的执行流程:
流程描述
- 爬虫中起始的
url
构造成request
对象–>爬虫中间件–>引擎–>调度器 - 调度器把
request
–>引擎–>下载中间件–>下载器 - 下载器发送请求,获取
response
响应---->下载中间件---->引擎—>爬虫中间件—>爬虫 - 爬虫提取
url
地址,组装成request
对象---->爬虫中间件—>引擎—>调度器,重复第二个步骤 - 爬虫提取数据—>引擎—>管道处理和保存数据
注意点
- 图中绿色线条表示数据的传递
- 注意图中中间件的位置,决定了其作用
- 注意其中引擎的位置,所有的模块之前相互独立,只和引擎进行交互
上图中的1 - 12序号的解释说明:
Scrapy
从Spider
子类中提取start_urls
,然后构造为request
请求对象- 将
request
请求对象传递给爬虫中间件 - 将
request
请求对象传递给Scrapy
引擎(就是核心代码) - 将
request
请求对象传递给调度器(它负责对多个request
调度,好比交通管理员负责交通的指挥员) - 将
request
请求对象传递给Scrapy
引擎 Scrapy
引擎将request
请求对象传递给下载中间件(可以更换代理IP
,更换Cookies
,更换User-Agent
,自动重试。等)request
请求对象传给到下载器(它通过异步的发送HTTP(S)
请求),得到响应封装为response
对象- 将
response
对象传递给下载中间件 - 下载中间件将
response
对象传递给Scrapy
引擎 Scrapy
引擎将response
对象传递给爬虫中间件(这里可以处理异常等情况)- 爬虫对象中的
parse
函数被调用(在这里可以对得到的response
对象进行处理,例如用status
得到响应状态码,xpath
可以进行提取数据等) - 将提取到的数据传递给
Scrapy
引擎,它将数据再传递给管道(在管道中我们可以将数据存储到csv
、MongoDB
等)
Scrapy 的主要对象和模块
Scrapy
内置的三个对象
request
请求对象:由url
、method
、post_data
、headers
等构成response
响应对象:由url
、body
、status
、headers
等构成item
数据对象:本质是个字典
Scrapy
的主要组件
Scrapy 主要由以下几个组件组成:
- Scrapy Engine (Scrapy 引擎)
用来处理整个系统的数据流处理,触发事务(框架核心) - Scheduler (调度器)
调度器存放的是需要爬取的页面链接的列表。具体的说是用来接受引擎发过来的请求,压入队列中,并在引擎再次请求的时候返回。可以想象成一个URL(抓取网页的网址或者说是链接)的优先队列,由它来决定下一个要抓取的网址是什么,同时去除重复的网址 - Downloader (下载器)
用于下载网页内容,并将网页内容返回给爬虫(Scrapy 下载器是建立在 twisted 这个高效的异步模型上的) - Spiders (爬虫)
用于从特定的网页中提取自己需要的信息(解析下载器下载的页面数据),即实体(Item) - Item (实体)
爬取数据的信息实体,即实际的信息数据 - Pipeline (项目管道)
负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。 - Downloader Middlewares (下载器中间件)
位于 Scrapy 引擎和下载器之间的框架,主要是处理 Scrapy 引擎与下载器之间的请求及响应 - Spider Middlewares (爬虫中间件)
介于引擎和爬虫之间的框架,主要工作是处理爬虫的响应输入和请求输出 - Scheduler Middlewares (调度中间件)
介于引擎和调度之间的框架,处理从引擎发送到调度的请求和响应
Scrapy
内置模块图解
注意:爬虫中间件和下载中间件只是运行逻辑的位置不同,作用是重复的:如替换User-Agent
等。
安装
Scrapy 是一个框架,由多个模块组成 。
注:网上有说非顺序安装会报错。但是直接安装 scrapy 可以顺带安装需要的支持库。
不过需要注意的是:
- scrapy 需要的
Twisted
版本限制在 22.10.0版本,而默认安装scrapy
的twisted
版本可能会过高,会报错AttributeError:'AsyncioSelectorReactor' object has no attribute '_handleSignals'
。 parse
版本需要限制在1.7.0,否则会报警告:UserWarning: Selector got both text and root, root is being ignored. super().__init__(text=text, type=st, root=root, **kwargs)
cryptography
版本需要使用36.0.2,否则会报错:twisted.web._newclient.ResponseNeverReceived: [<twisted.python.failure.Failure OpenSSL.SSL.Error: [(‘SSL routines’, ‘’, ‘unsafe legacy renegotiation disabled’)]>
scrapy
Scrapy 主引擎模块,使用 pip install scrapy
进行安装
使用 scrapy 项目
一个 scrapy 爬虫的流程为:
- 新建项目(Project): 新建一个新的爬虫项目
- 明确目标(Items): 明确想要抓取的目标
- 制作爬虫(Spider): 制作爬虫开始爬取网页
- 存储内容(Pipeline): 设计管道存储爬取内容
创建项目
当安装完各需要的库后,就可以创建项目了。在 cmd 中,进入需要进入的目录,然后执行命令 scrapy startproject 项目名称
。
当看到如下提示时,说明项目创建成功,且在目录下建立了一个项目名称命名的文件夹:
scrapy startproject 项目名称
New Scrapy project '项目名称', using template directory 'C:\Program Files\Python310\lib\site-packages\scrapy\templates\project', created in:
项目目录\项目名称
You can start your first spider with:
cd 项目名称
scrapy genspider example example.com
创建好了后,目录结构为:
- scrapy.cfg : 项目的配置文件
- 项目名称/ : 项目的 python 模块,将会从这里引用代码
- 项目名称/items.py :项目的 items 文件,用来存放抓取内容容器的文件
- 项目名称/pipelines.py :负责处理爬虫从网页中抽取的实体,持久化实体、验证实体有效性、清除不需要的信息
- 项目名称/settings.py :项目的设置文件
- 项目名称/spiders/ :存储爬虫的目录
创建爬虫
创建一个继承 scrapy.Spider
的子类,且定义以下三个属性一个方法:
name
: 用于区别 Spider ,该名字必须是唯一的allowed_domains
: 确定爬取的域名列表。有时候从页面中获取需要爬取的 url 可能会获取到其他网站的域名,如果不在列表中则忽略爬取。如不需要使用可以注释掉。start_urls
: 包含了 Spider 在启动时进行爬取的 url 列表,初始 url 是其中之一。后续的 url 则从初始 url 获取到的数据中提取parse()
: Spider 的一个方法,每个初始 url 完成下载后生成的 Response 对象将会作为唯一的参数传递给该函数进行处理。该方法负责解析返回的数据(response data)、提取数据(生成item)以及生成需要进一步处理的 url 的 Request 对象
根据提示,进入项目工作目录,使用 scrapy genspider 爬虫名称 爬取页面url
来创建爬虫。
cd doubanTest
scrapy genspider top250 movie.douban.com/top250
Created spider 'top250' using template 'basic' in module:
doubanTest.spiders.top250
这样就创建了 top250.py 这个爬虫文件。修改一下需要的数据,完成 parse()
方法,就可以了。
response
对象的常用属性和方法
scrapy.Spider
类的 parse()
方法是 scrapy
爬虫的一个回调方法,此方法接受一个 response
对象参数,该对象是 scrapy.http.HtmlResponse
类。response
对象里的内容就是请求获取的响应数据。该对象有一些常用的属性和方法用来访问这些数据:
属性 | 说明 |
---|---|
response.url | 响应的 url 地址 |
response.headers | 响应头信息,格式为字典格式。但是字典中使用的不是字符串,而是字节格式 |
response.status | 响应状态码 |
response.body | 响应体,字节类型 |
response.text | 文本内容 |
response.request | 请求对象 |
response.request.url | 请求地址 |
response.request.headers | 请求头 |
response
对象的解析方法和属性也适用于 selector
对象。
方法或属性 | 说明 |
---|---|
css() | 使用 css 选择器解析 response 并返回一个 selector 节点对象 |
xpath() | 使用 xpath 解析 response 并返回一个 selector 节点对象 |
re() | 使用正则解析并返回一个字符串 |
extract() 或 getall() | 获取 selector 对象的的数据列表 |
extract_first() 或 get() | 获取 selector 对象的第一个匹配数据值 |
attrib | 获取selector 对象的属性字典 |
解析提取实体
在 parse()
方法中,可以使用 css 选择器解析,也可以使用 xpath 解析。另外scrapy 使用了一种基于 xpath 和 css 表达式机制的解析: scrapy selectors (selectors 选择器)。parse()
方法中传入的参数 response 就是一个 Selector
对象,也可以使用 Selector(response.text)->Selector
将 HTML 文本创建成 selector 对象。
selector 对象可以使用 extract()
方法和 extrcat_first()
方法来获取需要的数据,区别在于第一种方法会获取一个列表,而第二种方法会获取第一个匹配结果。也可以使用 getall()
方法和 get()
方法,这两种方法是之前两种的简写。
另外可以使用 attrib
对象获取属性字典。例如 selector.attrib['href']
来获取元素的 href
的值。也可以使用 xpath 的 @
获取属性对象,并使用get()
方法获取其数据。例如selector.xpath('//a/@href').get()
css 选择器解析
可以使用 .css()
方法返回一系列的 selectors ,每一个selector 表示一个 css 参数表达式选择的节点。
使用 Selector.css(css选择器文本).get()
来获取需要的内容,例如:
response.css('span::text').get() # 获取 span 标签内的文本内容
xpath 解析
可以使用 .xpath()
方法返回一系列的 selectors ,每一个selector 表示一个 xpath 参数表达式选择的节点。
使用 Selector.xpath(xpath文本).get()
来获取需要的内容,例如:
response.xpath('//span/text()').get() # 获取 span 标签内的文本内容
response.xpath('//div/span[contains(text(),"文本内容")]').extract() # 根据文本内容定位标签
re 解析
可以使用 .re()
方法返回一个 unicode 字符串,内容为正则表达式抓取的内容
解析完成
parse()
方法解析完成后,数据会返回给引擎,由引擎判断数据类型。如果是 BaseItem
、Dict
或None
,则交给管道进行处理;如果是request
,则交给调度器,进入下载队列。
将解析的实体数据存入容器
管道会将 parse()
方法作为迭代器调用,并将相应的数据交给容器进行处理,例如持久化保存。
数据容器已经设置过,在 items.py 里,所以需要先在爬虫文件中导入 items.py 中的类(类名称根据项目名称不同而改变),例如:
from doubanTest.items import DoubantestItem
然后在 parse()
方法中,创建容器实例并将解析的数据存储到实例中相应的字段中。例如:
def parse(self, response):
lis = response.css('.grid_view').css('li') # 获取 class='grid_view' 的标签下的 li 标签列表
for li in lis: # 在每个 li 标签里
name = li.xpath('.//span[@class="title"]/text()').get() # 查找 class='title' 的 span 标签,取文本内容
stars = li.css('span.rating_num::text').get() # 查找 class='rating_num 的 span 标签,取文本内容
critical = li.xpath('.//div[@class="star"]/span')[-1].xpath('./text()').get()[:-3]
quote = li.css('p.quote>span::text').get()
# 创建 item 容器,将解析到的数据存放到容器中
item = DoubantestItem(name=name,stars=stars,critical=critical,quote=quote)
# 将 item 容器传送到 pipeline
yield item
# 或直接返回字典
# yield {'name': name,'stars': stars,'critical': critical,'quote': quote}
也可以使用 yield
直接返回字典,字典的 key
需要和 item 的名称对应。需要注意的是解析方法中的yield
能够传递的对象只能是:BaseItem
、Request
、dict
、None
启动爬虫的准备
启动爬虫前,要进行一些准备:
- 取消 robots 协议
在设置文件setting.py
中,找到 ROBOTSTXT_OBEY,设置为False
- 修改请求头,防止反扒。
在设置文件settings.py
中,找到被注释的 DEFAULT_REQUEST_HEADERS ,取消注释并修改内容添加UA即可。(UA也可以在USER_AGENT 字段中配置) - 设置导出数据的格式
在设置文件settings.py
中,添加 FEED_FORMAT ,其值可以设为 json、json lines、csv 和 xml - 设置导出文件路径
在设置文件settings.py
中,添加 FEED_URI ,其值为导出文件路径,支持 FTP 等协议,也可以保存为本地文件,例如file:///d:/tmp/export.csv
,需要注意的是绝对路径 - 设置导出字段及顺序
在设置文件settings.py
中,添加 FEED_EXPORT_FIELDS ,其值为导出字段的文本列表,例如 [‘name’, ‘stars’, ‘critical’, ‘quote’] - 设置等待延迟(因为scrapy是异步的,防止请求太快被封)
在设置文件settings.py
中,找到注释的 DOWNLOAD_DELAY,取消注释。参数值会乘以 0.5-1.5 之间的数
启动爬虫
在控制台使用 scrapy crawl 爬虫名称
来启动爬虫。启动后会看到很多 scrapy 的输出日志。可以使用 scrapy crawl 爬虫名称 --nolog
来屏蔽默认的输出日志。但是需慎用,因为一旦屏蔽,代码报错信息也无法显示。
另外可以使用 scrapy crawl 爬虫名称 -o 文件名 -t 输出格式
保存信息到文件中(需要在 settings.py
中正确配置),支持 json,xml,csv格式
爬取流程
一个最简单的流程就是通过开始链接列表初始化 url 并交给调度器,然后调度器封装 request 交给下载器,下载器下载的数据 response 交给爬虫,爬虫解析并处理数据,将数据交给管道或将需要的链接交给调度器继续下载并爬取,管道进行数据处理并保存。
使用解析出的 url (下一页)
在parse()
方法中获取的url,例如需要下载的文件、图片,或继续访问的链接(例如翻页),可以打包为 request
对象,并指定处理回调。引擎会将此对象交给调度器,在合适时候交给下载器进行下载,并使用回调函数进行处理。另外 scrapy
也可以使用 response.urljoin()
方法,将下一页的链接地址拼接进请求的 start_url
(从start_urls
中迭代) 中。
解析“下一页”链接并添加到调度器队列
在爬虫的 parse()
方法中,添加解析和递归代码(因为是递归调用函数,所以添加的代码应该在正常解析数据之后):
# 解析下一页链接
nextLink = self.start_urls[0] + response.css('span.next>a').attrib['href'] # css 方式解析
# nextLink = self.start_urls[0] + response.xpath('//span[@class="next"]/a/@href').get() # xpath 方式解析
# 使用 urljoin() 方法
# nextLink = response.urljoin(response.css('span.next>a').attrib['href'])
if nextLink: # 如果存在下一页链接
# 将下一页链接添加到 scheduler 队列
# callback 是解析数据的函数
yield scrapy.Request(nextLink, callback=self.parse)
解析图片地址并下载图片
def parse_img(self, response, image_name): # 用于处理获取的 img 数据, image_name 在构造 request 对象时进行传递
# 可以直接将 response.body 交给管道进行保存
yield {'image_name': image_name + '.jpg', 'image_content': response.body}
def parse(self, response):
# 解析图片地址
img_link = response.xpath('//img/@src').get()
img_name = response.xpath('//img/@alt').get()
if img_link:
yield scrapy.Request(img_link, callback=self.parse_img, cb_kwargs={'image_name':img_name})
明确实体目标(设置容器)
scrapy 的items.py可以用作数据校验。查看 items.py ,可以看到类定义的说明里写了“在这里为实体定义字段”,并举了例子。所以可以根据例子进行字段定义:
name = scrapy.Field() # 电影名称
stars = scrapy.Field() # 评分
critical = scrapy.Field() # 评分人数
quote = scrapy.Field() # 经典影评
使用cmdline
直接执行scrapy
爬虫
默认 scrapy
是使用控制台命令行来执行爬虫的,可能在调试中会感觉不方便。这时可以使用 cmdline
来直接执行 scrapy
爬虫。
import scrapy
from scrapy import cmdline
class BaiduSpider(scrapy.Spider):
# 爬虫名称
name = "baidu"
# 允许爬取的域名
allowed_domains = ["baidu.com"]
# 爬取地址
start_urls = ["https://baidu.com"]
# 数据解析方法,之后需要我们自己编写逻辑
def parse(self, response):
pass
if __name__ == '__main__':
cmdline.execute('scrapy crawl baidu'.split())
此种执行方法和直接输入指令效果是一样的(有可能日志信息的颜色不太一样)。
信号
扩展
scrapy 可以自定义扩展(Extend),类似于插件。可以在项目根目录下创建相应的扩展脚本,例如 extend.py
,并写入合适的逻辑。通常扩展会使用信号和爬虫进行绑定。例如使用扩展每15秒钟获取一个代理ip
# extend.py
import time
import threading
import requests
from scrapy import signals
# 提取代理IP的api
api_url = 'https://dps.kdlapi.com/api/getdps/?secret_id=o1fjh1re9o28876h7c08&signature=xxxxx&num=10&pt=1&format=json&sep=1'
foo = True
# 代理类,通过此类对象获取代理
class Proxy:
def __init__(self, ):
self._proxy_list = requests.get(api_url).json().get('data').get('proxy_list')
@property
def proxy_list(self):
return self._proxy_list
@proxy_list.setter
def proxy_list(self, list):
self._proxy_list = list
pro = Proxy()
print(pro.proxy_list)
# 扩展类
class MyExtend:
def __init__(self, crawler):
self.crawler = crawler
# 将自定义方法绑定到scrapy信号上,使程序与spider引擎同步启动与关闭
# scrapy信号文档: https://www.osgeo.cn/scrapy/topics/signals.html
# scrapy自定义拓展文档: https://www.osgeo.cn/scrapy/topics/extensions.html
crawler.signals.connect(self.start, signals.engine_started)
crawler.signals.connect(self.close, signals.spider_closed)
@classmethod
def from_crawler(cls, crawler):
return cls(crawler)
def start(self):
t = threading.Thread(target=self.extract_proxy)
t.start()
def extract_proxy(self):
while foo:
pro.proxy_list = requests.get(api_url).json().get('data').get('proxy_list')
#设置每15秒提取一次ip
time.sleep(15)
def close(self):
global foo
foo = False
然后在 settings.py
中的 EXTENSIONS 中添加 项目名称.extend.MyExtend
,即可以使用该扩展了。如果需要添加代理,可以引入此文件的 pro
对象。
CrawlSpider 类
查看爬虫文件可以看到,新建的爬虫文件继承的是 scrapy.Spider 类,但是也可以继承 CrawlSpider 类。当使用 Crawl Spider 类时需引入 from scrayp.spiders import CrawlSpider, Rule
两个类和 from scrapy.linkextractors import LinkExtractor
这个类。
使用 CrawlSpider 比较主要的区别是爬取页面时按照规则(rules)获取链接继续爬取下一页,而 Spider 类需要手动写 yield scrapy.Request(url, callback=self.parse)
。除此之外,一些流程和方法也有改变。
创建 CrawlSpider 爬虫模板
scrapy genspider -t crawl 爬虫名称 域名
Rules 对象
Rules 指定了继续爬取的链接,例如下一页等。
rules =(
Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True, process_links=None),
)
需要注意的是:
- LinkExtractro 对象用于定义需要提取的链接
- follow=True 的意思是是否跟进,即是否继续提取链接
- process_links 是 LinkExtractor 获取到链接的处理函数,主要用于过滤链接,例如更改参数等
- callback 的处理函数名称需要使用引号括起来
LinkExtractor 对象
LinkExtractor 对象主要用于获取链接,其主要参数有:
- allow=() :匹配括号中的正则表达式,如果为空则匹配全部
- deny=() :和 allow 相反
- allow_domains=() :会被提取链接的域名
- deny_domains=() :不被提取链接的域名
- restrict_xpaths=() :被提取链接的 xpath 表达式(只选到节点,不选属性)
处理函数
处理函数的写法基本没变,需要注意的是,在 CrawlSpider 中不能使用 parse 这个方法名称。
爬取流程
和 Spider 类不同的是, CrawlSpider 的流程是从 start_urls 开始,初始化 url 然后交给调度器,调度器将 request 交给下载器,下载器下载到的 response 交给 rules 解析,获取到的链接交给调度器封装成 request 发送给下载器下载,然后新下载的 response 交给 rules 提取下一步链接的同时进行解析数据提取,然后交给管道。即第一次访问的页面是不会被提取数据的。
通常处理这个的方式是从这个页面的上一级页面开始爬取,然后在 rules 中添加一条规则,指向这个页面,注意此规则的 follow 也要设置为 True。
管道的处理和使用
在 scrapy 的流程中,爬虫将页面数据 response 解析后发送给管道。管道会以调用迭代器的方式调用解析方法。这里需要注意的是,之前版本发送给管道的数据必须是 item 对象,而比较新的版本中可以发送字典格式。
启用管道
管道文件 pipellines.py 里可以写很多处理类,如果使用的话必须在 setting.py 文件中开启。
在 setting.py 文件中,有 ITEM_PIPELINES 的字典,key 即为需要开启的管道名称,value 是处理优先级。这里需要注意的是 key 的规则为 “项目名称.管道文件名.管道处理类名” 。
管道类的组成
管道类主要由三个方法组成:
open_spider(self, spider)
: 爬虫开启时执行此方法一次close_spider(self, spider)
: 爬虫关闭时执行此方法一次process_item(self, item, spider)
: 处理 item 时执行
爬虫和管道的对应关系
当爬虫启动时,每个管道都会被调用且调用时机是相同的,只是根据优先级的不同执行顺序不同而已。因此每个管道可以处理多个爬虫的数据,也可以每个爬虫由多个管道来处理数据。如何确定管道处理哪个爬虫的数据,则需要在管道的类中判断爬虫名称(spider.name)。
由此可见,每个 item 过来,会经过所有的管道。但是如果 item 是无效数据,不想继续进入其他的管道怎么办?可以设置一个优先级比较高(值小)的管道进行判断,如果要扔掉,则使用 scrapy.exceptions.DropItem
类来处理
from scrapy.exceptions import DropItem
class DropPipeline(object):
def process_item(self, item, spider):
if item['type'] == '恐怖':
raise DropItem # 手动抛出异常
return item
这样就直接扔掉,其他管道也不会进行处理了。
管道处理不同的解析方法传来的数据
因为每个解析方法都会将数据传给管道,所以管道处理时,需要分辨这些数据是哪个解析方法传来的,然后做相应处理
class SavePipeline(object):
def process_item(self, item, spider):
if spider.name == '下载图片' and item['type'] == 'img':
with open(item.get('img_name'), 'wb') as f:
f.write(item.get('img_content')
elif spider.name == '下载图片' and item['type'] == 'info':
# 保存数据到数据库
pass
各种数据格式的处理
字典数据
管道获取的 item 是字典时,可以直接将字典内的数据抓出进行处理或保存,也可以将字典转换为字符串进行处理或保存。
可以使用 json 库的 jsum.dumps()
方法将字典转换为字符串,转换时需要注意编码的问题,添加参数 ensure_ascii=False 。
item = json.dumps(item,ensure_ascii=False)
item 对象
item 对象可以直接转换成字典对象,然后再进行处理
item = dict(item)
图片
scrapy 提供了 ImagesPipeline 专门处理图片的数据,且需按照以下流程进行:
- 在 item.py 中,添加 image_urls 和 images 两个容器 ()
- 开启图片管道:
'scrapy.contrib.pipeline.images.ImagesPipeline': 1
- 在 setting.py 中设置存储图片的文件夹 :
- 在爬虫中,抓取一个项目,将其中图片的 url 放入 image_urls 组内
- 当项目进入 ImagesPipeline ,image_urls 组内的 urls 将被 scrapy 的调度器和下载器安排下载。其优先级更高,会在其他页面被抓取前进行处理。项目会在这个特定的管道阶段保持锁定(locked)的状态,直到完成图片下载(或由于某些原因未完成下载)
- 当图片下载完成,另一个组(
image
)将会被更新到结构中。这个组将包含一个字典列表,其中包括下载图片的信息,比如下载路径、源抓取地址和图片校验码。images
列表的图片顺序将和images_urls
保持一致。如果某个图片下载失败,将会记录下错误信息,不会出现在images
组中。
实际使用中,只需要设置好 setting.py ,然后在爬虫中将图片的地址列表以字典形式返回,并不需要写 pipelines.py 文件,即可爬取图片。例:
import scrapy
class ZolSpider(scrapy.Spider):
name = 'zol'
allowed_domains = ['zol.com.cn']
start_urls = ['https://desk.zol.com.cn/bizhi/9945_119391_2.html']
def parse(self, response):
image_url = response.xpath('//img[@id="bigImg"]/@src').extract()
yield {
'image_urls': image_url
}
深入使用 ImagePipeline 处理图片
简单的爬取图片有一些限制,比如图片名称是 Hash 值 等。如果想要更多功能,则要重写 ImagesPipeline 。
在 pipelines.py 中重写 ImagePipeline:
from scrapy.pipelines.images import ImagesPipeline
import scrapy
class ImagePipeline(ImagesPipeline):
def get_media_requests(self, item, info):
for image_url in item['image_urls']:
yield scrapy.Request(image_url, meta={'image_name': item['image_name']}) # 将文件名传给 request
def file_path(self, request, response=None, info=None, *, item=None):
filename = request.meta['image_name'].strip().replace('\r\n\t\t', '').replace('/', '_') + '.jpg' # 设置文件名
return filename
需要注意的是,在 setting.py 中将默认处理管道删除,并添加此管道。
Scrapy 的中间件
scrapy 有两种中间件:爬虫中间件和下载中间件。两者的功能是一样的,都是预处理request
和response
对象。在Scrapy
默认的情况下,两种中间件都在middlewares.py
一个文件中。
爬虫中间件使用方法和下载中间件相同,且功能重复,区别只在处理时机不同(但是在引擎和调度器自动工作的情况下,对用户来说两者处理的时机基本是一致的),所以常使用下载中间件。
具体的处理时机和使用方法在后面。
Scrapy 进阶,重写一些方法
爬虫的 start_request()
方法会返回一个 Request
对象,将此对象传给下载中间件的 process_request()
方法进行处理,那么我们可以通过重写这些方法进行很多复杂的操作。
重写 start_request
发送带参数的请求
如果对请求有另外的需求,比如需要传递 form 参数等,需要重写 start_request,可以将 url、method、headers、cookies、meta、callback 等参数传给 Request。如果需要传 form ,则可以将 formdata 和上述参数传给 FormRequest 。
# 重写传递 form 数据,字典格式
class sxtSpider(scrapy.Spider):
name = 'sxt'
allowed_domains = ['sxt.cn']
# 重写 request 请求
def start_requests(self):
url = 'http://www.sxt.cn/index/login/login.html'
form_data = { # 设置 formdata
'user': '13513535555',
'password': '123456'
}
# 带 form 数据的发送给 FormRequest ,否则可以发给 Request
yield scrapy.FormRequest(url, formdata=form_data, callback=self.parse)
def parse(self,response):
# 已经登录过,可以直接访问其他的页面,并更改解析方法
yield scrapy.Request('http://www.sxt.cn/index/user.html', callback=self.parse_info)
def parse_info(self,response):
# 进一步解析数据
pass
# 重写传递 cookies ,需要注意的是 cookies 只支持字典和列表
class LoginSpider(scrapy.Spider):
name = 'login'
def start_requests(self):
url = 'http://www.sxt.cn/index/user.html'
cookie_str = 'Um_distinctid=163d8c88a6740c-01c2fe892f8d8c-737245c-100200-163d8c88a582a2; 53gid2=104556692380; 53revisit=13513535555; acw_tc=AqqAAAecrfectaA0aocesdvt'
# 将 cookie_str 转成字典
for cookie in cookie_str.split(';'):
key,value = cookie.split('=', 1)
cookies[key.strip()] = value.strip()
yield scrapy.Request(url, cookies=cookies, callback=self.parse)
def parse(self,response):
# 进一步解析数据
pass
# 传递自定义参数 meta
class LoginSpider(scrpay.Spider):
name = 'login'
start_urls = ['https://passport.ganji.com/login.php']
def parse(self, response):
hash_code = re.findall(r'"__hash__":"(.+)"', response.text)[0] # 获取页面中的 hash 值
img_url = response.xpath('img[@class="login-img-checkcode"]/@data-url').extract_first() # 页面中验证码的 url
yield scrapy.Request(img_url, callback=self.parse_info, meta={'hash_code': hash_code}) # 传递自定义参数 meta
def parse_info(self, response):
hash_code = response.request.meta['hash_code'] # 传递的参数是传到 request 中,所以从 response 的 request 中获取
with open('yzm.jpg', 'wb') as f: # 保存验证码图片
f.write(response.body)
code = input('请输入验证码:') # 人工查看验证码
form_data={
"username": "17777777777",
"password": "123456abcd",
"setcookie": "0",
"checkCode": code,
"next": "/",
"source": "passport",
"__hash__": hash_code
}
login_url = 'https://passport.ganji.com/login.php'
yield scrapy.FormRequest(login_url, callback=self.after_login, formdata=form_data)
def after_login(self.response):
# 继续解析
pass
实时爬取更新信息
既然 start_requests 是 scrapy 的请求入口,那么如果循环固定对某个页面进行访问爬取,则可以获得实时更新的数据。需要注意的是, scrapy 有默认去重的功能,此时需要将这个功能关闭。
def start_requests(self):
while True:
yield scrapy.Request(url, callback=self.parse, dont_filter=True)
重写 process_request
修改 request 的头部信息
可以在下载中间件中设置动态 UA 和代理
使用下载中间件时,同样要在 setting.py 里开启 DOWNLOADER_MIDDLEWARES。如果中间件设置没起作用,可能是优先级太低(数字太大),将优先级提高(数字调小)再试即可。
UA 在中间件的函数 process_request 里设置。
def process_request(self, request, spider):
from fake_useragent import UserAgent
request.headers.setdefault(b'User-Agent', UserAgent().random) # 使用随机 UA
# 也可以这样写:
# request.headers['User-Agent'] = UserAgent().random
return None
也可以在这里设置代理
# request.meta['proxy'] = 'http://ip:port' # 匿名代理
# request.meta['proxy'] = 'http://username:password@ip:port' # 独享代理
截断 request 请求,并返回数据
正常的流程是 process_request
处理请求信息,然后交给下载器进行下载。如果我们有特殊要求,跳过下载器直接返回响应数据给爬虫,可以在此函数中进行处理。例如使用 selenium 获取已经编译后的页面代码,交给爬虫进行处理。
需要注意的是 process_request
原功能只处理 request 对象,并不返回数据。所以需要使用此方法返回数据,要引入 from scrapy.http import HtmlResponse
类。
from scrapy.http import HtmlResponse
class SeleniumMiddleware(object): # 下载中间件
def process_request(self, request, spider):
# 从传入的 request 对象中获得 url
url = request.url
# selenium 浏览器打开页面,注意这个浏览器对象是 spider 的一个成员
spider.chrome.get(url)
# 获取页面编译后的源代码
html = spider.chrome.page_source
# 跳过下载器,直接返回 response 对象至爬虫
return HtmlResponse(url=url, body=html, request=request, encoding='utf-8')
因为涉及到打开 selenium 浏览器,以及爬虫结束后需要自动关闭,所以要在爬虫中重写 spider 的创建过程,添加打开浏览器和关闭浏览器的事件。
import scrapy
from scrapy import signals
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
class GuaziSpider(scrapy.Spider):
name = 'guazi'
allowed_domains = ['guazi.com']
start_urls = ['https://www.guazi.com/buy']
def parse(self, response): # 解析部分,先不写
# print(response.text)
pass
# 根据爬虫设置创建 spider 对象,并且此对象创建时创建并打开 selenium 浏览器
@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
spider = super(GuaziSpider, cls).from_crawler(crawler, *args, **kwargs) # 创建 spider 对象
# 设置一个 spider_closed 的信号,并指定此信号的处理方法
crawler.signals.connect(spider.spider_closed, signal=signals.spider_closed)
path = Service('D:\\chromedriver.exe') # 获取浏览器驱动的路径
spider.chrome = webdriver.Chrome(service=path) # 创建 selenium 浏览器实例
return spider
# spider 结束时会发送 spider_closed 信号,监听到此信号的处理方法
def spider_closed(self, spider):
spider.chrome.quit() # 关闭浏览器
重写 process_response
下载中间件的 process_response
方法接收3个参数,request
、response
和spider
,该方法在下载器下载完成后触发。
重新请求
可以通过判断 response.status
状态的方式,将 request
对象发给调度器的方式进行重新请求。需要注意的是,scrapy 自带去重过滤,会将已经请求过的 request
对象过滤掉,所以需要设置 requerst.dont_filter
。
def process_response(self, request, response, spider):
if response.status != 200:
request.dont_filter = True # 关闭过滤
return request
scrapy 常用的内置方法
scrapy 可以按照预设的流程执行 Spider、SpiderMiddleware、DownloaderMiddleware、ItemPipeline 的相应方法,所以我们可以根据需要重写或继承这些方法。这里列举一些常用的方法。
Spider 的内置方法
Spider
类主要有**parse(self, response, **kwargs)
**、**start_request(self)**
内置方法
parse(self, response, **kwargs)
- 当引擎获取
Response
对象时,交给Spider
解析时被调用 - 返回
Request
对象:把request
对象通过引擎交给调度器 - 返回
BaseItem
对象,或字典对象:将数据交给管道进行处理 - 此方法是一个生成器,使用
yield
发送结果
**start_request(self)**
- 当爬虫启动时,发送请求给引擎。需注意的是,如果使用此方法,则会忽略
start_urls
列表中的地址 - 可以不设置 start_urls,而是在此方法中直接封装
Request
对象,发给引擎 - 此方法也是一个生成器,所以使用
yield
返回结果
start_request
方法通常使用在:
- 如果
start_urls
列表中的地址需要登录后才能访问,则需要重写start_requests
方法并手动添加cookie
- 需要自己构建翻页地址的情况下可以重写
start_requests
方法 - 如果在
start_urls
中的URL
需要用POST
提交的话,则需要在start_requests
方法中修改 - 默认情况下
start_urls
中的URL
在被生成Request
对象时,都是设置为不过滤,即dont_filter=True
,所以如果想使用暂停、恢复爬取功能的话,就需要重写此方法了。
可以在start_request
方法中发送 post 请求,或表单:
yield scrapy.FormRequest(url=url, formdata=data, callback=self.parse, dont_filter=False)
# yield scrapy.Request(url=url, body=json.dumps(data), method='POST', callback=self.parse)
# yield scrapy.JsonRequest(url=url, data=json_data)
SpiderMiddleware 的内置方法
SpiderMiddleware 的用处有些和 DownloadMiddleware 重合了,大部分时候可能会在 DownloadMiddleware 中进行相应的处理。
SpiderMiddleware 主要有 process_start_requests(self, start_requests, spider)
、 process_spider_input(self, response, spider)
和 process_spider_output(self, response, result, spider)
process_start_requests(self, start_requests, spider)
- 当
request
对象通过爬虫中间件时,该方法被调用 - 该方法必须且仅返回
request
对象,交给引擎 - 因为爬虫是以迭代器方式发送数据,所以此方法也使用
yield
process_spider_input(self, response, spider)
- 当
response
通过中间件交给爬虫时,该方法被调用 - 返回值为
None
或引发错误
process_spider_output(self, response, result, spider)
- 当爬虫处理完
response
对象并返回结果时被调用 - 必须返回
request
对象或item
对象(也可以是字典对象) - 因为爬虫是以迭代器方式发送数据,所以此方法也使用
yield
DownloadMiddleware 的内置方法
DownloadMiddleware
主要有两个内置方法:process_request(self, request, spider)
和 process_response(self, request, response, spider)
。
-
process_request(self, request, spider)
- 当每个
request
通过下载中间件时,该方法被调用 - 返回
None
值:没有return
也是返回None
,该request
对象传递给下载器,或通过引擎传递给其他权重低的process_request
方法 - 返回
Response
对象:不再请求,把response
返回给引擎 - 返回
Request
对象:把request
对象通过引擎交给调度器,此时将不通过其他权重低的process_request
方法
- 当每个
-
process_response(self, request, response, spider)
- 当下载器完成
http
请求,传递响应给引擎的时候调用 - 返回
Resposne
对象:通过引擎交给爬虫处理或交给权重更低的其他下载中间件的process_response
方法 - 返回
Request
对象:通过引擎交给调度器继续请求,此时将不通过其他权重低的process_request
方法
- 当下载器完成
注意:需要在settings.py
文件中开启中间件,权重越小越优先执行。
ItemPipeline 的内置方法
管道类主要有三个内置方法:process_item(self, item, spider)
、open_spider(self, spider)
和 close_spider(self, spider)
process_item(self, item, spider)
- 管道类中必须要有的方法
- 实现对
item
数据的处理 - 一般情况下都会
return item
,如果没有return
,那么就相当于将None
传递给权重低的process_item
,也可以raise DropItem
。
open_spider(self, spider)
- 在爬虫开启的时候仅执行一次
- 可以在该方法中链接数据库、打开文件等等
close_spider(self, spider)
- 在爬虫关闭的时候仅执行一次
- 可以在该方法中关闭数据库连接、关闭文件对象等
增量爬虫
有些情况下,我们希望能暂停爬虫,之后在恢复运行,尤其是抓取大型站点的时候可以完成暂停与恢复。此时就用到了Scrapy
的爬虫暂停与爬虫恢复。
启用可暂停爬虫的命令
想要实现暂停,Scrapy
代码不用修改,只需要在启动时修改运行命令即可:
# scrapy crawl 爬虫名称 -s JOBDIR=缓存scrapy信息的路径
scrapy crawl MySpider -s JOBDIR=crawls/my_spider-1
暂停爬虫
暂停爬虫就是终止 python 程序,一般使用快捷键 ctrl + c。需要注意的是,如果程序在执行一些耗时操作,例如下载视频,并不会立刻暂停。此时稍等一下,而不要再按 ctrl + c 强制结束程序。
恢复爬虫执行
恢复爬虫时运行与启用可暂停爬虫相同的命令。
分布式爬虫
分布式爬虫是网络爬虫的一种,它将任务分散在多台计算机上,这些计算机协同工作以更高效地收集网络数据。与传统的单机爬虫相比,分布式爬虫由多个节点组成,每个节点都可以执行爬虫任务,而且这些节点之间相互协作,共享资源和信息。
- 高效性能:分布式爬虫可以充分利用多台计算机的计算资源和网络带宽,同时执行多个爬虫任务,从而大大提高数据抓取的效率。它能够快速地处理大规模的数据,并在较短的时间内完成爬取任务。
- 高拓展性:分布式爬虫系统可以根据需求进行横向扩展,通过增加更多的爬虫节点来处理更大规模的数据抓取任务。这使得系统能够适应不断增长的数据量和更高的并发需求。
- 高可靠性:分布式爬虫系统具有容错和冗余的特性。当某个节点出现故障或者网络问题时,其他节点可以继续执行任务,从而保证数据抓取的连续性和可靠性。
- 数据一致性:分布式爬虫可以通过合理的任务调度和数据同步机制,确保多个节点爬取的数据保持一致性。这对于需要对多个数据源进行聚合和分析应用非常重要。
- 大规模数据处理:分布式爬虫系统可以方便地应对大规模数据的处理和存储需求。通过将数据分布在多个节点上,系统可以更高效地处理和存储大量数据。
scrapy 实现分布式爬虫需要用到 scrapy-redis 包
# 使用 scrapy-redis之前最好将scrapy版本保持在2.6.3,2.11.0版本有兼容问题
pip install scrapy=2.6.3
pip install scrapy-redis
添加 scrapy-redis 配置
想要让scrapy
实现增量爬取(即暂停、恢复)功能,就需要在scrapy
项目中的settings.py
文件中进行配置
""" scrapy-redis配置 """
# 调度器类
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 指纹去重类
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 可以替换成布隆过滤器(支持亿万级别数据去重)
# 下载 - pip install scrapy-redis-bloomfilter
# from scrapy_redis_bloomfilter.dupefilter import RFPDupeFilter
# DUPEFILTER_CLASS = 'scrapy_redis_bloomfilter.dupefilter.RFPDupeFilter'
# 是否在关闭时候保留原来的调度器和去重记录,True=保留,False=清空
SCHEDULER_PERSIST = True
# Redis服务器地址
REDIS_URL = "redis://127.0.0.1:6379/0" # Redis默认有16库,/1的意思是使用序号为2的库,默认是0号库(这个可以任意)
# 使用密码: "redis:@user:password//127.0.0.1:6379/0"
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue' # 使用有序集合来存储
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue' # 先进先出
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue' # 先进后出、后进先出
# 配置redis管道
# from scrapy_redis.pipelines import RedisPipeline
ITEM_PIPELINES = {
"douban.pipelines.DoubanPipeline": 300,
'scrapy_redis.pipelines.RedisPipeline': 301
}
# 重爬: 一般不配置,在分布式中使用重爬机制会导致数据混乱,默认是False
# SCHEDULER_FLUSH_ON_START = True
实现分布式爬虫
实现分布式爬虫需要以下步骤:
- 普通爬虫继承的类是
scrapy.Spider
,分布式爬虫需要将继承的类改为scrapy_redis.spiders.RedisSpider
- 添加类属性
redis_key
,表示保存需要访问的 urls 的键名,即从哪里获取 url 请求。 - 删除类属性
start_urls
,因为从 redis 中获取请求,所以不自行设置 - 修改
settings.py
中的ITEM_PIPELINES
,改为(按需要添加)'scrapy_redis.pipelines.RedisPipeline': 300
- 因为爬虫本身并没有请求地址,所以创建方法存储请求地址到 redis 中
class Top250Spider(RedisSpider):
...
redis_key = 'top250:start_urls'
# settings.py
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 300,
}
# insert_start_urls.py
# 添加请求地址到 redis 中
import redis
with redis.Redis(host='192.168.1.55', port=6379, db=0) as redis_client:
redis_client.lpush('top250:start_urls', 'https://movie.douban.com/top250?start=0&filter=')
需要注意的是,如果有下载图片或文件之类的,是无法直接存储到 redis 中的,需要编码为 base64 才能进行存储。不过一般不会在 redis 中存储文件,而是使用一个优先级高点的管道来保存文件,然后 raise DropItem
,不交给 redis 管道。
到这里分布式配置就结束了,所有的分布式爬虫都会从redis服务器中获取请求,并且保存数据到 redis 服务器中。
部署 scrapyd
如果有大量的 scrapy 项目需要启动,可以使用 scrapyd
进行远程部署调度。scrapyd
是 scrapy
的一个组件,需要另外安装(安装在服务端,非scrapy客户端)。
pip install scrapyd
安装完成后可以使用命令来查看是否可以正常使用
scrapyd
配置 scrapyd
安装完成后,需要配置一下 scrapyd
,让其支持远程访问。随便在某一目录中创建配置文件 scrapyd.conf
,并输入以下内容
[scrapyd]
# 监听的IP地址,默认为127.0.0.1(只有改成0.0.0.0才能在别的电脑上能够访问scrapyd运行之后的服务器)
bind_address = 0.0.0.0
# 监听的端口,默认为6800
http_port = 6800
# 是否打开debug模式,默认为off
debug = off
然后可以在配置文件所在目录运行 scrapyd
,查看是否安装成功。
本地 scrapy 项目上传服务端
配置好并运行了 scrapyd
后,可以将本地的 scrapy
项目上传到服务端。首先配置 scrapy
项目,在项目下的 scrapy.cfg
文件里,设置好 url
项,改位服务端的地址。可以在 deploy
后添加当前节点(客户端)的名称。
[settings]
default = douban.settings
[deploy:node-1]
url = http://192.168.55.5:6800/
project = douban
配置好后可以将项目上传,需要使用到 scrapyd-client
。安装完成包后,可以使用 scrapyd-deploy -l
来验证是否可以使用。需要注意的是,此命令必须在 scrapy.cfg
文件所在目录下使用。使用后可以看见节点名称和服务端地址。
使用以下命令发布 scrapy
项目到 scrapyd
服务器:
scrapy-deploy <target> -p <project> --version <version>
参数 <target>
是配置文件中 deploy
后面节点的名称;参数 <project>
可以随便写,一般是项目名称;参数<version>
是自定义的版本信息,默认使用时间戳。所以实际项目中使用以下指令上传项目:
scrapy-deploy node-1 -p douban
上传成功将返回一个字典,里面有 status:ok
。
项目将打包并上传到服务端配置文件的目录下。
web端控制 scrapyd
使用浏览器打开服务端地址,能看到 scrapyd
的信息。如果上传了项目,则可以在 Available projects
中看到。
Jobs
里能监测项目的状态。
执行上传的项目
在客户端里,可以使用 curl
命令来执行已经上传的项目:
curl http://192.168.55.5:6800/schedule.json -d project=douban -d spider=top250
需要注意的有:
- 服务端的地址和端口
- project 是需要执行的项目名称,在上传的项目时候设置
- spider 是执行的爬虫名称,
scrapy
爬虫类的name
值
执行完成后,可以在返回值中看到 status: ok
,表示运行成功。此时可以在 web 端的 jobs
项里看到运行信息。
停止执行
使用 curl
命令来停止执行项目
curl http://192.168.55.5:6800/cancel.json -d project=douban -d job=234798u2318979184u9812344912379851638941
其中 job 的值是任务的 id 值,可以在 web 端的 jobs
项下查看。
使用 scrapydweb
部署
scrapydweb
是 scrapyd
的可视化组件。
pip install scrapydweb
secrpydweb
是基于 scrapyd
的,所以运行之前需要先运行 scrapyd
。
scrapyd
scrapydweb
注:第一次执行可能会报错,报错后再次执行就可以了。执行成功之后会提示一个地址,可以使用浏览器通过该地址来访问 web 管理页面。
可以在 web 页面中,左侧的 Deploy Project 项目中上传本地项目代码的 zip 压缩文件。
可以在 Run Spider 项目中来选择工程和爬虫来运行。
使用 Gerapy
部署
Gerapy
是国产的爬虫管理软件。可以通过 gerapy
配置 scrapyd
,然后直接通过图形化界面开启爬虫。需注意的是 Gerapy
是安装在客户端而不是服务端的。
pip install gerapy=0.9.12
安装完成后,需要初始化。在目标目录中输入
gerapy init
然后进行配置
cd gerapy
gerapy migrate # 同步 sqlite 数据库
gerapy createsuperuser # 创建超级管理员
gerapy runserver # 启动服务,访问地址 127.0.0.1:8000
在浏览器中使用访问地址和超级管理员的账户进行登录,然后在主机管理中创建服务器主机(需要启动 scrapyd
)。
在项目管理中可以上传爬虫项目的 zip 文件,并进行部署。在主机管理中的调度项里可以执行已经上传的爬虫项目。