Go分布式爬虫笔记(二十二)

news/2024/4/24 23:29:38/

文章目录

  • 22 辅助任务管理:任务优先级、去重与失败处理
  • 设置爬虫最大深度
  • 避免请求重复
  • 设置优先队列
  • 设置随机User-Agent
  • 失败处理

22 辅助任务管理:任务优先级、去重与失败处理

设置爬虫最大深度

目的:

  • 防止访问陷入到死循环
  • 控制爬取的有效链接的数量

最大爬取深度是和任务有关的,因此我们要在 Request 中加上 MaxDepth 这个字段,它可以标识到爬取的最大深度。Depth 则表示任务的当前深度,最初始的深度为 0。

type Request struct {Url       stringCookie    stringWaitTime  time.DurationDepth     intMaxDepth  intParseFunc func([]byte, *Request) ParseResult
}

那在异步爬取的情况下,我们怎么知道当前网站的深度呢?最好的时机是在采集引擎采集并解析爬虫数据,并将下一层的请求放到队列中的时候。以我们之前写好的 ParseURL 函数为例,在添加下一层的 URL 时,我们将 Depth 加 1,这样就标识了下一层的深度。

func ParseURL(contents []byte, req *collect.Request) collect.ParseResult {re := regexp.MustCompile(urlListRe)matches := re.FindAllSubmatch(contents, -1)result := collect.ParseResult{}for _, m := range matches {u := string(m[1])result.Requesrts = append(result.Requesrts, &collect.Request{Url:      u,WaitTime: req.WaitTime,Cookie:   req.Cookie,Depth:    req.Depth + 1,MaxDepth: req.MaxDepth,ParseFunc: func(c []byte, request *collect.Request) collect.ParseResult {return GetContent(c, u)},})}return result
}

最后一步,我们在爬取新的网页之前,判断最大深度。如果当前深度超过了最大深度,那就不再进行爬取。


func (r *Request) Check() error {if r.Depth > r.MaxDepth {return errors.New("Max depth limit reached")}return nil
}func (s *Schedule) CreateWork() {for {r := <-s.workerChif err := r.Check(); err != nil {s.Logger.Error("check failed",zap.Error(err),)continue}...}
}

避免请求重复

目的:

  • 避免死循环
  • 无效爬取

考虑点:

  • 用什么数据结构来存储数据才能保证快速地查找到请求的记录?
    哈希表
  • 如何保证并发查找与写入时,不出现并发冲突问题?
    锁, sync.Map
  • 在什么条件下,我们才能确认请求是重复的,从而停止爬取?
    任务进行前检查

在解决上面的三个问题之前,我们先优化一下代码。我们之前的 Request 结构体会在每一次请求时发生变化,但是我们希望有一个字段能够表示一整个网站的爬取任务,因此我们需要抽离出一个新的结构 Task ​作为一个爬虫任务,而 Request 则作为单独的请求存在。有些参数是整个任务共有的,例如 Task 中的 Cookie、MaxDepth(最大深度)、WaitTime(默认等待时间)和 RootReq(任务中的第一个请求)。


type Task struct {Url         stringCookie      stringWaitTime    time.DurationMaxDepth    intRootReq     *RequestFetcher     Fetcher
}// 单个请求
type Request struct {Task      *TaskUrl       stringDepth     intParseFunc func([]byte, *Request) ParseResult
}

由于抽象出了 Task,代码需要做对应的修改,例如我们需要把初始的 Seed 种子任务替换为 Task 结构。

for i := 0; i <= 0; i += 25 {str := fmt.Sprintf("<https://www.douban.com/group/szsh/discussion?start=%d>", i)seeds = append(seeds, &collect.Task{...Url:      str,RootReq: &collect.Request{ParseFunc: doubangroup.ParseURL,},})}

同时,在深度检查时,每一个请求的最大深度需要从 Task 字段中获取。


func (r *Request) Check() error {if r.Depth > r.Task.MaxDepth {return errors.New("Max depth limit reached")}return nil
}

接下来,我们继续用一个哈希表结构来存储历史请求。
由于我们希望随时访问哈希表中的历史请求,所以把它放在 Request、Task 中都不合适。 放在调度引擎中也不合适,因为调度引擎从功能上讲,应该只负责调度才对。所以,我们还需要完成一轮抽象,将调度引擎抽离出来作为一个接口,让它只做调度的工作,不用负责存储全局变量等任务。

