当前位置: 首页 > 编程日记 > 正文

colly源码学习

colly源码学习

colly是一个golang写的网络爬虫。它使用起来非常顺手。看了一下它的源码,质量也是非常好的。本文就阅读一下它的源码。

使用示例

func main() {c := colly.NewCollector()// Find and visit all linksc.OnHTML("a[href]", func(e *colly.HTMLElement) {e.Request.Visit(e.Attr("href"))})c.OnRequest(func(r *colly.Request) {fmt.Println("Visiting", r.URL)})c.Visit("http://go-colly.org/")
}

从Visit开始说起

首先,要做一个爬虫,我们就需要有一个结构体 Collector, 所有的逻辑都是围绕这个Collector来进行的。

这个Collector在“爬取”一个URL的时候,我们使用的是Collector.Visit方法。这个Visit方法具体有几个步骤:

  • 组装Request
  • 获取Response
  • Response解析HTML/XML
  • 结束页面抓取
  • 在任何一个步骤都有可能出现错误

colly能让你在每个步骤制定你需要执行的逻辑,而且这个逻辑不一定要是单个,可以是多个。比如你可以在Response获取完成,解析为HTML之后使用OnHtml增加逻辑。这个也是我们最常使用的函数。它的实现原理如下:

type HTMLCallback func(*HTMLElement)type htmlCallbackContainer struct {Selector stringFunction HTMLCallback
}type Collector struct {...htmlCallbacks     []*htmlCallbackContainer  // 这个htmlCallbacks就是用户注册的HTML回调逻辑地址...
}// 用户使用的注册函数,注册的是一个htmlCallbackContainer,里面包含了DOM选择器,和选择后的回调方法
func (c *Collector) OnHTML(goquerySelector string, f HTMLCallback) {...if c.htmlCallbacks == nil {c.htmlCallbacks = make([]*htmlCallbackContainer, 0, 4)}c.htmlCallbacks = append(c.htmlCallbacks, &htmlCallbackContainer{Selector: goquerySelector,Function: f,})...
}// 系统在获取HTML的DOM之后做的操作,将htmlCallbacks拆解出来一个个调用函数
func (c *Collector) handleOnHTML(resp *Response) error {...doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(resp.Body))...for _, cc := range c.htmlCallbacks {i := 0doc.Find(cc.Selector).Each(func(_ int, s *goquery.Selection) {for _, n := range s.Nodes {e := NewHTMLElementFromSelectionNode(resp, s, n, i)...cc.Function(e)}})}return nil
}// 这个是Visit的主流程,在合适的地方增加handleOnHTML的逻辑。
func (c *Collector) fetch(u, method string, depth int, requestData io.Reader, ctx *Context, hdr http.Header, req *http.Request) error {...err = c.handleOnHTML(response)...return err
}

整体这个代码的模式我觉得是很巧妙的,简要来说就是在结构体中存储回调函数,回调函数的注册用OnXXX开放出去,内部在合适的地方进行回调函数的嵌套执行。

这个代码模式可以完全记住,适合的场景是有注入逻辑的需求,可以增加类库的扩展性。

比如我们设计一个ORM,想在Save或者Update的时候可以注入一些逻辑,使用这个代码模式大致就是这样逻辑:


// 这种模型适合流式,然后每个步骤进行设计
type SaveCallback func(*Resource)
type UpdateCallback func(string, *Resource)type UpdateCallbackContainer struct {Id stringFunction UpdateCallback
}type Resource struct {Id stringsaveCallbacks []SaveCallbackupdateCallbacks []*UpdateCallbackContainer
}func (r *Resource) OnSave(f SaveCallback) {if r.saveCallbacks == nil {r.saveCallbacks = make([]SaveCallback, 0, 4)}r.saveCallbacks = append(r.saveCallbacks, f)
}func (r *Resource) Save() {// Do Somethingif r.saveCallbacks != nil {for _, f := range r.saveCallbacks {f(r)}}
}func (r *Resource) OnUpdate(id string, f UpdateCallback) {if r.updateCallbacks == nil {r.updateCallbacks = make([]*UpdateCallbackContainer, 0, 4)}r.updateCallbacks = append(r.updateCallbacks, &UpdateCallbackContainer{ id, f})
}func (r *Resource) Update() {// Do somethingid := r.Idif r.updateCallbacks != nil {for _, c := range r.updateCallbacks {c.Function(id, r)}}
}

Collector的组件模型

colly的Collector的创建也是很有意思的,我们可以看看它的New方法

func NewCollector(options ...func(*Collector)) *Collector {c := &Collector{}c.Init()for _, f := range options {f(c)}...return c
}func UserAgent(ua string) func(*Collector) {return func(c *Collector) {c.UserAgent = ua}
}func main() {c := NewCollector(colly.UserAgent("Chrome"))
}

