Go 爬虫之 colly 从入门到不放弃指南

2022年3月31日 318点热度 0人点赞 0条评论

图片

最近发现知乎上感兴趣的问题越来越少,于是准备聚合下其他平台技术问答,比如 segmentfault、stackoverflow 等。

要完成这个工作,肯定是离不开爬虫的。我就顺便抽时间研究了 Go 的一款爬虫框架 colly。

概要介绍

colly 是 Go 实现的比较有名的一款爬虫框架,而且 Go 在高并发和分布式场景的优势也正是爬虫技术所需要的。它的主要特点是轻量、快速,设计非常优雅,并且分布式的支持也非常简单,易于扩展。

如何学习

爬虫最有名的框架应该就是 Python 的 scrapy,很多人最早接触的爬虫框架就是它,我也不例外。它的文档非常齐全,扩展组件也很丰富。当我们要设计一款爬虫框架时,常会参考它的设计。之前看到一些文章介绍 Go 中也有类似 scrapy 的实现。

相比而言,colly 的学习资料就少的可怜了。刚看到它的时候,我总会情不自禁想借鉴我的 scrapy 使用经验,但结果发现这种生搬硬套并不可行。

到此,我们自然地想到去找些文章阅读,但结果是 colly 相关文章确实有点少,能找到的基本都是官方提供的,而且看起来似乎不是那么完善。没办法,慢慢啃吧!官方的学习资料通常都会有三处,分别是文档、案例和源码。

今天,暂时先从官方文档角度吧!正文开始。

官方文档

官方文档[1]介绍着重使用方法,如果是有爬虫经验的朋友,扫完一遍文档很快。我花了点时间将官网文档的按自己的思路整理了一版。

主体内容不多,涉及安装[2]快速开始[3]如何配置[4]调试[5]分布式爬虫[6]存储[7]运用多收集器[8]配置优化[9]扩展[10]

其中的每篇文档都很短小,甚至是少的基本都不用翻页滚动。

如何安装[11]

colly 的安装和其他的 Go 库安装一样简单。如下:

go get -u github.com/gocolly/colly

一行命令搞定。So easy!

快速开始[12]

我们来通过一个 hello word 案例快速体验下 colly 的使用。步骤如下:

第一步,导入 colly。

import "github.com/gocolly/colly"

第二步,创建 collector。

c := colly.NewCollector()

第三步,事件监听,通过 callback 执行事件处理。

// Find and visit all links
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Attr("href")
// Print link
fmt.Printf("Link found: %q -> %s\n", e.Text, link)
// Visit link found on page
// Only those links are visited which are in AllowedDomains
c.Visit(e.Request.AbsoluteURL(link))
})

c.OnRequest(func(r *colly.Request) {
fmt.Println("Visiting", r.URL)
})

我们顺便列举一下 colly 支持的事件类型,如下:

  • OnRequest 请求执行之前调用

  • OnResponse 响应返回之后调用

  • OnHTML 监听执行 selector

  • OnXML 监听执行 selector

  • OnHTMLDetach,取消监听,参数为 selector 字符串

  • OnXMLDetach,取消监听,参数为 selector 字符串

  • OnScraped,完成抓取后执行,完成所有工作后执行

  • OnError,错误回调

最后一步,c.Visit() 正式启动网页访问。

c.Visit("http://go-colly.org/")

案例的完整代码在 colly 源码的 _example 目录下 basic[13] 中提供。

如何配置[14]

colly 是一款配置灵活的框架,提供了大量的可供开发人员配置的选项。默认情况下,每个选项都提供了较优的默认值。

如下是采用默认创建的 collector。

c := colly.NewCollector()

配置创建的 collector,比如设置 useragent 和允许重复访问。代码如下:

c2 := colly.NewCollector(
colly.UserAgent("xy"),
colly.AllowURLRevisit(),
)

我们也可以创建后再改变配置。

c2 := colly.NewCollector()
c2.UserAgent = "xy"
c2.AllowURLRevisit = true