type Crawler struct {out         chan collect.ParseResult //负责处理爬取后的数据,完成下一步的存储操作。schedule 函数会创建调度程序,负责的是调度的核心逻辑。Visited     map[string]bool          //存储请求访问信息VisitedLock sync.Mutexoptions
}type Scheduler interface {Schedule()                //启动调度器Push(...*collect.Request) //将请求放入到调度器中Pull() *collect.Request   //从调度器中获取请求
}type Schedule struct {requestCh chan *collect.Request //负责接收请求workerCh  chan *collect.Request //负责分配任务给 workerreqQueue  []*collect.RequestLogger    *zap.Logger
}

Visited 中的 Key 是请求的唯一标识,我们现在先将唯一标识设置为 URL + method 方法,并使用 MD5 生成唯一键。后面我们还会为唯一标识加上当前请求的规则条件。

// 请求的唯一识别码
func (r *Request) Unique() string {block := md5.Sum([]byte(r.Url + r.Method))return hex.EncodeToString(block[:])
}

接着,编写 HasVisited 方法,判断当前请求是否已经被访问过。StoreVisited 方法用于将请求存储到 Visited 哈希表中。

func (e *Crawler) HasVisited(r *collect.Request) bool {e.VisitedLock.Lock()defer e.VisitedLock.Unlock()unique := r.Unique()return e.Visited[unique]
}func (e *Crawler) StoreVisited(reqs ...*collect.Request) {e.VisitedLock.Lock()defer e.VisitedLock.Unlock()for _, r := range reqs {unique := r.Unique()e.Visited[unique] = true}
}

最后在 Worker 中,在执行 request 前,判断当前请求是否已被访问。如果请求没有被访问过,将 request 放入 Visited 哈希表中。