参数是一个返回函数func(*Collector)的可变数组。然后它的组件就可以以参数的形式在New函数中进行定义了。

这个设计模式很适合的是组件化的需求场景,如果一个后台有不同组件,我按需加载这些组件,基本上可以参照这种逻辑:

type Admin struct {SideBar string
}func NewAdmin(options ...func(*Admin)) *Admin {ad := &Admin{}for _, f := range options {f(ad)}return ad
}func SideBar(sidebar string) func(*Admin) {return func(admin *Admin) {admin.SideBar = sidebar}
}

Collector的Debugger逻辑

创建完成Collector,但是在各种地方是需要进行“调试”的,这里的调试colly设计为可以是日志记录,也可以是开启一个web进行实时显示。

这个是怎么做到的呢?也是非常巧妙的使用了事件模型。

基本上核心代码如下:

package adminimport ("io""log"
)type Event struct {Type stringRequestID intMessage string
}type Debugger interface {Init() errorEvent(*Event)
}type LogDebugger struct {Output io.Writerlogger *log.Logger
}func (l *LogDebugger) Init() error {l.logger = log.New(l.Output, "", 1)return nil
}func (l *LogDebugger) Event(e *Event) {l.logger.Printf("[%6d - %s] %q\n", e.RequestID, e.Type, e.Message)
}func createEvent( requestID, collectorID uint32) *debug.Event {return &debug.Event{RequestID:   requestID,Type:        eventType,}
}c.debugger.Event(createEvent("request", r.ID, c.ID, map[string]string{"url": r.URL.String(),
}))

设计了一个Debugger的接口,里面的Init其实可以根据需要是否存在,最核心的是一个Event函数,它接收一个Event结构指针,所有调试信息相关的调试类型,调试请求ID,调试信息等都可以存在这个Event里面。

在需要记录的地方,创建一个Event事件,并且通过debugger进行输出到调试器中。

colly的debugger还有个惊喜,它支持web方式的查看,我们查看里面的debug/webdebugger.go


type WebDebugger struct {Address         stringinitialized     boolCurrentRequests map[uint32]requestInfoRequestLog      []requestInfo
}type requestInfo struct {URL            stringStarted        time.TimeDuration       time.DurationResponseStatus stringID             uint32CollectorID    uint32
}func (w *WebDebugger) Init() error {...if w.Address == "" {w.Address = "127.0.0.1:7676"}w.RequestLog = make([]requestInfo, 0)w.CurrentRequests = make(map[uint32]requestInfo)http.HandleFunc("/", w.indexHandler)http.HandleFunc("/status", w.statusHandler)log.Println("Starting debug webserver on", w.Address)go http.ListenAndServe(w.Address, nil)return nil
}func (w *WebDebugger) Event(e *Event) {switch e.Type {case "request":w.CurrentRequests[e.RequestID] = requestInfo{URL:         e.Values["url"],Started:     time.Now(),ID:          e.RequestID,CollectorID: e.CollectorID,}case "response", "error":r := w.CurrentRequests[e.RequestID]r.Duration = time.Since(r.Started)r.ResponseStatus = e.Values["status"]w.RequestLog = append(w.RequestLog, r)delete(w.CurrentRequests, e.RequestID)}
}

看到没,重点是通过Init函数把http server启动起来,然后通过Event收集当前信息,然后通过某个路由handler再展示在web上。

这个设计比其他的各种Logger的设计感觉又优秀了一点。

总结

看下来colly代码,基本上代码还是非常清晰,不复杂的。我觉得上面三个地方看明白了,基本上这个爬虫框架的架构设计就很清晰了,剩下的是具体的代码实现的部分,可以慢慢看。

colly的整个框架给我的感觉是很干练,没有什么废话和过度设计,该定义为结构的地方就定义为结构了,比如Colletor,这里它并没有设计为很复杂的Collector接口啥的。但是在该定义为接口的地方,比如Debugger,就定义为了接口。而且colly也充分考虑了使用者的扩展性。几个OnXXX流程和回调函数的设计也非常合理。

原文地址https://www.cnblogs.com/yjf512/p/10441678.html

相关文章:

可惜了,你们只看到“双马会”大型尬聊

作者 | 夕颜出品 | AI科技大本营(ID:rgznai100)导读:2019 年 8 月 29 日,世界人工智能大会(WAIC)在上海正式拉开帷幕。开幕式上,最让人瞩目的莫过于阿里巴巴前 CEO 马云与特斯拉 CEO Elon Musk …

Java 过滤特殊字符的 正则表达式