collector 的配置可以在爬虫执行到任何阶段改变。一个经典的例子,通过随机改变 user-agent,可以帮助我们实现简单的反爬。

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func RandomString() string {
b := make([]byte, rand.Intn(10)+10)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}

c := colly.NewCollector()

c.OnRequest(func(r *colly.Request) {
r.Headers.Set("User-Agent", RandomString())
})

前面说过,collector 默认已经为我们选择了较优的配置,其实它们也可以通过环境变量改变。这样,我们就可以不用为了改变配置,每次都得重新编译了。环境变量配置是在 collector 初始化时生效,正式启动后,配置是可以被覆盖的。

支持的配置项,如下:

ALLOWED_DOMAINS (字符串切片),允许的域名,比如 []string{"segmentfault.com", "zhihu.com"}
CACHE_DIR (string) 缓存目录
DETECT_CHARSET (y/n) 是否检测响应编码
DISABLE_COOKIES (y/n) 禁止 cookies
DISALLOWED_DOMAINS (字符串切片),禁止的域名,同 ALLOWED_DOMAINS 类型
IGNORE_ROBOTSTXT (y/n) 是否忽略 ROBOTS 协议
MAX_BODY_SIZE (int) 响应最大
MAX_DEPTH (int - 0 means infinite) 访问深度
PARSE_HTTP_ERROR_RESPONSE (y/n) 解析 HTTP 响应错误
USER_AGENT (string)

它们都是些非常容易理解的选项。

我们再来看看 HTTP 的配置,都是些常用的配置,比如代理、各种超时时间等。

c := colly.NewCollector()
c.WithTransport(&http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second, // 超时时间
KeepAlive: 30 * time.Second, // keepAlive 超时时间
DualStack: true,
}).DialContext,
MaxIdleConns: 100, // 最大空闲连接数
IdleConnTimeout: 90 * time.Second, // 空闲连接超时
TLSHandshakeTimeout: 10 * time.Second, // TLS 握手超时
ExpectContinueTimeout: 1 * time.Second,
}

调试[15]

在用 scrapy 的时候,它提供了非常好用的 shell 帮助我们非常方便地实现 debug。但非常可惜 colly 中并没有类似功能,这里的 debugger 主要是指运行时的信息收集。

debugger 是一个接口,我们只要实现它其中的两个方法,就可完成运行时信息的收集。

type Debugger interface {
// Init initializes the backend
Init() error
// Event receives a new collector event.
Event(e *Event)
}

源码中有个典型的案例,LogDebugger[16]。我们只需提供相应的 io.Writer 类型变量,具体如何使用呢?

一个案例,如下:

package main

import (
"log"
"os"

"github.com/gocolly/colly"
"github.com/gocolly/colly/debug"
)

func main() {
writer, err := os.OpenFile("collector.log", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
panic(err)
}

c := colly.NewCollector(colly.Debugger(&debug.LogDebugger{Output: writer}), colly.MaxDepth(2))
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
if err := e.Request.Visit(e.Attr("href")); err != nil {
log.Printf("visit err: %v", err)
}
})

if err := c.Visit("http://go-colly.org/"); err != nil {
panic(err)
}
}

运行完成,打开 collector.log 即可查看输出内容。

分布式[17]

分布式爬虫,可以从几个层面考虑,分别是代理层面、执行层面和存储层面。

代理层面

通过设置代理池,我们可以将下载任务分配给不同节点执行,有助于提供爬虫的网页下载速度。同时,这样还能有效降低因爬取速度太快而导致IP 被禁的可能性。

colly 实现代理 IP 的代码如下:

package main

import (
"github.com/gocolly/colly"
"github.com/gocolly/colly/proxy"
)

func main() {
c := colly.NewCollector()

if p, err := proxy.RoundRobinProxySwitcher(
"socks5://127.0.0.1:1337",
"socks5://127.0.0.1:1338",
"http://127.0.0.1:8080",
); err == nil {
c.SetProxyFunc(p)
}
// ...
}