func (s *Crawler) CreateWork() {for {r := s.scheduler.Pull()if err := r.Check(); err != nil {s.Logger.Error("check failed",zap.Error(err),)continue}// 判断当前请求是否已被访问if s.HasVisited(r) {s.Logger.Debug("request has visited",zap.String("url:", r.Url),)continue}// 设置当前请求已被访问s.StoreVisited(r)...}
}

设置优先队列

爬虫任务的优先级有时并不是相同的,一些任务需要优先处理。因此,接下来我们就来设置一个任务的优先队列。优先队列还可以分成多个等级,在这里将它简单地分为了两个等级,即优先队列和普通队列。优先级更高的请求会存储到 priReqQueue 优先队列中。

type Schedule struct {requestCh   chan *collect.RequestworkerCh    chan *collect.RequestpriReqQueue []*collect.RequestreqQueue    []*collect.RequestLogger      *zap.Logger
}

设置随机User-Agent

避免服务器检测到我们使用了同一个 User-Agent,继而判断出是同一个客户端在发出请求,我们可以为发送的 User-Agent 加入随机性。

这个操作的本质就是将浏览器的不同型号与不同版本拼接起来,组成一个新的 User-Agent。

随机生成 User-Agent 的逻辑位于 extensions/randomua.go 中,里面枚举了不同型号的浏览器和不同型号的版本,并且通过排列组合产生了不同的 User-Agent。

最后一步,我们要在采集引擎中调用 GenerateRandomUA 函数,将请求头设置为随机的 User-Agent,如下所示:

func (b BrowserFetch) Get(request *spider.Request) ([]byte, error) {...req.Header.Set("User-Agent", extensions.GenerateRandomUA())resp, err := client.Do(req)

失败处理

我们在爬取网站时,网络超时等诸多潜在风险都可能导致爬取失败。这时,我们可以对失败的任务进行重试。但是如果网站多次失败,那就没有必要反复重试了,我们可以将它们放入单独的队列中。为了防止失败请求日积月久导致的内存泄露,同时也为了在程序崩溃后能够再次加载这些失败网站,我们最后还需要将这些失败网站持久化到数据库或文件中。

我们先完成前半部分,即失败重试。我们要在全局 Crawler 中存储 failures 哈希表,设置 Key 为请求的唯一键,用于快速查找。failureLock 互斥锁用于并发安全。

type Crawler struct {...failures    map[string]*collect.Request // 失败请求id -> 失败请求failureLock sync.Mutex
}

当请求失败之后,调用 SetFailure 方法将请求加入到 failures 哈希表中,并且把它重新交由调度引擎进行调度。这里我们为任务 Task 引入了一个新的字段 Reload,标识当前任务的网页是否可以重复爬取。如果不可以重复爬取,我们需要在失败重试前删除 Visited 中的历史记录。

「此文章为4月Day6学习笔记,内容来源于极客时间《Go分布式爬虫实战》,强烈推荐该课程!/推荐该课程」


http://www.ppmy.cn/news/46850.html

相关文章

ChatGPT实战100例 - (09) Python工具类库终结者

文章目录 ChatGPT实战100例 - (09) Python工具类库终结者一、需求与思路二、时间工具三、扩充工具四、编写测试五、 总结 ChatGPT实战100例 - (09) Python工具类库终结者 一、需求与思路 自从用了ChatGPT&#xff0c;再也不用满大街找工具类了。 需要啥工具&#xff0c;咱用C…

数据结构入门(C语言版)二叉树概念及结构(入门)

二叉树概念及结构&#xff08;入门&#xff09; 树的概念及结构1.树的概念及结构1.1 树的概念1.2 树的相关知识1.3 树的结构体表示1.4 树的实际运用 2.二叉树概念及结构2.1 二叉树的概念2.2 现实中的二叉树2.3 特殊的二叉树2.4 二叉树的性质2.5 二叉树的存储结构 结语 树的概念…

根据 cadence 设计图学习硬件知识 day01了解腾锐 D2000芯片

1. 首先了解 腾锐 D2000 1.介绍 腾锐D2000 芯片 D2000芯片集成8个飞腾自主研发的新一代高性能处理器内核FTC663&#xff0c;采用乱序四发射超标量流水线&#xff0c;兼容64位ARMV8指令集并支持ARM64和ARM32两种执行模式&#xff0c;支持单精度、双精度浮点运算指令和ASIMD处…

设计模式 -- 门面模式

前言 月是一轮明镜,晶莹剔透,代表着一张白纸(啥也不懂) 央是一片海洋,海乃百川,代表着一块海绵(吸纳万物) 泽是一柄利剑,千锤百炼,代表着千百锤炼(输入输出) 月央泽,学习的一种过程,从白纸->吸收各种知识->不断输入输出变成自己的内容 希望大家一起坚持这个过程,也同…

100种思维模型之非共识思维模型-48

某一件事或者某一个环境当中&#xff0c;绝大多数人往右走&#xff0c;而你 发现真正的路是左边 的时候&#xff0c;该怎么选择&#xff1f; 往左边走&#xff0c;做非共识但正确的事&#xff0c;因为一旦你的真的是对的&#xff0c;你将会得到一切。 非共识思维模型是一个提…

集群和分布式

本文以即时通讯软件&#xff08;IM&#xff09;为例&#xff0c;介绍单机、集群、分布式的区别&#xff0c;以及它们各自的优缺点。 假设现在开发一款IM&#xff0c;刚开始业务比较简单&#xff0c;用户量也较少&#xff0c;我们将服务部署在一台单机服务器上足矣。软件开发过程…

std::regex正则表达式

std::match_results &#xff08;匹配的结果存入其中&#xff09; result[0]是完整的文本&#xff0c;result[1]是第一个分组匹配的数据。如果正则表达式有n个分组&#xff0c;match_results的size也就是n1个 This is a specialized allocator-aware container. It can only …

【FPGA实验4】举重比赛机制

举重比赛有三名裁判&#xff0c;当运动员将杠铃举起后&#xff0c;须有两名或两名以上裁判认可&#xff0c;方可判定试举成功&#xff0c;若用A、B、C分别代表三名裁判的意见输入&#xff0c;同意为1&#xff0c;否定为0;F为裁判结果输出&#xff0c;试举成功时F1&#xff0c;试…

逍遥自在学C语言 | 位运算符~的高级用法

前言 在上一篇文章中&#xff0c;我们介绍了^运算符的高级用法&#xff0c;本篇文章&#xff0c;我们将介绍~ 运算符的一些高级用法。 一、人物简介 第一位闪亮登场&#xff0c;有请今后会一直教我们C语言的老师 —— 自在。 第二位上场的是和我们一起学习的小白程序猿 ——…

Direct3D 12——计算着色器——计算着色器概念

计算着色器虽然是一种可编程的着色器&#xff0c;但Direct3D并没有将它直接归为渲染流水线中的一部分。虽然如此&#xff0c;但位于流水线之外的计算着色器却可以读写GPU资源。从本质上来说&#xff0c;计算着 色器能够使我们访问GPU来实现数据并行算法&#xff0c;而不必渲染出…

5.Spring Cloud (Hoxton.SR8) 实战笔记—项目中细节实现 约束 注意事项、模块难点总结

本文目录如下&#xff1a; 二、项目中细节实现 & 约束 & 注意事项判断字符串是否为空&#xff1f;入参 Num 字段转换为 num 的问题&#xff1f;通过 Java 获取时间 (Date类型) 并插入数据库&#xff1f;神坑: baseMapper.selectById(String str)之坑?AES 实现加密函数…

[Eigen中文文档] 切片和索引

专栏总目录 本文目录 概述基本的切片编译时的大小和步长倒序索引序列自定义索引列表 英文原文(Slicing and Indexing) 本文介绍了如何使用操作运算符operator()索引行和列的子集。该 API 在 Eigen 3.4 中引入。它支持 block API 提供的所有功能。特别是&#xff0c;它支持切片…

查询练习:条件加组筛选

查询 student 表中至少有 2 名男生的 class 。 -- 查看学生表信息 SELECT * FROM student; ---------------------------------------- | no | name | sex | birthday | class | ---------------------------------------- | 101 | 曾华 | 男 | 1977-09-01 | 95…

vue 动态组件

一见如故 // 一般配合<keep-alive>组件&#xff0c;避免反复重新渲染dom <keep-alive><component :is"com"></component> </keep-alive> <script> import Left from ./LeftComponent.vue import Right from ./RightComponent.v…

Ae:自动定向

Ae 菜单&#xff1a;图层/变换/自动定向 Auto-Orient 快捷键&#xff1a;Ctrl Alt O 自动定向 Auto-Orient是 Ae 图层中的一个附加的、隐藏实现&#xff08;不会在时间轴面板上更改属性的值&#xff09;的功能&#xff0c;它可以使得图层自动旋转或改变方向以朝向指定的运动路…

【Linux阅读笔记】LinuxC一站式编程1-程序基本概念与gdb调试

目录 程序基本概念配置开发环境第一个程序C 复习 gdb单步执行与跟踪断点观察点段错误 程序基本概念 使用 ubuntu22.0 作为演示环境&#xff08;vmware 虚拟机搭设&#xff09; 配置开发环境 配置完基础开发环境后&#xff0c;可以直接下载一个 vscode 作为初始 LDE 使用&#x…

机器学习 day03(成本函数,简化后的和一般情况下的成本函数)

1. 成本函数 平方误差成本函数是最通常用于线性回归的成本函数最终&#xff0c;我们要找到一组w和b&#xff0c;让j函数的值最小误差&#xff1a;ŷ - y 2. 简化后的平方误差成本函数&#xff0c;即b 0 当w 1时&#xff0c;f(x) x&#xff0c;J(1) 0 左侧为f(x)函数&am…

Python 图像处理实用指南:11~12

原文&#xff1a;Hands-On Image Processing with Python 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 本文来自【ApacheCN 计算机视觉 译文集】&#xff0c;采用译后编辑&#xff08;MTPE&#xff09;流程来尽可能提升效率。 当别人说你没有底线的时候&#xff0c;…

程序员 如何避免职场危机,打造自己稀缺性

你在职场上如何又脱颖而出 你唯一的办法就干嘛 找一找怎么才能构建我自己的稀缺性 我经过我 多年的工作和有观察的体验 化是什么呢 从技术迭代进步的角度来说 因为软件越来a p i化 个人的能力被解脱了 一定会向上下有延伸 因为当你做一块 以前是一年做下来时间只需要一个月 是一…

(一)Linux:自由、开放、灵活的操作系统内核

目录 一、Linux的发展史 二、linux的开源 三、目前的现状 四、企业应用现状 五、发行的版本 六、安装与使用 七、利用云服务器配置Linux环境 一、Linux的发展史 Linux是一款由林纳斯托瓦兹&#xff08;Linus Torvalds&#xff09;开发的操作系统内核&#xff0c;它的发布…