Java正则表达式学习: 因为正则表达式是一个很庞杂的体系,此例仅举些入门的概念,更多的请参阅相关书籍及自行摸索。 \\ 反斜杠 \t 间隔 (\u0009) \n 换行 (\u000A) \r 回车 (\u000D) \d 数字 等价于[0-9] \D 非数字 等价于[^0-9] \s 空…

C++11中unique_ptr的使用

在C中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,…

从这篇YouTube论文,剖析强化学习在工业级场景推荐系统中的应用

作者 | 吴海波转载自知乎用户吴海波【导读】本文作者根据两篇工业界背景的论文解答了 RL 在推荐场景需要解决的问题与困难,以及入门需要学习得相关知识点。2 个月前,业界开始流传 youtube 成功将 RL 应用在了推荐场景,并且演讲者在视频中说是…

java中两个Integer类型的值相比较的问题

转载自: https://www.cnblogs.com/xh0102/p/5280032.html 两个Integer类型整数进行比较时,一定要先用intValue()方法将其转换为int数之后再进行比较,因为直接使用比较两个Integer会出现问题。 总结: 当给Integer直接赋值时&#x…

C#共享内存实例 附源码

原文 C#共享内存实例 附源码 网上有C#共享内存类,不过功能太简单了,并且写内存每次都从开头写。故对此进行了改进,并做了个小例子,供需要的人参考。 主要改进点: 通过利用共享内存的一部分空间(以下称为“数据信息区”…

C++11中weak_ptr的使用

在C中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,…

经典不过时,回顾DeepCompression神经网络压缩

作者 | 薰风初入弦转载自知乎导读:本文作者为我们详细讲述了 ICLR 2016 的最佳论文 Deep Compression 中介绍的神经网络压缩方法。神经网络压缩一直是一个重要的研究方向,而目前业界最认可的压缩方法莫过于 ICLR 2016 的最佳论文 Deep Compression&#…

区块链技术特点之去中心化特性

想知道更多关于区块链技术知识,请百度【链客区块链技术问答社区】 链客,有问必答!! 由于区块链技术去中心化的特性,其在我们生活中的很多重要领域(如金融、管理)等方面具有重要的意义。例如&…

Android APK反编译

转自:http://blog.csdn.net/ithomer/article/details/6727581 一、Apk反编译得到Java源代码 下载上述反编译工具包,打开apk2java目录下的dex2jar-0.0.9.9文件夹,内含apk反编译成java源码工具,以及源码查看工具。 apk反编译工具dex…

Java泛型进阶 - 如何取出泛型类型参数

在JDK5引入了泛型特性之后,她迅速地成为Java编程中不可或缺的元素。然而,就跟泛型乍一看似乎非常容易一样,许多开发者也非常容易就迷失在这项特性里。多数Java开发者都会注意到Java编译器的类型擦除实现方式,Type Erasure会导致关…

C++11中override的使用

override是C11中的一个继承控制关键字。override确保在派生类中声明的重载函数跟基类的虚函数有相同的声明。 override明确地表示一个函数是对基类中一个虚函数的重载。更重要的是,它会检查基类虚函数和派生类中重载函数的签名不匹配问题。如果签名不匹配&#xff…

平头哥发布一站式芯片设计平台“无剑”,芯片设计成本降低50%

导读:8 月 29 日,在上海举行的世界人工智能大会上,阿里巴巴旗下半导体公司平头哥发布 SoC 芯片平台“无剑”。无剑是面向 AIoT 时代的一站式芯片设计平台,提供集芯片架构、基础软件、算法与开发工具于一体的整体解决方案&#xff…

Windows XP下,JDK环境变量配置

2019独角兽企业重金招聘Python工程师标准>>> 1.安装JDK,安装过程中可以自定义安装目录等信息,例如我们选择安装目录为D:\java\jdk1.5.0_08; 2.安装完成后,右击“我的电脑”,点击“属性”; 3.选择…

Markdown语法简介

Markdown是一种方便记忆、书写的纯文本标记语言,用户可以使用这些标记符号以最小的输入代价生成极富表现力的文档。它目标是实现易读易写。Markdown的语法全由一些符号所组成。Markdown语法的目标是成为一种适用于网络的书写语言。 Markdown优点:纯文本…

吴恩达:AI未来将呈现四大发展趋势

作者 | 夕颜出品 | AI科技大本营(ID:rgznai100)导读:8 月 30 日,世界人工智能大会精彩继续。在今天的全球工业智能峰会上,Landing.AI 创始人及首席执行官吴恩达来到现场,做了题为《人工智能是新电力》的演讲…

嵌入式课程安排 嵌入式培训课程大纲参考

嵌入式是一门综合性的学科,现在学习嵌入式开发不是单纯局限于单片机或者Linux,嵌入式课程中包含着非常多的内容。以粤嵌嵌入式课程进行参考,看看我们要学习嵌入式的话,要掌握哪些必备的技能。嵌入式课程安排包含:1、入…

