有些人可能认为爬虫框架和 http client 库的功能一样,用 http client 库也可以写爬虫。当然,无论用第三方的 http client 库还是官方的http
库,都可以写爬虫。但术业有专攻,爬虫框架专门为批量爬取设计,往往拥有并发控制、队列、缓存、HTML 解析等一系列开箱即用的 API,能大幅简化在爬虫实现过程中的负担
Python 中非常知名的爬虫框架有Scrapy
,Go 中也有一些 star 数较高的爬虫框架。colly
就是其中的佼佼者,它 API 简洁,性能优良,开箱即用。今天就来快速学习一下吧!
基本使用
首先引入依赖
go
go get -u github.com/gocolly/colly/...
之后就可以使用colly
,通过Visit
函数来告知colly 采集器要访问的 URL
go
package main
import (
"fmt"
"github.com/gocolly/colly/v2"
)
func main() {
c := colly.NewCollector()
c.Visit("http://go-colly.org/")
}
这样就行了么?运行下试试,没有任何输出。
shell
$ go run main.go
原因在于代码要求 colly 采集器访问http://go-colly.org/
,但没有设定访问 URL 成功或者失败后要执行的动作。colly
提供了一系列的回调函数,用于 URL 访问和响应过程中各种情况的处理
例如,可以设定访问 URL 前、响应成功、响应失败时不同逻辑的处理
go
package main
import (
"fmt"
"github.com/gocolly/colly/v2"
)
func main() {
c := colly.NewCollector()
c.OnRequest(func(r *colly.Request) {
fmt.Println("Visiting", r.URL)
})
c.OnResponse(func(r *colly.Response) {
fmt.Println("Visited", r.Request.URL)
})
c.OnError(func(_ *colly.Response, err error) {
fmt.Println("Something went wrong:", err)
})
c.Visit("http://go-colly.org/")
}
colly
提供的回调和回调的顺序如下图,每个回调可以设置多次,会依次执行
常规配置
配置分两部分,一部分是 colly 采集器的配置,一部分是 HTTP 的配置
🌲 colly 采集器的配置
✨ 新建 colly 采集器时指定配置,例如
go
c := colly.NewCollector(
colly.UserAgent("example.com"),
colly.DisallowedDomains("baidu.com", "bing.com"),
)
//...
c.Visit("http://baidu.com/")
c.Visit("http://go-colly.org/")
✨ 使用环境变量可以不用重新编译代码。看名字大家应该也能猜到每个环境变量都有什么作用
注意环境变量有固定前缀COLLY_
,官方文档里并没有说明(坑!)
COLLY_ALLOWED_DOMAINS
(逗号分隔)COLLY_CACHE_DIR
(string)COLLY_DETECT_CHARSET
(y/n)COLLY_DISABLE_COOKIES
(y/n)COLLY_DISALLOWED_DOMAINS
(逗号分隔)COLLY_IGNORE_ROBOTSTXT
(y/n)COLLY_MAX_BODY_SIZE
(int)COLLY_MAX_DEPTH
(0 代表不限深度)COLLY_PARSE_HTTP_ERROR_RESPONSE
(y/n)COLLY_USER_AGENT
(string)
shell
$ COLLY_DISALLOWED_DOMAINS=baidu.com,bing.com go run main.go
Visiting http://go-colly.org/
Visited http://go-colly.org/
Finished http://go-colly.org/
✨ 通过 colly 采集器的属性进行配置
go
c := colly.NewCollector()
c.UserAgent = "example.com"
//...
c.Visit("http://go-colly.org/")
c.DisallowedDomains = []string{"baidu.com", "bing.com"}
c.Visit("http://baidu.com/")
colly 采集器的配置优先级就是上面介绍的顺序:新建 < 环境变量 < 实例属性
🌲 HTTP 的配置
colly 默认使用的是 Go 标准库中的 http client,我们可以进行替换
go
c := colly.NewCollector()
c.WithTransport(&http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
常见功能
作为一个爬虫,很少会仅抓取一个链接,通常会抓取大量的链接,甚至会不断分析当前页面中的链接,继续进行深度的爬取。代码通常会类似下面
✨ 为了避免无限制的爬取,可以限制爬取的域名范围,和访问深度;
✨ colly 会记录已经爬取过的链接,不会再重复爬取
go
func main() {
c := colly.NewCollector(
colly.AllowedDomains("httpbin.org"),
colly.MaxDepth(2),
)
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Attr("href")
fmt.Printf("Link found: %q -> %s\n", e.Text, link)
c.Visit(link)
})
c.OnError(func(_ *colly.Response, err error) {
fmt.Println("Something went wrong:", err)
})
c.Visit("http://httpbin.org/links/20/3")
}
并行抓取
colly 默认是串行逐个链接进行爬取,想要提高爬取能力最快速简单的方式就是开启并行。
✨ 除了需要设置colly.Async(true)
之外,还需要在最后c.Wait()
等待所有并发的请求执行完成
✨ c.Limit
可以针对某一个域名设置并发度和发起每一个请求的延迟时间
go
func main() {
c := colly.NewCollector(
colly.AllowedDomains("httpbin.org"),
colly.MaxDepth(2),
colly.Async(true),
)
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Attr("href")
fmt.Printf("Link found: %q -> %s\n", e.Text, link)
c.Visit(link)
})
c.Limit(&colly.LimitRule{
DomainGlob: "*httpbin.*",
Parallelism: 2,
Delay: 5 * time.Second,
})
c.Visit("http://httpbin.org/links/20/3")
c.Wait()
}
持久化的外部存储
默认情况下已访问的 URL 和 cookie 等信息都是存储在内存中,服务重新启动后将会丢失这部分信息,且无法在多个机器直接共享。
当我们构建一个分布式爬虫时,我们需要一个公共的用于维护状态的持久化存储,例如 redis 等。
go
package main
import (
"fmt"
"github.com/gocolly/colly/v2"
"github.com/gocolly/redisstorage"
)
func main() {
c := colly.NewCollector(
colly.AllowedDomains("httpbin.org", "go-colly.org"),
colly.MaxDepth(2),
)
storage := &redisstorage.Storage{
Address: "127.0.0.1:6379",
Password: "",
DB: 0,
Prefix: "httpbin_test",
}
err := c.SetStorage(storage)
if err != nil {
panic(err)
}
// 清除之前存储的信息,可选
if err := storage.Clear(); err != nil {
panic(err)
}
defer storage.Client.Close()
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Attr("href")
fmt.Printf("Link found: %q -> %s\n", e.Text, link)
c.Visit(link)
})
// ...
c.Visit("http://httpbin.org/links/20/3")
}
队列
可以使用队列来控制爬取的速率,默认情况下队列也是在内存中的
go
import (
"fmt"
"github.com/gocolly/colly/v2"
"github.com/gocolly/colly/v2/queue"
)
func main() {
q, _ := queue.New(
2, // 消费的进程数
&queue.InMemoryQueueStorage{MaxSize: 10000}, // 默认的队列,内存队列
)
c := colly.NewCollector(
colly.AllowedDomains("httpbin.org", "go-colly.org"),
colly.MaxDepth(2),
)
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Attr("href")
fmt.Printf("Link found: %q -> %s\n", e.Text, link)
q.AddURL(link)
})
q.AddURL("http://go-colly.org/")
q.Run(c)
}
对于构建分布式爬虫来说,可以借助外部的队列提高整体的消费能力。
go
package main
import (
"fmt"
"github.com/gocolly/colly/v2"
"github.com/gocolly/colly/v2/queue"
"github.com/gocolly/redisstorage"
)
func main() {
// 创建redis存储
storage := &redisstorage.Storage{
Address: "127.0.0.1:6379",
Password: "",
DB: 0,
Prefix: "httpbin_test",
}
q, err := queue.New(
2, // 消费的进程数
storage,
)
if err != nil{
panic(err)
}
c := colly.NewCollector(
colly.AllowedDomains("httpbin.org", "go-colly.org"),
colly.MaxDepth(2),
)
err = c.SetStorage(storage)
if err != nil {
panic(err)
}
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Attr("href")
fmt.Printf("Link found: %q -> %s\n", e.Text, link)
q.AddURL(link)
})
q.AddURL("http://go-colly.org/")
q.Run(c)
}
总结
这篇文章作为一个入门介绍,看完后你应该能 Get 到普通的 http client 库和爬虫库的区别了吧。
colly 作为一个爬虫框架集成了一系列针对爬虫的 API,想要体验 colly 的更多能力,建议还是好好阅读下 colly 的文档