【7.0】爬虫之scrapy框架进阶
【一】引言
【1】启动Spider项目位置
- 注意:
- 如果终端还在第一个项目的文件夹中,则需要在终端中执行cd ../返回到上级目录,在去新建另一个项目。
【2】新建数据解析项目
(1)创建工程
scrapy startproject 项目名称
(2)切换到项目目录
cd 项目名称
(3)创建爬虫文件
scrapy genspider 爬虫文件名 www.xxx.com
(4)配置文件的修改
- settings.py
# 不遵从robots协议
ROBOTSTXT_OBEY = False
# 指定输出日志的类型:
LOG_LEVEL = 'ERROR'
# 指定UA:
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36'
(5)编写爬虫文件:
- spiders/爬虫文件名.py
【3】启动项目
- 命令启动
scrapy crawl wangyi
- 项目根目录下新建启动文件
- bin.py:
from scrapy.cmdline import execute
execute(['scrapy', 'crawl', '爬虫文件名', "--nolog"])
【4】项目执行流程简解
-
启动爬虫:
- 开始执行爬虫程序。
-
创建Request对象:
- 将start_urls地址包装成request对象,其中包含待爬取的网页链接、回调函数等信息。
-
交给引擎:
- 将创建好的request对象交给引擎进行处理。
-
调度器排队:
- 引擎将请求交给调度器,调度器按照一定的策略(如先进先出)将请求放入队列中进行排队。
-
引擎处理:
- 引擎从调度器中取出请求,并选择合适的下载中间件进行处理。
-
下载器处理:
- 下载器根据请求中的链接发起网络请求,下载相应的页面内容,并将下载结果返回给引擎。
-
下载完成:
- 下载器将网页内容下载完成后,将下载结果返回给引擎。
-
引擎传递给爬虫:
- 引擎将下载结果传递给相应的爬虫解析函数(例如parse函数)进行处理。
-
爬虫解析:
- 爬虫解析函数对下载下来的网页内容进行解析,提取出需要的数据,并生成新的Request对象或是Item对象。
-
回到步骤2:
- 解析函数可以继续生成新的Request对象,或是处理Item对象
- 然后回到步骤2,继续执行后续的请求和解析过程。
【二】数据解析
【1】爬虫文件结构解析
import scrapy
# 爬虫类,继承了scrapy.Spider
class FirstSpider(scrapy.Spider):
# 爬虫文件的名称,是当前爬虫文件的唯一标识
name = "first"
# 允许访问的域名
allowed_domains = ["www.cnblogs.com/"]
# 起始的url列表:可以将即将被请求的url,存放在当前列表中。
# 默认情况,列表中存储的url都会被scrapy框架进行get请求的发送
start_urls = ["https://www.cnblogs.com/"]
# 实现数据解析
# 参数response表示请求对应的响应对象
# parse方法调用的次数取决于请求的次数
def parse(self, response):
# 可以在响应对象中直接使用xpath进行数据解析
pass
- 1 response对象有css方法和xpath方法
- css中写css选择器
- xpath中写xpath选择
- 2 重点1:
- xpath取文本内容
'.//a[contains(@class,"link-title")]/text()'
- xpath取属性
'.//a[contains(@class,"link-title")]/@href'
- css取文本
'a.link-title::text'
- css取属性
'img.image-scale::attr(src)'
- 3 重点2:
.extract_first()
- 取一个
.extract()
- 取所有
【2】使用bs4解析响应
import scrapy
# 爬虫类,继承了scrapy.Spider
class FirstSpider(scrapy.Spider):
# 爬虫文件的名称,是当前爬虫文件的唯一标识
name = "first"
# 允许访问的域名
allowed_domains = ["www.cnblogs.com/"]
# 起始的url列表:可以将即将被请求的url,存放在当前列表中。
# 默认情况,列表中存储的url都会被scrapy框架进行get请求的发送
start_urls = ["https://www.cnblogs.com/"]
# 实现数据解析
# 参数response表示请求对应的响应对象
# parse方法调用的次数取决于请求的次数
def parse(self, response):
# http响应包装成了response
# 可以在响应对象中直接使用xpath进行数据解析
# print(response.text)
######css选择器#####
# 解析出所有文章
article_list = response.css('article.post-item') # 列表中放对象
print(len(article_list))
# 继续解析文章详情:文章标题,文章摘要,作者图片,作者名字,文章详情地址
for article in article_list:
title = article.css('section>div>a.post-item-title::text').extract_first()
desc = article.css('p.post-item-summary::text').extract()
real_desc = desc[0].replace('\n', '').replace(' ', '')
if real_desc:
desc = real_desc
else:
real_desc = desc[1].replace('\n', '').replace(' ', '')
desc = real_desc
author_img = article.css('p.post-item-summary img::attr(src)').extract_first()
author_name = article.css('footer.post-item-foot span::text').extract_first()
url = article.css('div.post-item-text a::attr(href)').extract_first()
print('''
文章名字:%s
文章摘要:%s
作者图片:%s
作者名字:%s
文章地址:%s
''' % (title, desc, author_img, author_name, url))
【3】使用xpath解析响应
import scrapy
# 爬虫类,继承了scrapy.Spider
class FirstSpider(scrapy.Spider):
# 爬虫文件的名称,是当前爬虫文件的唯一标识
name = "first"
# 允许访问的域名
allowed_domains = ["www.cnblogs.com/"]
# 起始的url列表:可以将即将被请求的url,存放在当前列表中。
# 默认情况,列表中存储的url都会被scrapy框架进行get请求的发送
start_urls = ["https://www.cnblogs.com/"]
# 实现数据解析
# 参数response表示请求对应的响应对象
# parse方法调用的次数取决于请求的次数
def parse(self, response):
# http响应包装成了response
# 可以在响应对象中直接使用xpath进行数据解析
# print(response.text)
######xpath选择器#####
# 解析出所有文章
article_list = response.xpath('//article[contains(@class,"post-item")]') # 列表中放对象
print(len(article_list))
# 继续解析文章详情:文章标题,文章摘要,作者图片,作者名字,文章详情地址
for article in article_list:
# title = article.xpath('./section/div/a/text()').extract_first()
title = article.xpath('.//a/text()').extract_first()
desc = article.xpath('.//p[contains(@class,"post-item-summary")]/text()').extract()
real_desc = desc[0].replace('\n', '').replace(' ', '')
if real_desc:
desc = real_desc
else:
real_desc = desc[1].replace('\n', '').replace(' ', '')
desc = real_desc
# p.post-item-summary img::attr(src)
author_img = article.xpath('.//p//img/@src').extract_first()
# div.post-item-text a::attr(href)
author_name = article.xpath('.//footer//span/text()').extract_first()
url = article.xpath('.//div[contains(@class,"post-item-text")]//a/@href').extract_first()
print('''
文章名字:%s
文章摘要:%s
作者图片:%s
作者名字:%s
文章地址:%s
''' % (title, desc, author_img, author_name, url))
【三】配置文件说明
【1】基础配置
#1 项目名字,整个爬虫名字
BOT_NAME = "firstscrapy"
#2 爬虫存放位置的设置
SPIDER_MODULES = ["firstscrapy.spiders"]
NEWSPIDER_MODULE = "firstscrapy.spiders"
#3 是否遵循爬虫协议,一般都设为False
ROBOTSTXT_OBEY = False
# 4 User-Agent设置
USER_AGENT = "firstscrapy (+http://www.yourdomain.com)"
#5 日志级别设置
LOG_LEVEL='ERROR'
#6 DEFAULT_REQUEST_HEADERS 默认请求头
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
}
#7 SPIDER_MIDDLEWARES 爬虫中间件
SPIDER_MIDDLEWARES = {
'cnblogs.middlewares.CnblogsSpiderMiddleware': 543,
}
#8 DOWNLOADER_MIDDLEWARES 下载中间件
DOWNLOADER_MIDDLEWARES = {
'cnblogs.middlewares.CnblogsDownloaderMiddleware': 543,
}
#9 ITEM_PIPELINES 持久化配置
ITEM_PIPELINES = {
'cnblogs.pipelines.CnblogsPipeline': 300,
}
【2】高级配置
#1 增加并发数,默认为16,可以根据需求进行调整
# 默认scrapy开启的并发线程为32个,可以适当进行增加。
# 值为100,并发设置成了为100。
# 在settings配置文件中修改
CONCURRENT_REQUESTS = 100
#2 降低日志级别,可设置为INFO或ERROR,减少日志输出,提高性能
#在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。
在配置文件中编写:
LOG_LEVEL = 'INFO'
# 3 禁止使用Cookie,默认为True,如果不需要使用Cookie可以设置为False
# 如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:
COOKIES_ENABLED = False
# 4 禁止重试,默认为True,如果不需要进行重试请求可以设置为False
# 对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:
RETRY_ENABLED = False
# 5 设置下载超时时间,默认180秒,可以根据需求进行调整
# 如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:
DOWNLOAD_TIMEOUT = 10 超时时间为10s
【四】持久化存储
【1】方案一(存到本地,一般不用)
- 通过使用Scrapy框架提供的命令将爬取到的数据持久化存储为JSON或CSV格式。
- parse必须有return值,必须是列表套字典形式--->使用命令,可以保存到json格式中,csv中
import scrapy
# 爬虫类,继承了scrapy.Spider
class FirstSpider(scrapy.Spider):
# 爬虫文件的名称,是当前爬虫文件的唯一标识
name = "first"
# 允许访问的域名
allowed_domains = ["www.cnblogs.com/"]
# 起始的url列表:可以将即将被请求的url,存放在当前列表中。
# 默认情况,列表中存储的url都会被scrapy框架进行get请求的发送
start_urls = ["https://www.cnblogs.com/"]
# 实现数据解析
# 参数response表示请求对应的响应对象
# parse方法调用的次数取决于请求的次数
def parse(self, response):
# http响应包装成了response
# 可以在响应对象中直接使用xpath进行数据解析
# print(response.text)
######xpath选择器#####
# 解析出所有文章
data_list = []
article_list = response.xpath('//article[contains(@class,"post-item")]') # 列表中放对象
print(len(article_list))
# 继续解析文章详情:文章标题,文章摘要,作者图片,作者名字,文章详情地址
for article in article_list:
# title = article.xpath('./section/div/a/text()').extract_first()
title = article.xpath('.//a/text()').extract_first()
desc = article.xpath('.//p[contains(@class,"post-item-summary")]/text()').extract()
real_desc = desc[0].replace('\n', '').replace(' ', '')
if real_desc:
desc = real_desc
else:
real_desc = desc[1].replace('\n', '').replace(' ', '')
desc = real_desc
# p.post-item-summary img::attr(src)
author_img = article.xpath('.//p//img/@src').extract_first()
# div.post-item-text a::attr(href)
author_name = article.xpath('.//footer//span/text()').extract_first()
url = article.xpath('.//div[contains(@class,"post-item-text")]//a/@href').extract_first()
print('''
文章名字:%s
文章摘要:%s
作者图片:%s
作者名字:%s
文章地址:%s
''' % (title, desc, author_img, author_name, url))
data_list.append(
{'author_name': author_name,
'author_img': author_img,
'title': title,
"desc": desc,
'url': url})
return data_list
- 执行下述命令
scrapy crawl first -o first.json #以json形式保存
scrapy crawl first -o first.csv #以csv形式保存
first.json
[
{"author_name": "薛晓明c++算法", "author_img": null, "title": "c++算法之动态规划:01背包", "desc": "什么是动态规划?动态规划算法(dynamicprograming),是一种由递推为基础的比贪心更稳定的一种优化策略,为运筹学的一部分。就是通过以递推为基础的手段非暴力求出最值。它的总体思想其实就是一个比较过程:假如你有一个数据,它的价值是x,代价为y,如果用动态规划就是和你不加这个元素和你加上...", "url": "https://www.cnblogs.com/xuexue1234/p/dongtaiguihua.html"},
{"author_name": "Linux-1874", "author_img": "https://pic.cnblogs.com/face/1503305/20181003204442.png", "title": "k8s实战案例之运行WordPress", "desc": "LNMP案例之基于Nginx+PHP实现WordPress博客站点,要求Nginx+PHP运⾏在同⼀个Pod的不同容器;nginx主要作用是接入站点请求,如果请求静态资源nginx就直接响应;如果请求的是一个动态php资源,就将对应请求转发给另一个php容器进行处理;在一个pod中运行多容器,网络名......", "url": "https://www.cnblogs.com/qiuhom-1874/p/17601063.html"},
{"author_name": "春告鳥", "author_img": "https://pic.cnblogs.com/face/1828215/20220327232939.png", "title": "U盘目录穿越获取车机SHELL - 分析与复现", "desc": "github上破解日系车机的文章-https://github.com/ea/bosch_headunit_root![](https://springbird3.oss-cn-chengdu.aliyuncs.com/lianxiang/20230820013430.png)其中有利用U...", "url": "https://www.cnblogs.com/Cl0ud/p/17643514.html"},
{"author_name": "我是水货", "author_img": "https://pic.cnblogs.com/face/878843/20160817141154.png", "title": "2.如何选择go语言基础类型——Leetcode习题9", "desc": "[TOC]#本篇前瞻欢迎来go语言的基础篇,这里会帮你梳理一下go语言的基本类型,注意本篇有参考[go圣经](https://gopl-zh.github.io/),如果你有完整学习的需求可以看一下。另外,go语言的基本类型比较简单,介绍过程就比较粗暴,不过我们需要先从一个例题开始。#Le...", "url": "https://www.cnblogs.com/onlyac/p/17642741.html"},
{"author_name": "熊仔其人", "author_img": "https://pic.cnblogs.com/face/840264/20151118165749.png", "title": "VS2015项目.net-framework-4.5.2升级或新建项目无法选择framework 4.6.2(解决办法)", "desc": "####VS2015里面没有.NETFramework4.6.2VS2015默认安装的目标框架最高是.NETFramework4.6.1,但是我的项目里面某些NuGet软件包更新需要依赖.NETFramework4.6.2,项目就需要升级到目标框架.NETFramework4.6...", "url": "https://www.cnblogs.com/xiongzaiqiren/p/NetFramework462.html"},
{"author_name": "博客园团队", "author_img": "https://pic.cnblogs.com/face/35695/20230612120532.png", "title": "【故障公告】会员救园,阿里云故障", "desc": "今天19:09通过微信服务号发布了会员救园的文章,满怀期待地等着会员降临时,却收到好几位用户的私信,反馈说https://cnblogs.vip/网站打不开。当时很是纳闷,会员站点自上线以来从来没有出过问题,怎么这么巧,一发公众号就出现问题,真会找时间添乱。排查后万万没有想到竟然是会员站点所......", "url": "https://www.cnblogs.com/cmt/p/17643188.html"},
{"author_name": "Mysticbinary", "author_img": "https://pic.cnblogs.com/face/1552062/20181202144547.png", "title": "Callback Function Essence", "desc": "#IncludeExample![](https://img2023.cnblogs.com/blog/1552062/202308/1552062-20230819084715897-1568289359.png)Input:```Iama.routeexecutefinis...", "url": "https://www.cnblogs.com/mysticbinary/p/17631144.html"},
{"author_name": "护发师兄", "author_img": "https://pic.cnblogs.com/face/2722327/20220216172220.png", "title": "Jni GetMethodID中函数标识sig的详细解释", "desc": "在JNI(JavaNativeInterface)中,`GetMethodID`函数用于获取Java类的方法的标识符。这个函数的详细解释如下:```cCopycodejmethodIDGetMethodID(JNIEnv*env,jclassclazz,constch...", "url": "https://www.cnblogs.com/jonil/p/17642525.html"},
{"author_name": "赵榕", "author_img": "https://pic.cnblogs.com/face/465891/20150420130358.png", "title": "基于Supabase开发公众号接口", "desc": "Woa(WechatOfficialAccount)是一个基于.net7开发的微信公众平台接口项目,利用Supabase作为数据存储和消息通信服务,同时提供了ChatGPT和Claude2等目前热门的生成式AI会话功能接入。...", "url": "https://www.cnblogs.com/zhaorong/p/woa-use-supabase.html"},
{"author_name": "豌豆花下猫", "author_img": null, "title": "Python 潮流周刊#16:优雅重要么?如何写出 Pythonic 的代码?", "desc": "你好,我是猫哥。这里每周分享优质的Python、AI及通用技术内容,大部分为英文。标题取自其中两则分享,不代表全部内容都是该主题,特此声明。本周刊由**Python猫**出品,精心筛选国内外的250+信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容...", "url": "https://www.cnblogs.com/pythonista/p/17643091.html"},
{"author_name": "Jason Long", "author_img": "https://pic.cnblogs.com/face/423657/20160315104019.png", "title": "Deno 中使用 @typescript/vfs 生成 DTS 文件", "desc": "##背景前段时间开源的[STC](https://github.com/long-woo/stc)工具,这是一个将OpenApi规范的Swagger/Apifox文档转换成代码的工具。可以在上一篇([《OpenApi(Swagger)快速转换成TypeScript代码-STC...", "url": "https://www.cnblogs.com/JasonLong/p/17638932.html"},
{"author_name": "狒猩橙", "author_img": "https://pic.cnblogs.com/face/2684101/20211213220347.png", "title": "CVE-2022-42475-FortiGate-SSLVPN HeapOverflow 学习记录", "desc": "###前言之前就想复现这个洞,不过因为环境的问题迟迟没有开工。巧在前一阵子有个师傅来找我讨论劫持**ssl结构体**中函数指针时如何确定堆溢出的偏移,同时还他把搭建好了的环境发给了我,因此才有了此文。###如何劫持SSL结构体指针实现控制程序流就我个人理解而言,我觉得劫持的这个函数指针类...", "url": "https://www.cnblogs.com/pwnfeifei/p/17641251.html"},
{"author_name": "chinjinyu", "author_img": "https://pic.cnblogs.com/face/1816317/20200617192156.png", "title": "ATtiny88初体验(一):点灯", "desc": "ATtiny88是Atmel公司推出的一款基于8位AVR架构的高性能单片机,具有8KB的Flash、512B的SRAM以及64B的EEPROM。本文介绍了如何借助PROGISP软件配置ATtiny88的熔丝位,并基于搭载ATtiny88的MH-ETLIVETiny88核心板介绍如何点亮LED。另......", "url": "https://www.cnblogs.com/chinjinyu/p/17642707.html"},
{"author_name": "水果好好吃哦", "author_img": "https://pic.cnblogs.com/face/3252915/20230802213440.png", "title": "【pytorch】目标检测:新手也能彻底搞懂的YOLOv5详解", "desc": "YOLOv5是GlennJocher等人研发,它是Ultralytics公司的开源项目。YOLOv5根据参数量分为了`n、s、m、l、x`五种类型,其参数量依次上升,当然了其效果也是越来越好。从2020年6月发布至2022年11月已经更新了7个大版本,在v7版本中还添加了语义分割的功能。本文以YO...", "url": "https://www.cnblogs.com/wpx123/p/17642664.html"},
{"author_name": "xiezhr", "author_img": "https://pic.cnblogs.com/face/2381533/20210501223456.png", "title": "给你安利一款带有AI功能的数据库管理工具", "desc": "###写在前面说到数据库管理工具,大家应该不陌生了小伙伴们应该都用过**`Navicat`、`DBever`、`DataGrip`、`SQLyog`、`plsqldeveloper`等**数据库管理工具这些工具呢都各自有优缺点。今天要给大家推荐的是一款带有AI功能的数据库管理工具**`C...", "url": "https://www.cnblogs.com/xiezhr/p/17642516.html"},
{"author_name": "博客猿马甲哥", "author_img": "https://pic.cnblogs.com/face/587720/20220419171001.png", "title": "抓的是周树人,与我鲁迅有什么关系?", "desc": "单独拎出这个题目,是因为昨天看到一线码农大佬公众号的分享[Dictionary.Clear和newDictionary()有什么不同?](https://mp.weixin.qq.com/s/JUtr9TFRDfAvEeu6vJkI1w)###1.无心插画```voidExamp...", "url": "https://www.cnblogs.com/JulianHuang/p/17642511.html"},
{"author_name": "护发师兄", "author_img": "https://pic.cnblogs.com/face/2722327/20220216172220.png", "title": "[超详细] GraalVM打包含有JNI的本地镜像", "desc": "GraalVM是一种高性能、多语言通用虚拟机和编译器技术。它由Oracle开发并开源,旨在为不同的编程语言和应用场景提供统一的运行时环境和编译器平台。以下是GraalVM的一些主要特点和功能:1.**多语言支持:**GraalVM支持多种编程语言,包括Java、JavaScrip...", "url": "https://www.cnblogs.com/jonil/p/17642484.html"},
{"author_name": "博客园团队", "author_img": "https://pic.cnblogs.com/face/35695/20230612120532.png", "title": "【故障公告】多年的故障老朋友又来了:数据库服务器 CPU 100%", "desc": "数据库服务器CPU100%问题几乎每年都要来几次,今天`13:35`首先收到我们自己的异常告警通知。这时从博客后台看,操作的响应速度比较慢,但可以完成操作。我们知道它又来了,这次我们毫不犹豫,立马登录阿里云RDS控制台重启实例,重启时间是`13:37:58`。...", "url": "https://www.cnblogs.com/cmt/p/17642411.html"},
{"author_name": "疯狂学习GIS", "author_img": "https://pic.cnblogs.com/face/3080295/20230111162426.png", "title": "全球都有哪些高光谱遥感卫星?", "desc": "本文对目前国内外的**高光谱遥感卫星**加以汇总,并对主要卫星进行**参数**介绍与对比。[TOC](全球主要高光谱遥感卫星介绍与分析)#1引言上一篇文章[ENVI、ERDAS计算Landsat7地表温度:单窗算法实现](https://www.cnblogs.com/fkxxgis/p...", "url": "https://www.cnblogs.com/fkxxgis/p/17642359.html"},
{"author_name": "付威的网络博客", "author_img": "https://pic.cnblogs.com/face/562505/20200508203330.png", "title": "领域驱动设计(DDD):从基础代码探讨高内聚低耦合的演进", "desc": "大家好,我是付威,一名已在编码第一线奋斗了十余年的程序员。在2019年我初次接触到领域驱动设计(Domain-DrivenDesign,简称DDD)的概念。在我的探索中,我发现许多有关DDD的教程过于偏重于战略设计,充斥着许多晦涩难懂的概念,导致阅读起来相当艰难。有些教程往往只是解释了DDD的概念...", "url": "https://www.cnblogs.com/OceanHeaven/p/17642286.html"}
]
- Scrapy框架提供了
-o
选项来指定输出文件的格式和路径。- 使用-o选项时,可以将爬取到的数据保存为JSON或CSV格式。
scrapy crawl spidername -o output.json
:以JSON格式保存到output.json文件中。scrapy crawl spidername -o output.csv
:以CSV格式保存到output.csv文件中。
- 这种方案适用于一般的持久化存储需求
- 例如将爬取到的数据保存到本地文件中,方便后续的数据分析、处理或备份。
【2】方案二(使用pipline存储)
(1)第一步
-
在item.py中写一个类
import scrapy
class Day06StartItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = scrapy.Field()
author_img = scrapy.Field()
author_name = scrapy.Field()
desc = scrapy.Field()
url = scrapy.Field()
# 博客文章详情,但是暂时没有
content = scrapy.Field()
(2)第二步
多方案存储之本地存储
- 在pipline.py中写代码,写一个类:
- open_spide
- 开启爬虫会触发
- close_spider
- 爬完会触发
- process_item
- 每次要保存一个对象会触发
- open_spide
class StartBlogFilePipeline:
def open_spider(self, spider):
# print('我开了')
# 打开文件对象存储到对象中
self.f = open('a.txt', 'w', encoding='utf-8')
def close_spider(self, spider):
# print('我关了')
self.f.close()
# 这个很重要
def process_item(self, item, spider):
# 持久化存储到本地 txt 文件中
self.f.write(item['title'] + '\n')
return item
多方案存储之数据库存储
class MysqlStoragePipeline:
def open_spider(self, spider):
self.count = 0
print('我开了')
self.conn = pymysql.connect(
user='root', # The first four arguments is based on DB-API 2.0 recommendation.
password="1314521",
host='127.0.0.1',
port=3306,
database='cnblogs')
self.cursor = self.conn.cursor()
def close_spider(self, spider):
print('我关了')
# 关闭数据库
self.cursor.close()
# 关闭连接
self.conn.close()
def process_item(self, item, spider):
print('我来了-----')
self.count += 1
print(self.count)
sql = 'insert into article (title,url,`desc`,author_name,author_img,content) values (%s,%s,%s,%s,%s,%s)'
self.cursor.execute(sql,
args=[item['title'], item['url'], item['desc'], item['author_name'], item['author_img'],
item['content']])
# 每次存储数据都提交数据,防止数据丢失
self.conn.commit()
# 必须将 item 对象返回,下次调用
return item
(3)第三步
- 配置文件配置
ITEM_PIPELINES = {
# "day06Start.pipelines.Day06StartPipeline": 300,
# 优先级一
"day06Start.pipelines.StartBlogFilePipeline": 300,
# 优先级二
"day06Start.pipelines.MysqlStoragePipeline": 301,
}
(4)第四步
- 在解析方法parse中yield item对象
import scrapy
from ..items import Day06StartItem
# 爬虫类,继承了scrapy.Spider
class FirstSpider(scrapy.Spider):
# 爬虫文件的名称,是当前爬虫文件的唯一标识
name = "first"
# 允许访问的域名
allowed_domains = ["www.cnblogs.com/"]
# 起始的url列表:可以将即将被请求的url,存放在当前列表中。
# 默认情况,列表中存储的url都会被scrapy框架进行get请求的发送
start_urls = ["https://www.cnblogs.com/"]
# 实现数据解析
# 参数response表示请求对应的响应对象
# parse方法调用的次数取决于请求的次数
def parse(self, response):
# http响应包装成了response
# 可以在响应对象中直接使用xpath进行数据解析
# print(response.text)
# 实例化得到 item 对象
item = Day06StartItem()
######xpath选择器#####
# 解析出所有文章
article_list = response.xpath('//article[contains(@class,"post-item")]') # 列表中放对象
print(len(article_list))
# 继续解析文章详情:文章标题,文章摘要,作者图片,作者名字,文章详情地址
for article in article_list:
# title = article.xpath('./section/div/a/text()').extract_first()
title = article.xpath('.//a/text()').extract_first()
desc = article.xpath('.//p[contains(@class,"post-item-summary")]/text()').extract()
real_desc = desc[0].replace('\n', '').replace(' ', '')
if real_desc:
desc = real_desc
else:
real_desc = desc[1].replace('\n', '').replace(' ', '')
desc = real_desc
# p.post-item-summary img::attr(src)
author_img = article.xpath('.//p//img/@src').extract_first()
# div.post-item-text a::attr(href)
author_name = article.xpath('.//footer//span/text()').extract_first()
url = article.xpath('.//div[contains(@class,"post-item-text")]//a/@href').extract_first()
# print('''
# 文章名字:%s
# 文章摘要:%s
# 作者图片:%s
# 作者名字:%s
# 文章地址:%s
# ''' % (title, desc, author_img, author_name, url))
# 向 item 对象 提交数据
item['title'] = title
item['desc'] = desc
item['author_img'] = author_img
item['author_name'] = author_name
item['url'] = url
yield item
(5)启动项目
【五】全站爬取
【1】需求
- 继续爬取下一页
- 爬取文章详情
【2】引言
(1)Request创建
- Request创建:在parse中,for循环中,创建Request对象时,传入meta
- item对象一定要在for循环中创建,否则,当前页面都用同一个item导致同一页数据都一样
yield Request(url=url, callback=self.detail_parse,meta={'item':item})
(2)Response对象
- Response对象:detail_parse中,通过response取出meta取出item,把文章详情写入
def parser_detail(self,response):
# 获取到传入的 item 对象
item=response.meta.get('item')
# 继续解析需要解析的内容
content=str(response.xpath('//div[@id="cnblogs_post_body"]').extract_first())
# 向 item 对象中添加信息
item['content']=content
# 将完整的信息 返回
yield item
【3】实现
day06Start\spiders\first.py
import scrapy
from scrapy import Request
from ..items import Day06StartItem
class FirstSpider(scrapy.Spider):
name = "first"
allowed_domains = ["www.cnblogs.com/"]
start_urls = ["https://www.cnblogs.com/"]
def parse(self, response):
# 列表中放对象
article_list = response.xpath('//article[contains(@class,"post-item")]')
# print(len(article_list))
for article in article_list:
# 每次新造一个对象
# item对象一定要在for循环中创建
# 否则,当前页面都用同一个item导致同一页数据都一样
item = Day06StartItem()
title = article.xpath('.//a/text()').extract_first()
desc = article.xpath('.//p[contains(@class,"post-item-summary")]/text()').extract()
real_desc = desc[0].replace('\n', '').replace(' ', '')
if real_desc:
desc = real_desc
else:
real_desc = desc[1].replace('\n', '').replace(' ', '')
desc = real_desc
author_img = article.xpath('.//p//img/@src').extract_first()
author_name = article.xpath('.//footer//span/text()').extract_first()
url = article.xpath('.//div[contains(@class,"post-item-text")]//a/@href').extract_first()
item['title'] = title
item['desc'] = desc
item['author_img'] = author_img
item['author_name'] = author_name
item['url'] = url
# url : 调用上述方法解析当前页面
# detail_parse : 解析详情
# meta : 携带 item 对象给 detail_parse 解析后向内部追加内容
yield Request(url=url, callback=self.detail_parse, meta={'item': item}) # 详情
# 解析出下一个 链接
next_url = 'https://www.cnblogs.com' + response.xpath(
'//div[contains(@class,"pager")]/a[last()]/@href').extract_first()
print(next) # 拿到地址,继续爬取,组装成一个Request对象
# callback 参数是控制返回response后使用的解析方法
yield Request(url=next_url, callback=self.parse) # 下一页地址,继续爬取,解析还是用parse
# Response对象:detail_parse中,通过response取出meta取出item,把文章详情写入
def detail_parse(self, response):
# 获取到原来的 item 对象
item = response.meta.get('item')
# 解析到文章详情
content = str(response.xpath('//div[@id="cnblogs_post_body"]').extract_first())
# 继续向 item 对象中追加文章详情内容
item['content'] = content
# 将 item 对象提交给管道
yield item
day06Start\pipelines.py
import pymysql
from itemadapter import ItemAdapter
class StartBlogFilePipeline:
def open_spider(self, spider):
print('我开了')
self.f = open('a.txt', 'w', encoding='utf-8')
def close_spider(self, spider):
print('我关了')
self.f.close()
# 这个很重要
def process_item(self, item, spider):
self.f.write(item['title'] + '\n')
return item
class MysqlStoragePipeline:
def open_spider(self, spider):
self.count = 0
print('我开了')
self.conn = pymysql.connect(
user='root', # The first four arguments is based on DB-API 2.0 recommendation.
password="1314521",
host='127.0.0.1',
port=3306,
database='cnblogs')
self.cursor = self.conn.cursor()
def close_spider(self, spider):
print('我关了')
# 关闭数据库
self.cursor.close()
# 关闭连接
self.conn.close()
def process_item(self, item, spider):
print('我来了-----')
self.count += 1
print(self.count)
sql = 'insert into article (title,url,`desc`,author_name,author_img,content) values (%s,%s,%s,%s,%s,%s)'
self.cursor.execute(sql,
args=[item['title'], item['url'], item['desc'], item['author_name'], item['author_img'],
item['content']])
# 每次存储数据都提交数据,防止数据丢失
self.conn.commit()
# 必须将 item 对象返回,下次调用
return item
day06Start\items.py
import scrapy
class Day06StartItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = scrapy.Field()
author_img = scrapy.Field()
author_name = scrapy.Field()
desc = scrapy.Field()
url = scrapy.Field()
# 博客文章详情,但是暂时没有
content = scrapy.Field()
day06Start\settings.py
ITEM_PIPELINES = {
# "day06Start.pipelines.Day06StartPipeline": 300,
# 优先级一
# "day06Start.pipelines.StartBlogFilePipeline": 300,
# 优先级二
"day06Start.pipelines.MysqlStoragePipeline": 301,
}
【六】爬虫中间件和下载中间件
【1】爬虫中间件
# 爬虫中间件
class Day06StartSpiderMiddleware:
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the spider middleware does not modify the
# passed objects.
@classmethod
def from_crawler(cls, crawler):
# 该方法是Scrapy用于创建爬虫实例的方法。
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_spider_input(self, response, spider):
# 当响应从爬虫中间件进入爬虫时,调用该方法进行处理。
# 应返回None或引发异常。
# Called for each response that goes through the spider
# middleware and into the spider.
# Should return None or raise an exception.
return None
def process_spider_output(self, response, result, spider):
# 当爬虫处理完响应后,调用该方法对处理结果进行处理。
# 必须返回一个可迭代的Request对象或item对象。
# Called with the results returned from the Spider, after
# it has processed the response.
# Must return an iterable of Request, or item objects.
for i in result:
yield i
def process_spider_exception(self, response, exception, spider):
# 当爬虫中抛出异常时,调用该方法进行处理。
# 应返回None或者一个可迭代的Request对象或item对象。
# Called when a spider or process_spider_input() method
# (from other spider middleware) raises an exception.
# Should return either None or an iterable of Request or item objects.
pass
def process_start_requests(self, start_requests, spider):
# 在爬虫启动时,对初始请求进行处理。
# Called with the start requests of the spider, and works
# similarly to the process_spider_output() method, except
# that it doesn’t have a response associated.
# Must return only requests (not items).
for r in start_requests:
yield r
def spider_opened(self, spider):
spider.logger.info("Spider opened: %s" % spider.name)
这段代码是一个爬虫中间件,用于在Scrapy框架中对爬虫进行处理。下面是对每个方法的功能和用途的详细解释:
class Day06StartSpiderMiddleware: # Not all methods need to be defined. If a method is not defined, # scrapy acts as if the spider middleware does not modify the # passed objects. @classmethod def from_crawler(cls, crawler): # 该方法是Scrapy用于创建爬虫实例的方法。 s = cls() crawler.signals.connect(s.spider_opened, signal=signals.spider_opened) return s
from_crawler
方法是一个类方法,它会在创建爬虫实例时被Scrapy调用,用于初始化爬虫中间件的实例。在该方法中,首先创建了一个中间件实例s
,然后通过crawler.signals.connect
方法连接了spider_opened
信号和对应的处理方法。def process_spider_input(self, response, spider): # 当响应从爬虫中间件进入爬虫时,调用该方法进行处理。 # 应返回None或引发异常。 return None
process_spider_input
方法会在响应从爬虫中间件传递到爬虫之前调用。它接收两个参数:response
是响应对象,spider
是当前爬虫实例。这个方法可以用来对响应进行预处理或检查。应该返回None
或引发异常。def process_spider_output(self, response, result, spider): # 当爬虫处理完响应后,调用该方法对处理结果进行处理。 # 必须返回一个可迭代的Request对象或item对象。 for i in result: yield i
process_spider_output
方法在爬虫处理完响应后会被调用。它接收三个参数:response
是爬虫处理后的响应对象,result
是爬虫的处理结果,spider
是当前爬虫实例。这个方法主要用于对爬虫处理结果进行进一步处理或过滤,并将处理结果返回。必须返回一个可迭代的Request对象或item对象。def process_spider_exception(self, response, exception, spider): # 当爬虫中抛出异常时,调用该方法进行处理。 # 应返回None或者一个可迭代的Request对象或item对象。 pass
process_spider_exception
方法在爬虫或process_spider_input()
方法中抛出异常时会被调用。它接收三个参数:response
是发生异常的响应对象,exception
是抛出的异常对象,spider
是当前爬虫实例。这个方法可以用来对爬虫处理过程中的异常进行处理,可以返回None
或一个可迭代的Request对象或item对象。def process_start_requests(self, start_requests, spider): # 在爬虫启动时,对初始请求进行处理。 # Must return only requests (not items). for r in start_requests: yield r
process_start_requests
方法在爬虫启动时被调用,用于对初始请求进行处理。它接收两个参数:start_requests
是初始请求的列表,spider
是当前爬虫实例。这个方法必须返回一个可迭代的Request对象,而不能返回item对象。def spider_opened(self, spider): spider.logger.info("Spider opened: %s" % spider.name)
spider_opened
方法在爬虫打开时被调用。它接收一个参数spider
,表示当前爬虫实例。在这个方法中,通过日志记录器(logger
)输出 "Spider opened: 爬虫名称" 的信息。
【2】下载中间件
# 下载中间件
class Day06StartDownloaderMiddleware:
# 不是所有的方法都需要定义。如果某个方法没有被定义,
# Scrapy会认为这个下载中间件不会修改传递的对象。
@classmethod
def from_crawler(cls, crawler):
# Scrapy使用该方法创建您的爬虫。
s = cls()
# 通过signals连接spider_opened信号和spider_opened方法
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
# 拦截处理所有的请求对象
# 参数:request就是拦截到的请求对象,spider爬虫文件中爬虫类实例化的对象
# spider参数的作用可以实现爬虫类和中间类的数据交互
def process_request(self, request, spider):
# 返回None:继续处理本次请求,执行下一个中间件的process_request方法
# 返回一个Response对象:执行当前中间件的process_response方法,重新回到引擎,被调度
# 返回一个Request对象:直接返回给引擎,被调度。进入调度器等待下次被调用
# 抛出IgnoreRequest异常:调用已安装的下载中间件的process_exception方法
return None
# 拦截处理所有的响应对象
# 参数:response就是拦截到的响应对象,request就是被拦截到响应对象对应的唯一的一个请求对象
def process_response(self, request, response, spider):
# - 返回一个Response对象:继续执行,进入引擎,被调度到爬虫进行解析
# - 返回一个Request对象:进入引擎,返回到调度器被重新调用
# - 或者抛出IgnoreRequest异常:抛出异常
return response
# 拦截和处理发生异常的请求对象
# 参数:reqeust就是拦截到的发生异常的请求对象
# 方法存在的意义:将发生异常的请求拦截到,然后对其进行修正
def process_exception(self, request, exception, spider):
# 当下载处理程序或process_request()方法(来自其他下载中间件)引发异常时调用。
# 必须返回以下之一:
# - 返回None:继续处理该异常
# - 返回一个Response对象:停止process_exception()链
# - 返回一个Request对象:停止process_exception()链
pass
# 控制日志数据的(忽略)
def spider_opened(self, spider):
spider.logger.info("Spider opened: %s" % spider.name)
from_crawler(cls, crawler)
方法:该方法是Scrapy用来创建爬虫的入口点。它返回一个中间件对象,并通过crawler.signals
连接到spider_opened
信号,以便在爬虫开启时执行相应的操作。process_request(self, request, spider)
方法:该方法在发送请求之前被调用。您可以在此方法中对请求进行处理和修改。返回值决定了后续处理的行为,可以是None
继续处理当前请求,返回一个Response
对象以便执行process_response
方法,返回一个Request
对象以便重新调度,或者抛出IgnoreRequest
异常以调用其他下载中间件的process_exception
方法。process_response(self, request, response, spider)
方法:该方法在收到响应后被调用。您可以在此方法中对响应进行处理和修改。返回值决定了后续处理的行为,可以是返回Response
对象以便进一步处理和解析,返回Request
对象以便重新调度,或者抛出IgnoreRequest
异常。process_exception(self, request, exception, spider)
方法:当下载处理程序或其他下载中间件的process_request
方法引发异常时调用该方法。您可以在此处处理异常,并根据需要返回值。spider_opened(self, spider)
方法:在爬虫开启时被调用。在这个示例中,它会输出一个日志信息。
【3】配置文件
# Enable or disable spider middlewares
# See https://docs.scrapy.org/en/latest/topics/spider-middleware.html
SPIDER_MIDDLEWARES = {
"day06Start.middlewares.Day06StartSpiderMiddleware": 543,
}
# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
"day06Start.middlewares.Day06StartDownloaderMiddleware": 543,
}
【七】修改请求头
- 在下载中间件的def process_request(self, request, spider):写代码
【1】加代理
- 在下载中间件写process_request方法
def get_proxy(self):
import requests
res = requests.get('http://127.0.0.1:5010/get/').json()
if res.get('https'):
return 'https://' + res.get('proxy')
else:
return 'http://' + res.get('proxy')
def process_request(self, request, spider):
request.meta['proxy'] = self.get_proxy()
return None
- 代理可能不能用,会触发process_exception,在里面写
def process_exception(self, request, exception, spider):
# 第二步:代理可能不能用,会触发process_exception,在里面写
def process_exception(self, request, exception, spider):
print('-----', request.url) # 这个地址没有爬
return request
【2】加cookies
def process_request(self, request, spider):
# 添加cookie
request.cookies['cookies'] = 'cookies'
print(request.url+':请求对象拦截成功!')
return None
【3】修改请求头
def process_request(self, request, spider):
request.headers['referer'] = 'http://www.lagou.com'
return None
【4】随机UA
# 动态生成User-agent使用
def process_request(self, request, spider):
# fake_useragent模块
from fake_useragent import UserAgent
request.headers['User-Agent']=str(UserAgent().random)
pprint(request.url+':请求对象拦截成功!')
return None
【5】小结
def process_request(self, request, spider):
# 返回None:继续处理本次请求,执行下一个中间件的process_request方法
# 返回一个Response对象:执行当前中间件的process_response方法,重新回到引擎,被调度
# 返回一个Request对象:直接返回给引擎,被调度。进入调度器等待下次被调用
# 抛出IgnoreRequest异常:调用已安装的下载中间件的process_exception方法
# 构建代理池
# 第一步:构建代理池
def get_proxy(self):
import requests
res = requests.get('http://127.0.0.1:5010/get/').json()
if res.get('https'):
return 'https://' + res.get('proxy')
else:
return 'http://' + res.get('proxy')
def process_request(self, request, spider):
# 在 meta 中加入代理
request.meta['proxy'] = self.get_proxy()
# 代理可能不能用,会触发process_exception,在里面写
return None
# 添加cookie
request.cookies['cookies'] = 'cookies'
# 修改请求头
request.headers['referer'] = 'http://www.lagou.com'
# 动态生成User-agent使用
request.headers['User-Agent'] = str(UserAgent().random)
return None
def process_exception(self, request, exception, spider):
# 第二步:代理可能不能用,会触发process_exception,在里面写
def process_exception(self, request, exception, spider):
print('-----', request.url) # 这个地址没有爬
return request
【八】Scrapy集成selenium
【1】引言
-
使用scrapy默认下载器
- 类似于requests模块发送请求,不能执行js,有的页面拿回来数据不完整
-
想在scrapy中集成selenium
- 获取数据更完整,获取完后,自己组装成 Response对象
- 就会进爬虫解析,现在解析的是使用selenium拿回来的页面,数据更完整
【2】使用步骤
- 保证整个爬虫中,只有一个浏览器器
- 只要爬取 下一页这种地址,使用selenium,爬取详情,继续使用原来的
(1)第一步
- 在爬虫类中写
from selenium import webdriver
class CnblogsSpider(scrapy.Spider):
# 实例化WebDriver,这里以Chrome为例
bro = webdriver.Chrome()
bro.implicitly_wait(10)
def close(spider, reason):
spider.bro.close() #浏览器关掉
(2)第二步
- 在中间件中
def process_request(self, request, spider):
# 爬取下一页这种地址---》用selenium,但是文章详情,就用原来的
if 'sitehome/p' in request.url:
spider.bro.get(request.url)
from scrapy.http.response.html import HtmlResponse
response = HtmlResponse(url=request.url, body=bytes(spider.bro.page_source, encoding='utf-8'))
return response
else:
return None
【3】案例
- 爬虫程序
import scrapy
from scrapy import Request
from selenium import webdriver
from ..items import Day06StartItem
class FirstSpider(scrapy.Spider):
name = "first"
allowed_domains = ["www.cnblogs.com/"]
start_urls = ["https://www.cnblogs.com/"]
# 实例化的到 Edge浏览器对象
browser = webdriver.Edge()
# 隐式等待
browser.implicitly_wait(10)
def parse(self, response):
# 列表中放对象
article_list = response.xpath('//article[contains(@class,"post-item")]')
# print(len(article_list))
for article in article_list:
# 每次新造一个对象
# item对象一定要在for循环中创建
# 否则,当前页面都用同一个item导致同一页数据都一样
item = Day06StartItem()
title = article.xpath('.//a/text()').extract_first()
desc = article.xpath('.//p[contains(@class,"post-item-summary")]/text()').extract()
real_desc = desc[0].replace('\n', '').replace(' ', '')
if real_desc:
desc = real_desc
else:
real_desc = desc[1].replace('\n', '').replace(' ', '')
desc = real_desc
author_img = article.xpath('.//p//img/@src').extract_first()
author_name = article.xpath('.//footer//span/text()').extract_first()
url = article.xpath('.//div[contains(@class,"post-item-text")]//a/@href').extract_first()
item['title'] = title
item['desc'] = desc
item['author_img'] = author_img
item['author_name'] = author_name
item['url'] = url
# url : 调用上述方法解析当前页面
# detail_parse : 解析详情
# meta : 携带 item 对象给 detail_parse 解析后向内部追加内容
yield Request(url=url, callback=self.detail_parse, meta={'item': item}) # 详情
# 解析出下一个 链接
next_url = 'https://www.cnblogs.com' + response.xpath(
'//div[contains(@class,"pager")]/a[last()]/@href').extract_first()
print(next) # 拿到地址,继续爬取,组装成一个Request对象
# callback 参数是控制返回response后使用的解析方法
yield Request(url=next_url, callback=self.parse) # 下一页地址,继续爬取,解析还是用parse
# Response对象:detail_parse中,通过response取出meta取出item,把文章详情写入
def detail_parse(self, response):
# 获取到原来的 item 对象
item = response.meta.get('item')
# 解析到文章详情
content = str(response.xpath('//div[@id="cnblogs_post_body"]').extract_first())
# 继续向 item 对象中追加文章详情内容
item['content'] = content
# 将 item 对象提交给管道
yield item
def close(spider, reason):
# 关闭浏览器
spider.browser.close()
- 中间件
# 下载中间件
class Day06StartDownloaderMiddleware:
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_request(self, request, spider):
# return None: 继续处理本次请求,执行下一个中间件的 process_request
# return a Response object:执行当前中间件的 process_response ,重新回到引擎,被调度
# return a Request object :直接返回,给引擎,被调度。进入到调度器等待下次被调用
# raise IgnoreRequest: process_exception() methods of
# installed downloader middleware will be called
# 加入 selenium
# 爬取下一页这种地址---》用selenium,但是文章详情,就用原来的
if 'sitehome/p' in request.url:
# 调用父类的 browser 对象
spider.browser.get(request.url)
from scrapy.http.response.html import HtmlResponse
response = HtmlResponse(url=request.url, body=bytes(spider.bro.page_source, encoding='utf-8'))
return response
else:
return None
【九】源码去重规则(布隆过滤器)
- 在Scrapy框架中,为了防止重复爬取相同的URL地址,采用了去重机制。
- 其中,一种常用的去重方法是使用布隆过滤器(Bloom Filter)。
-
如果爬取过的地址,就不会再爬了
- 调度器可以去重,研究一下,如何去重的
- 使用了集合
-
要爬取的Request对象
- 在进入到scheduler调度器排队之前,先执行enqueue_request
- 它如果return False,这个Request就丢弃掉,不爬了
- 如何判断这个Request要不要丢弃掉,执行了self.df.request_seen(request),它来决定的
- RFPDupeFilter类中的方法
- request_seen
- 会返回True或False
- 如果这个request在集合中,说明爬过了,就return True,如果不在集合中,就加入到集合中,然后返回False
- 在进入到scheduler调度器排队之前,先执行enqueue_request
【1】调度器源码
- 在Scrapy框架的调度器中
- 通过enqueue_request方法来判断是否需要对某个URL进行爬取
from scrapy.core.scheduler import Scheduler
# 这个方法如果return True表示这个request要爬取,如果return False表示这个网址就不爬了(已经爬过了)
def enqueue_request(self, request: Request) -> bool:
# request当次要爬取的地址对象
if self.df.request_seen(request):
# 有的请情况,在爬虫中解析出来的网址,不想爬了,就就可以指定
# yield Request(url=url, callback=self.detail_parse, meta={'item': item},dont_filter=True)
# 如果符合这个条件,表示这个网址已经爬过了
return False
return True
- 如果调度器中的去重类(RFPDupeFilter)的request_seen方法返回True,表示该URL已经被爬取过,不需要再次爬取;
- 如果返回False,表示该URL是新的,需要进行爬取。
【2】self.df 去重类
- 去重类的对象 RFPDupeFilter
# 表示,使用它作为去重类,按照它的规则做去重
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
- 该类的request_seen方法负责判断URL是否已经被爬取过,并返回相应的结果。
def request_seen(self, request: Request) -> bool:
# request_fingerprint 生成指纹
fp = self.request_fingerprint(request) # request当次要爬取的地址对象
# 判断 fp 在不在集合中,如果在,return True
if fp in self.fingerprints:
return True
# 如果不在,加入到集合,return False
self.fingerprints.add(fp)
return False
- 传进来是个request对象,生成的是指纹
- 爬取的网址
- 它俩是一样的,返回的数据都是一样的,就应该是一条url,就只会爬取一次
- 所以 request_fingerprint 就是来把它们做成一样的(核心原理是把查询条件排序,再拼接到后面)
【3】指纹生成原理
- 在Scrapy框架中,使用RequestFingerprinter来生成URL的指纹。
- 指纹是通过对URL进行规范化(排序后拼接)和加密生成的。
- 例如,对于以下两个URL:
- 这两个URL最终生成的指纹是相同的,因为它们具有相同的请求参数,只是参数的顺序不同。
from scrapy.utils.request import RequestFingerprinter
from scrapy import Request
fingerprinter = RequestFingerprinter()
request1 = Request(url='http://www.cnblogs.com?name=lqz&age=20')
request2 = Request(url='http://www.cnblogs.com?age=20&name=lqz')
res1 = fingerprinter.fingerprint(request1).hex()
res2 = fingerprinter.fingerprint(request2).hex()
print(res1)
print(res2)
- 假设爬了1亿条url,放在内存中,占空间非常大
- a6af0a0ffa18a9b2432550e1914361b6bffcff1a
- a6af0a0ffa18a9b2432550e191361b6bffc34f1a
- 想一种方式,极小内存实现去重---》布隆过滤器
【5】Scrapy的去重规则
- 根据配置的去重类RFPDupeFilter的request_seen方法,如果返回True,就不爬了,如果返回False就爬
- 后期咱们可以使用自己定义的去重类,实现去重
- Scrapy框架提供了灵活的去重规则配置。
- 可以根据需要自定义去重类,并在配置文件中指定相应的去重类。
- 例如:
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
- 通过配置DUPEFILTER_CLASS为RFPDupeFilter,即可按照RFPDupeFilter的去重规则进行URL的去重操作。
【6】布隆过滤器引入
- 更小内存实现去重
- 如果是集合:存的数据库越多,占内存空间越大,如果数据量特别大,可以使用布隆过滤器实现去重
bloomfilter:
- 是一个通过多哈希函数映射到一张表的数据结构,能够快速的判断一个元素在一个集合内是否存在,具有很好的空间和时间效率。(典型例子,爬虫url去重)
原理:
- BloomFilter 会开辟一个m位的bitArray(位数组),开始所有数据全部置 0 。
- 当一个元素(www.baidu.com)过来时,能过多个哈希函数(h1,h2,h3....)计算不同的在哈希值,并通过哈希值找到对应的bitArray下标处,将里面的值 0 置为 1 。
-
布隆过滤器是一种基于哈希函数映射的数据结构,常用于快速判断一个元素是否在一个集合内,具有较好的时间和空间效率。
- 它主要解决了传统集合数据结构在大规模数据存储和查询时所遇到的空间占用和时间复杂度的问题。
-
布隆过滤器的主要优点之一是它可以使用相对较小的内存空间来实现去重功能。
- 普通集合在存储大量数据时,占用的内存空间会随着数据量的增加而线性增长
- 而布隆过滤器则可以通过多个哈希函数将数据映射到一个位数组中,并将该位置的值置为1来表示存在,从而大大减少了空间占用。
-
在一些需要进行去重操作的场景中,如果数据量特别大,选择使用布隆过滤器可以提供较好的性能。
- 例如,在爬虫中进行URL去重时,由于爬取的网页数量巨大,使用传统集合进行去重会占用大量的内存空间
- 而布隆过滤器可以有效地减少内存消耗,提高去重的效率。
-
关于布隆过滤器的更详细介绍和原理
【7】Python中使用布隆过滤器
- 在Python中,可以使用第三方库来实现布隆过滤器的功能。
- 安装模块
pip install pybloom_live
(1)测试布隆过滤器
- 可以自动扩容指定错误率,底层数组如果大于了错误率会自动扩容
from pybloom_live import ScalableBloomFilter, BloomFilter
# 创建一个可扩容的布隆过滤器对象
bloom = ScalableBloomFilter(initial_capacity=100, error_rate=0.001, mode=ScalableBloomFilter.LARGE_SET_GROWTH)
# 添加元素到布隆过滤器中
url = "www.cnblogs.com"
url2 = "www.liu********g.top"
bloom.add(url)
bloom.add(url2)
# 判断元素是否在布隆过滤器中
print(url in bloom)
print(url2 in bloom)
- 上述示例中,使用
ScalableBloomFilter
类创建了一个可扩容的布隆过滤器对象,并通过add
方法将元素添加到布隆过滤器中。- 最后,使用
in
关键字判断元素是否存在于布隆过滤器中。
(2)使用redis
实现布隆过滤器
- 使用
redis
可以实现分布式布隆过滤器,将其应用于更大规模的数据集合去重场景。 - 以下是简单实现的示例:
- 首先需要编译安装
redis
,在安装时需要将第三方扩展布隆过滤器编译进去,才能使用布隆过滤器的功能。
- 首先需要编译安装
- 具体的步骤可以参考相关链接
import redis
# 连接redis数据库
r = redis.Redis(host='localhost', port=6379)
# 创建一个布隆过滤器对象
r.execute_command('BF.RESERVE', 'my_filter', '0.001', '100000')
# 添加元素到布隆过滤器中
r.execute_command('BF.ADD', 'my_filter', 'www.baidu.com')
# 判断元素是否在布隆过滤器中
result = r.execute_command('BF.EXISTS', 'my_filter', 'www.baidu.com')
if result == 1:
print("Element exists")
else:
print("Element does not exist")
- 上述示例中,使用
redis
库连接到Redis数据库,并使用BF.RESERVE
命令创建了一个布隆过滤器对象。- 然后,使用
BF.ADD
命令将元素添加到布隆过滤器中。- 最后,使用
BF.EXISTS
命令判断元素是否存在于布隆过滤器中。
【8】小结
-
如果有去重的情况,就可以使用集合
- 但是集合占的内存空间大,如果到了亿级别的数据量,想一种更小内存占用,而去重的方案
- 布隆过滤器
-
布隆过滤器:
- 通过不同的hash函数,加底层数组实现的极小内存去重
-
python中如何使用:pybloom_live
- 指定错误率
- 指定大小
-
使用redis实现布隆过滤器
- 编译redis(把第三方扩展布隆过滤器编译进去,才有这个功能)
【十】分布式爬虫
【1】引言
-
原来的Scrapy框架中
- Scheduler维护着本机的任务队列(待爬取的URL)和本机的去重队列(存放在集合中),这限制了爬虫只能在单台机器上运行。
-
如果将Scrapy项目部署到多台机器上
- 每台机器爬取的内容都会重复。
-
所以实现分布式爬取的关键就是
- 找一台专门的主机上运行一个共享的队列比如Redis
-
然后重写Scrapy的Scheduler,让新的Scheduler到共享队列存取Request,并且去除重复的Request请求
-
所以总结下来,实现分布式的关键就是三点:
- 1、多台机器共享队列
- 2、重写Scheduler,让其无论是去重还是任务都去访问共享队列
- 3、为Scheduler定制去重规则(利用redis的集合类型)
【2】scrapy-redis实现分布式爬虫
- scrapy-redis是Scrapy框架的一个扩展,可以方便地实现分布式爬虫。
- 主要功能包括:
- 公共的去重:
- 使用Redis的集合类型进行去重,避免重复爬取相同的URL。
- 公共的待爬取地址队列:
- 使用Redis的列表类型作为待爬取地址队列,多个机器可以共享这个队列。
- 公共的去重:
【3】使用步骤
- 官网参考:
(1)安装scrapy-redis
- 首先需要安装scrapy-redis扩展,可以使用以下命令进行安装:
pip intall scrapy-redis
(2)继承RedisSpider
- 将之前的爬虫类继承
RedisSpider
,这样可以使用scrapy-redis提供的功能。
class FirstSpider(RedisSpider):
(3)去掉起始爬取的地址
- 加入一个类属性
- 在爬虫类中添加一个类属性
redis_key
,用于手动插入起始地址。
# redis列表的key,后期我们需要手动插入起始地址
redis_key = 'myspider:start_urls'
(4)配置文件中配置
- 在Scrapy项目的配置文件中进行如下配置:
# 使用scrapy-redis提供的去重类,基于Redis的集合实现去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 不使用原生的调度器了,使用scrapy-redis提供的调度器,它就是使用了redis的列表
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 主机名
REDIS_HOST = 'localhost'
# 端口
REDIS_PORT = 6379
# 启用管道,可以自定义存储数据的方式
ITEM_PIPELINES = {
# 'mysfirstscrapy.pipelines.MyCnblogsPipeline': 300,
# 原来使用的是MySQL数据库
'mysfirstscrapy.pipelines.MyCnblogsMySqlPipeline': 301,
# 官方提供给我们存放到Redis缓存中
'scrapy_redis.pipelines.RedisPipeline': 400,
}
(5)启动项目
- 在不同的机器上运行Scrapy爬虫,就可以实现分布式爬虫。
- 可以使用以下命令向Redis队列中添加请求地址:
lpush myspider:start_urls '目标地址'
本文来自博客园,作者:Chimengmeng,转载请注明原文链接:https://www.cnblogs.com/dream-ze/p/17647616.html