Linux网站架构系列之Apache----进阶篇

本篇博文为Linux网站架构系列之apache的第二篇,我将带大家一起学习apache的编译参数,目录结构和配置文件等方面的知识,实现对apache服务的进一步掌握,并使之能更好的应用到生产实战中去。一、编译参数在上篇的apache部署中&#x…

仅用10天设计的JavaScript,凭什么成为程序员最受欢迎的编程语言?

导语:在这个世纪之交诞生的 JavaScript,没人想到会发展为当今世界上最流行的语言之一。它不够成熟,不够严肃,甚至连名字都是模仿的 Java。那么,JavaScript 的成功是依靠运气和完美时机的侥幸吗?其实不然——…

C++11中= delete;的使用

C11中,对于deleted函数,编译器会对其禁用,从而避免某些非法的函数调用或者类型转换,从而提高代码的安全性。 对于 C 的类,如果程序员没有为其定义特殊成员函数,那么在需要用到某个特殊成员函数的时候&…

vue 使用scss

使用vue-cli模板创建的项目中,使用scss步骤 1. cmd命令: cnpm install sass-loader --save-devcnpm install node-sass --sava-dev2.查看package.json文件中是否已自动添加以下信息 3. 转载于:https://www.cnblogs.com/duanzhenzhen/p/10453495.html

EBS form日历可选范围设置(calendar.setup )介绍

Calendar是Template提供给我们的standard object.可以使我们方便的为日期型字段提供日期的选择列表.form中设置日历方法:1. 为日期型字段指定LOV(ENABLE_LIST_LAMP)2. 在字段的KEY–LISTVAL事件中编写代码:Calendar.showCalendar Package包含如下几个Procedure:1. Calendar.sho…

人工智能对地球环境科学的推进

一项德国耶拿[1]和汉堡[2]科学家在《自然》杂志发起的研究表明,人工智能可以有效地推进我们对于地球气候系统的理解。特别是在当前深度学习的潜力还未被完全开发的情况下。在人工智能的帮助下一些复杂的动态环境,如飓风,森林火灾,…

从概念到应用,终于有人把数据挖掘讲明白了

作者:陈封能(Pang-Ning Tan)、迈克尔斯坦巴赫(Michael Steinbach)等来源 | 大数据(ID: hzdashuju)【导语】数据采集和存储技术的迅速发展,加之数据生成与传播的便捷性&am…

C++11中default的使用

在C11中,对于defaulted函数,编译器会为其自动生成默认的函数定义体,从而获得更高的代码执行效率,也可免除程序员手动定义该函数的工作量。 C的类有四类特殊成员函数,它们分别是:默认构造函数、析构函数、拷…

Android开发:setAlpha()方法和常用RGB颜色表----颜色, r g b分量数值(int), 16进制表示 一一对应...

杂家前文Android颜色对照表只有颜色和十六进制,有时候需要设置r g b分量的int值,如paint.setARGB(255, 127, 255, 212);就需要自己计算下分量的各个值。这里提供一个带有r g b分量的int型的颜色表。注意paint.setAlpha()及paint.setARGB(&…

【redis】c/c++操作redis(对于hiredis的封装)

前言 最近一直在学习redis&#xff0c;通过c/cpp来执行redis命令&#xff0c;使用的是hiredis客户端来实现的。 先简单贴一下代码 头文件 #include <vector> #include <string> #include <hiredis/hiredis.h> typedef enum en_redisResultType {redis_reply_…

OpenCV代码提取:transpose函数的实现

OpenCV中的transpose函数实现图像转置&#xff0c;公式为&#xff1a;目前fbc_cv库中也实现了transpose函数&#xff0c;支持多通道&#xff0c;uchar和float两种数据类型&#xff0c;经测试&#xff0c;与OpenCV3.1结果完全一致。实现代码transpose.hpp&#xff1a;// fbc_cv …

只给测试集不给训练集,要怎么做自己的物体检测器?

9 月5 日&#xff0c;下周四&#xff0c;大家期待已久的由《动手学深度学习》作者&#xff0c;亚马逊首席科学家亲自带领的「深度学习实训营」就要在北京开营了。今天&#xff0c;李沐已经把这次深度学习实训营白天的教学内容和代码上传到 Gituhub 和 D2L.ai 网站了&#xff0c…

MYSQL忘记登录密码

1、关闭Mysql&#xff1a; 如果 MySQL 正在运行&#xff0c;首先杀之 killall -TERM mysqld 2、另外的方法启动 MySQL &#xff1a;bin/safe_mysqld --skip-grant-tables 3、可以不需要密码就进入 MySQL 了。 然后就是 >use mysql>update user set passwordpassword(&qu…