colly源码学习
colly源码学习
colly是一个golang写的网络爬虫。它使用起来非常顺手。看了一下它的源码,质量也是非常好的。本文就阅读一下它的源码。
使用示例
func main() {
c := colly.NewCollector()
// Find and visit all links
c.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 string
Function 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 := 0
doc.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 string
Function UpdateCallback
}
type Resource struct {
Id string
saveCallbacks []SaveCallback
updateCallbacks []*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 Something
if 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 something
id := r.Id
if 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 admin
import (
"io"
"log"
)
type Event struct {
Type string
RequestID int
Message string
}
type Debugger interface {
Init() error
Event(*Event)
}
type LogDebugger struct {
Output io.Writer
logger *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 string
initialized bool
CurrentRequests map[uint32]requestInfo
RequestLog []requestInfo
}
type requestInfo struct {
URL string
Started time.Time
Duration time.Duration
ResponseStatus string
ID uint32
CollectorID 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流程和回调函数的设计也非常合理。
colly源码学习的更多相关文章
- Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结
2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...
- jQuery源码学习感想
还记得去年(2015)九月份的时候,作为一个大四的学生去参加美团霸面,结果被美团技术总监教育了一番,那次问了我很多jQuery源码的知识点,以前虽然喜欢研究框架,但水平还不足够来研究jQuery源码, ...
- MVC系列——MVC源码学习:打造自己的MVC框架(四:了解神奇的视图引擎)
前言:通过之前的三篇介绍,我们基本上完成了从请求发出到路由匹配.再到控制器的激活,再到Action的执行这些个过程.今天还是趁热打铁,将我们的View也来完善下,也让整个系列相对完整,博主不希望烂尾. ...
- MVC系列——MVC源码学习:打造自己的MVC框架(三:自定义路由规则)
前言:上篇介绍了下自己的MVC框架前两个版本,经过两天的整理,版本三基本已经完成,今天还是发出来供大家参考和学习.虽然微软的Routing功能已经非常强大,完全没有必要再“重复造轮子”了,但博主还是觉 ...
- MVC系列——MVC源码学习:打造自己的MVC框架(二:附源码)
前言:上篇介绍了下 MVC5 的核心原理,整篇文章比较偏理论,所以相对比较枯燥.今天就来根据上篇的理论一步一步进行实践,通过自己写的一个简易MVC框架逐步理解,相信通过这一篇的实践,你会对MVC有一个 ...
- MVC系列——MVC源码学习:打造自己的MVC框架(一:核心原理)
前言:最近一段时间在学习MVC源码,说实话,研读源码真是一个痛苦的过程,好多晦涩的语法搞得人晕晕乎乎.这两天算是理解了一小部分,这里先记录下来,也给需要的园友一个参考,奈何博主技术有限,如有理解不妥之 ...
- 我的angularjs源码学习之旅2——依赖注入
依赖注入起源于实现控制反转的典型框架Spring框架,用来削减计算机程序的耦合问题.简单来说,在定义方法的时候,方法所依赖的对象就被隐性的注入到该方法中,在方法中可以直接使用,而不需要在执行该函数的时 ...
- ddms(基于 Express 的表单管理系统)源码学习
ddms是基于express的一个表单管理系统,今天抽时间看了下它的代码,其实算不上源码学习,只是对它其中一些小的开发技巧做一些记录,希望以后在项目开发中能够实践下. 数据层封装 模块只对外暴露mod ...
- leveldb源码学习系列
楼主从2014年7月份开始学习<>,由于书籍比较抽象,为了加深思考,同时开始了Google leveldb的源码学习,主要是想学习leveldb的设计思想和Google的C++编程规范.目 ...
随机推荐
- 修改LINUX的时区。
新装的机器(redhat7)有几台时区不对: 百度了之后找到了以下解决方法输入 tz 依次选择Asia China east China Yes 1 然后 export TZ 新开对话发现 ...
- bzoj 4173 打表???
没有任何思路,打表发现ans=phi(n)*phi(m)*n*m %%% popoqqq Orz 然而并没有看懂-- #include<cstdio> #include<cstrin ...
- 种树 BZOJ2151 模拟费用流
分析: 我们如果选择点i,那么我们不能选择i-1和i+1,如果没有这个限制,直接贪心就可行,而加上这个限制,我们考虑同样贪心,每次选择i后,将点i-1,i+1从双向链表中删除,并且将-a[i]+a[i ...
- SQL 如何在自增列插入指定数据
SQL Server 中数据表往往会设置自增列,常见的比如说 首列的ID列. 往数据表插入新数据的时候,自增列是跳过的,无需插入即会按照设置的自增规则进行列增长.那么,如果我们想往自增列插入我们指定 ...
- Android性能优化-内存泄漏的8个Case
1为什么要做性能优化? 手机性能越来越好,不用纠结这些细微的性能? Android每一个应用都是运行的独立的Dalivk虚拟机,根据不同的手机分配的可用内存可能只有(32M.64M等),所谓的4GB. ...
- surging 微服务引擎 1.0 正式发布
surging 是一个分布式微服务引擎,提供高性能RPC远程服务调用,服务引擎支持http.TCP.WS.Mqtt协议,采用Zookeeper.Consul作为surging服务的注册中心,集成了哈希 ...
- 已实现乐观锁功能,FreeSql.DbContext 准备起航
上回说到 FreeSql.DbContext 的规则,以及演示它的执行过程,可惜当时还不支持"乐观锁",对于更新数据来讲并不安全. FreeSql 核心库 v0.3.27 已提供乐 ...
- 两个月的Java实习结束,继续努力
前言 只有光头才能变强 2018年8月30日,今天我辞职了.在6月25号入职,到现在也有两个月时间了. 感受: 第一天是期待的:第一次将项目拉到本地上看的时候,代码很多,有非常多的模块,模块下又有da ...
- Windows核心编程第一章.错误处理
Windows核心编程第一章,错误处理. 一丶错误处理 1.核心编程学习总结 不管是做逆向,开始做开发.在Windows下.你都需要看一下核心编程这本书.这本书确实写得很好.所以自己在学习这本书的同时 ...
- DS控件库 Win7链接列表框的仿Windows开始菜单样式
Win7链接列表框是依照Windows7的开始菜单开发的,同时进行了属性和功能的扩展. 效果图 项属性 控件属性 控件主要事件 点击项(Sender As Win7链接列表框, Itm As 链接项, ...