proxy.RoundRobinProxySwitcher 是 colly 内置的通过轮询方式实现代理切换的函数。当然,我们也可以完全自定义。

比如,一个代理随机切换的案例,如下:

var proxies []*url.URL = []*url.URL{
&url.URL{Host: "127.0.0.1:8080"},
&url.URL{Host: "127.0.0.1:8081"},
}

func randomProxySwitcher(_ *http.Request) (*url.URL, error) {
return proxies[random.Intn(len(proxies))], nil
}

// ...
c.SetProxyFunc(randomProxySwitcher)

不过需要注意,此时的爬虫仍然是中心化的,任务只在一个节点上执行。

执行层面

这种方式通过将任务分配给不同的节点执行,实现真正意义的分布式。

如果实现分布式执行,首先需要面对一个问题,如何将任务分配给不同的节点,实现不同任务节点之间的协同工作呢?

首先,我们选择合适的通信方案。常见的通信协议有 HTTP、TCP,一种无状态的文本协议、一个是面向连接的协议。除此之外,还可选择的有种类丰富的 RPC 协议,比如 Jsonrpc、facebook 的 thrift、google 的 grpc 等。

文档提供了一个 HTTP 服务示例代码,负责接收请求与任务执行。如下:

package main

import (
"encoding/json"
"log"
"net/http"

"github.com/gocolly/colly"
)

type pageInfo struct {
StatusCode int
Links map[string]int
}

func handler(w http.ResponseWriter, r *http.Request) {
URL := r.URL.Query().Get("url")
if URL == "" {
log.Println("missing URL argument")
return
}
log.Println("visiting", URL)

c := colly.NewCollector()

p := &pageInfo{Links: make(map[string]int)}

// count links
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Request.AbsoluteURL(e.Attr("href"))
if link != "" {
p.Links[link]++
}
})

// extract status code
c.OnResponse(func(r *colly.Response) {
log.Println("response received", r.StatusCode)
p.StatusCode = r.StatusCode
})
c.OnError(func(r *colly.Response, err error) {
log.Println("error:", r.StatusCode, err)
p.StatusCode = r.StatusCode
})

c.Visit(URL)

// dump results
b, err := json.Marshal(p)
if err != nil {
log.Println("failed to serialize response:", err)
return
}
w.Header().Add("Content-Type", "application/json")
w.Write(b)
}

