[译]go爬虫框架colly - 最佳实践

1,269 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情


本文翻译自官方文档:Scraping Framework for Golang (go-colly.org)


Gopher们的快速优雅的爬虫框架


最佳实践

调试

有时候,在调用回调函数中放置一些 log.Println() 函数就足够了,但是有时候不够。 Colly 内置有用于 collector 调试的能力。有一个调试器接口和不同类型的调试器实现可用。

绑定调试器到 collector

绑定一个基本的日志调试器需要 Colly 仓库的 debug (github.com/gocolly/colly/debug) 包。

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

func main() {
    c := colly.NewCollector(colly.Debugger(&debug.LogDebugger{}))
    // [..]
}

实现一个自定义调试器

可以通过实现 debug.Debugger 接口创建任何类型的自定义调试器。一个好的示例是 LogDebugger

logdebugger.go:

// Copyright 2018 Adam Tauber
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package debug

import (
	"io"
	"log"
	"os"
	"sync/atomic"
	"time"
)

// LogDebugger 是最简化的调试器,它打印日志信息到 STDERR (标准错误)
type LogDebugger struct {
	// Output 是日志的目标位置,它可以是任何实现 io.Writer 接口的对象。
	// 保持空白的话会使用 STDERR
	Output io.Writer
	// Prefix 在创建的每个日志行开头出现
	Prefix string
	// Flag 定义日志属性
	Flag    int
	logger  *log.Logger
	counter int32
	start   time.Time
}

// Init 初始化 LogDebugger
func (l *LogDebugger) Init() error {
	l.counter = 0
	l.start = time.Now()
	if l.Output == nil {
		l.Output = os.Stderr
	}
	l.logger = log.New(l.Output, l.Prefix, l.Flag)
	return nil
}

// Event 招收 Collector 事件然后将它们打印到 STDERR (标准错误)
func (l *LogDebugger) Event(e *Event) {
	i := atomic.AddInt32(&l.counter, 1)
	l.logger.Printf("[%06d] %d [%6d - %s] %q (%s)\n", i, e.CollectorID, e.RequestID, e.Type, e.Values, time.Since(l.start))
}

分布式爬取

分布式爬取可用不同的方式实现,这取决于爬取任务的需要。使用代理和 Colly 代理切换器可轻易地实现对网络通信层的扩展,大多数情况下,这已够用了。

代理切换器

当 HTTP 请求在多个代理之间分布时,使用代理切换器爬取仍然会保持集中(非分布)方式。 Colly 通过它的 SetProxyFunc() 成员支持代理切换。任何带 func(*http.Request) (*url.URL, error) 签名的自定义函数都可传递给 SetProxyFunc()

提示

SSH 服务器可加 -D 标志用作 socks5 代理。

Colly 有内置的代理切换器,可以对每个请求轮换代理列表中的代理。

用法

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)
	}
	// ...
}

实现自定义代理切换器:

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 服务器或 Google App Engine 。使用自定义的 存储 可实现集中,并且将 Cookie 和访问过的 URL 处理持久化。

提示 Colly 内置有对Google App Engine的支持。如果从 App Engine 的标准环境使用 Colly ,不要忘记调用 Collector.Appengine(*http.Request) 。

这里有一个实现的示例。

分布式存储

访问过的 URL 和 Cookie 默认存储在内存中。对短周期的爬取任务很方便,但是当处理大型的扩展或长周期的爬取任务会有严重的限制。

Colly 可使用基于实现 colly/storage.Storage 接口的任何存储来代替默认的内存存储。 可查看 存在的存储

后端存储

Colly 有内置的内存存储后端用来存储 Cookie 和 访问过的 URL,但它可以用任何实现colly/storage.Storage的自定义存储后端重写。

已有的存储后端

内存后端

Colly 的默认后端。可使用 collector.SetStorage() 覆写。

Redis后端

查看 redis 示例 了解详细内容。

boltdb 后端

SQLite3 后端

MongoDB 后端

PostgreSQL 后端

使用多个 collector

如果爬取任务很复杂或者有不同种类的子任务,建议对于单个爬取任务使用多个 collector 。

一个不错的示例是 coursera course scraper ,它使用了两个 collector - 一个是解析列出的视图和处理分页,另一个是收集过程的详细内容。

Colly 内置有支持使用多个 collector 的方法。

提示

在调试时使用 collector.ID 以区别不同的 collector

克隆 collector

如果 collector 有相似的配置,可以使用 collector 的 Clone() 方法。 Clone() 会用同样的配置复制 collector ,但是不会绑定回调。

c := colly.NewCollector(
	colly.UserAgent("myUserAgent"),
	colly.AllowedDomains("foo.com", "bar.com"),
)
// 自定义 User-Agent 和允许的域克隆到 c2
c2 := c.Clone()

在 collector 之间解析自定义数据

使用 collector 的 Request() 函数可以和其它 collector 共享上下文。

共享上下文的示例:

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

爬虫配置

对于单任务爬取少量的站点,Colly 的默认配置已经优化。如果想要爬取百万级的站点,该设置工作不是最优的。这里有一些调整。

使用持久化存储后端

默认情况下, Colly 在内存中存储 Cookie 和访问过的 URL 。可以用任何自定义的后端代替内存存储后端。更多详细内容查看 这里

对于使用递归调用的长周期运行的任务使用异步

默认情况下,请求未完成时 Colly 会阻塞,所以从回调中递归地调用 Collector.Visit 会产生持续增长的栈。使用 Collector.Async = true 可以避免该问题。(不要忘记使用 c.Wait() 进行异步)

禁用或限制连接的 keep-alive (保持连接)

Colly 使用 HTTP keep-alive 以加强爬取速度。它需要打开文件描述符,所以在长周期运行的任务中 max-fd (最大文件描述符)的限制可以轻易达到。

可使用以下代码禁用 HTTP Keep-alive:

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

扩展

扩展是 Colly 自带的小型辅助工具类。插件列表参考 这里

用法

下面的示例提供了随机的 User-Agent 切换器 和 Referrer 设定扩展,并访问 httpbin.org 两次。

import (
    "log"

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

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

    extensions.RandomUserAgent(c)
    extensions.Referer(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")
}