func main() {
// example usage: curl -s 'http://127.0.0.1:7171/?url=http://go-colly.org/'
addr := ":7171"

http.HandleFunc("/", handler)

log.Println("listening on", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}

这里并没有提供调度器的代码,不过实现不算复杂。任务完成后,服务会将相应的链接返回给调度器,调度器负责将新的任务发送给工作节点继续执行。

如果需要根据节点负载情况决定任务执行节点,还需要服务提供监控 API 获取节点性能数据帮助调度器决策。

存储层面

我们已经通过将任务分配到不同节点执行实现了分布式。但部分数据,比如 cookies、访问的 url 记录等,在节点之间需要共享。默认情况下,这些数据是保存内存中的,只能是每个 collector 独享一份数据。

我们可以通过将数据保存至 redis、mongo 等存储中,实现节点间的数据共享。colly 支持在任何存储间切换,只要相应存储实现 colly/storage.Storage[18] 接口中的方法。

其实,colly 已经内置了部分 storage 的实现,查看 storage[19]。下一节也会谈到这个话题。

存储[20]

前面刚提过这个话题,我们具体看看 colly 已经支持的 storage 有哪些吧。

InMemoryStorage[21],即内存,colly 的默认存储,我们可以通过 collector.SetStorage() 替换。

RedisStorage[22],或许是因为 redis 在分布式场景下使用更多,官方提供了使用案例[23]

其他还有 Sqlite3Storage[24]MongoStorage[25]

多收集器[26]

我们前面演示的爬虫都是比较简单的,处理逻辑都很类似。如果是一个复杂的爬虫,我们可以通过创建不同的 collector 负责不同任务的处理。

如何理解这段话呢?举个例子吧。

如果大家写过一段时间爬虫,肯定遇到过父子页面抓取的问题,通常父页面的处理逻辑与子页面是不同的,并且通常父子页面间还有数据共享的需求。用过 scrapy 应该知道,scrapy 通过在 request 绑定回调函数实现不同页面的逻辑处理,而数据共享是通过在 request 上绑定数据实现将父页面数据传递给子页面。

研究之后,我们发现 scrapy 的这种方式 colly 并不支持。那该怎么做?这就是我们要解决的问题。

对于不同页面的处理逻辑,我们可以定义创建多个收集器,即 collector,不同 collector 负责处理不同的页面逻辑。

c := colly.NewCollector(
colly.UserAgent("myUserAgent"),
colly.AllowedDomains("foo.com", "bar.com"),
)
// Custom User-Agent and allowed domains are cloned to c2
c2 := c.Clone()

通常情况下,父子页面的 collector 是相同的。上面的示例中,子页面的 collector c2 通过 clone,将父级 collector 的配置也都复制了下来。

而父子页面之间的数据传递,可以通过 Context 实现在不同 collector 之间传递。注意这个 Context 只是 colly 实现的数据共享的结构,并非 Go 标准库中的 Context。

c.OnResponse(func(r *colly.Response) {
r.Ctx.Put("Custom-header", r.Headers.Get("Custom-Header"))
c2.Request("GET", "https://foo.com/", nil, r.Ctx, nil)
})

如此一来,我们在子页面中就可以通过 r.Ctx 获取到父级传入的数据了。关于这个场景,我们可以查看官方提供的案例 coursera_courses[27]

配置优化[28]

colly 的默认配置针对是少量站点的优化配置。如果你是针对大量站点的抓取,还需要一些改进。

持久化存储

默认情况下,colly 中的 cookies 和 url 是保存在内存中,我们要换成可持久化的存储。前面介绍过,colly 已经实现一些常用的可持久化的存储组件。

启用异步加快任务执行

colly 默认会阻塞等待请求执行完成,这将会导致等待执行任务数越来越大。我们可以通过设置 collector 的 Async 选项为 true 实现异步处理,从而避免这个问题。如果采用这种方式,记住增加 c.Wait(),否则程序会立刻退出。

禁止或限制 KeepAlive 连接

colly 默认开启 KeepAlive 增加爬虫的抓取速度。但是,这对打开的文件描述符有要求,对于长时间运行的任务,进程非常容易就能达到最大描述符的限制。

禁止 HTTP 的 KeepAlive 的示例代码,如下。

c := colly.NewCollector()
c.WithTransport(&http.Transport{
DisableKeepAlives: true,
})

扩展[29]

colly 提供了一些扩展,主要与爬虫相关的常用功能,如 referer、random_user_agent、url_length_filter 等。源码路径在 colly/extensions/[30] 下。

通过一个示例了解它们的使用方法,如下:

import (
"log"

"github.com/gocolly/colly"
"github.com/gocolly/colly/extensions"
)

func main() {
c := colly.NewCollector()
visited := false

extensions.RandomUserAgent(c)
extensions.Referrer(c)

c.OnResponse(func(r *colly.Response) {
log.Println(string(r.Body))
if !visited {
visited = true
r.Request.Visit("/get?q=2")
}
})

c.Visit("http://httpbin.org/get")
}

只需将 collector 传入扩展函数中即可。这么简单就搞定了啊。

那么,我们能不能自己实现一个扩展呢?

在使用 scrapy 的时候,我们如果要实现一个扩展需要提前了解不少概念,仔细阅读它的文档。但 colly 在文档中压根也并没有相关说明啊。肿么办呢?看样子只能看源码了。

我们打开 referer 插件的源码,如下:

package extensions

import (
"github.com/gocolly/colly"
)

// Referer sets valid Referer HTTP header to requests.
// Warning: this extension works only if you use Request.Visit
// from callbacks instead of Collector.Visit.
func Referer(c *colly.Collector) {
c.OnResponse(func(r *colly.Response) {
r.Ctx.Put("_referer", r.Request.URL.String())
})
c.OnRequest(func(r *colly.Request) {
if ref := r.Ctx.Get("_referer"); ref != "" {
r.Headers.Set("Referer", ref)
}
})
}

在 collector 上增加一些事件回调就实现一个扩展。这么简单的源码,完全不用文档说明就可以实现一个自己的扩展了。当然,如果仔细观察,我们会发现,其实它的思路和 scrapy 是类似的,都是通过扩展 request 和 response 的回调实现,而 colly 之所以如此简洁主要得益于它优雅的设计和 Go 简单的语法。

总结

读完 colly 的官方文档会发现,虽然它的文档简陋无比,但应该介绍的内容基本上都涉及到了。如果有部分未涉及的内容,我也在本文之中做了相关的补充。之前在使用 Go 的 elastic[31] 包时,同样也是文档少的可怜,但简单读下源码,就能立刻明白了该如何去使用它。

或许这就是 Go 的大道至简吧。

最后,如果大家在使用 colly 时遇到什么问题,官方的 example 绝对是最佳实践,建议可以抽时间一读。

对了,看完文章,记得点击下方的卡片。关注我哦~ ???

资料下载

点击下方卡片关注公众号,发送特定关键字获取对应精品资料!

  • 回复「电子书」,获取入门、进阶 Go 语言必看书籍。

  • 回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!

  • 回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。

  • 回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。

  • 回复「后台」,获取后台开发必看 10 本书籍。

参考资料

[1]

官方文档: http://go-colly.org/docs/

[2]

安装: http://go-colly.org/docs/introduction/install/

[3]

快速开始: http://go-colly.org/docs/introduction/start/

[4]

如何配置: http://go-colly.org/docs/introduction/configuration/

[5]

调试: http://go-colly.org/docs/introduction/configuration/

[6]

分布式爬虫: http://go-colly.org/docs/best_practices/distributed/

[7]

存储: http://go-colly.org/docs/best_practices/storage/

[8]

运用多收集器: http://go-colly.org/docs/best_practices/multi_collector/

[9]

配置优化: http://go-colly.org/docs/best_practices/crawling/

[10]

扩展: http://go-colly.org/docs/best_practices/extensions/

[11]

如何安装: http://go-colly.org/docs/introduction/install/

[12]

快速开始: http://go-colly.org/docs/introduction/start/

[13]

basic: https://github.com/gocolly/colly/blob/master/_examples/basic/basic.go

[14]

如何配置: http://go-colly.org/docs/introduction/configuration/

[15]

调试: http://go-colly.org/docs/best_practices/debugging/

[16]

LogDebugger: https://github.com/gocolly/colly/blob/master/debug/logdebugger.go

[17]

分布式: http://go-colly.org/docs/best_practices/distributed/

[18]

colly/storage.Storage: https://godoc.org/github.com/gocolly/colly/storage#Storage

[19]

storage: http://go-colly.org/docs/best_practices/storage/

[20]

存储: http://go-colly.org/docs/best_practices/storage/

[21]

InMemoryStorage: https://github.com/gocolly/colly/blob/master/storage/storage.go

[22]

RedisStorage: https://github.com/gocolly/redisstorage

[23]

使用案例: http://go-colly.org/docs/examples/redis_backend/

[24]

Sqlite3Storage: https://github.com/velebak/colly-sqlite3-storage

[25]

MongoStorage: https://github.com/zolamk/colly-mongo-storage

[26]

多收集器: http://go-colly.org/docs/best_practices/multi_collector/

[27]

coursera_courses: http://go-colly.org/docs/examples/coursera_courses/

[28]

配置优化: http://go-colly.org/docs/best_practices/crawling/

[29]

扩展: http://go-colly.org/docs/best_practices/extensions/

[30]

colly/extensions/: https://github.com/gocolly/colly/tree/master/extensions

[31]

elastic: https://github.com/olivere/elastic

68020Go 爬虫之 colly 从入门到不放弃指南

这个人很懒,什么都没留下

文章评论