一、colly框架简介

前言:colly 是 Go 实现的比较有名的一款爬虫框架,而且 Go 在高并发和分布式场景的优势也正是爬虫技术所需要的。它的主要特点是轻量、快速,设计非常优雅,并且分布式的支持也非常简单,易于扩展。
github地址: github.com/gocolly/colly
colly官网地址:http://go-colly.org/
从上图中,我们可以看出colly在github社区有着超高的人气,到目前为止已经有17862个赞了。同Python爬虫框架Scrapy一样,属于不同语言中的超人气框架!
二、colly特性说明
谈起爬虫框架,我觉得大家听过最多的就是requests库、Scrapy这类型的Python框架吧。如果再细分,Scrapy框架应该是功能最多也最好用的框架之一吧,优点这里就省略了,今天我们引出一个新的框架colly,先来介绍一下他的特性吧:
-
干净的API
-
快速(单核>1k请求/秒)
-
管理每个域的请求延迟和最大并发性
-
自动cookie和会话处理
-
同步/异步并行抓取
-
分布式抓取
-
缓存
-
非unicode响应的自动编码
-
robots. txt的支持
-
抓取深度控制
-
设置跨域开关
-
谷歌应用程序引擎支持
总结:如果不是认真观察,我都感觉colly是scrapy的孪生兄弟呢,很多功能都极其的相似,接下来就让我们看看这个框架牛逼的地方吧,为啥会有这么多的star呢?
三、爬虫架构对比
了解爬虫的都知道一个爬虫请求的生命周期主要为以下五点:
-
构建爬虫请求
-
发送及调度请求
-
获取文档或数据
-
解析字段或清洗数据
-
数据处理或持久化
结合上面的步骤,我们先来谈下scrapy架构,如下图所示:

如上图,downloader负责请求获取页面,spiders中写具体解析字段的逻辑,item PipeLine数据最后处理, 中间有一些中间件,可以定制化一些功能设置。比如,代理,请求频率等。
然后,我们谈下colly架构的特别,colly的逻辑更像是面向过程编程的, colly的逻辑就是按上面生命周期的顺序进行处理, 只是在不同阶段,加上回调函数进行过滤的时候进行处理。架构图如下所示:

四、colly框架实战示例
go colly的网络爬虫还是很强大,下面我们通过代码来看一下这个功能的使用:
// Package main -----------------------------
// @author : 逆向与爬虫的故事
// @time : 2022/10/6 13:24
// -------------------------------------------
package main
import (
"fmt"
"github.com/gocolly/colly"
"github.com/gocolly/colly/debug"
"net"
"net/http"
"regexp"
"time"
)
func main() {
mUrl := "http://www.ifeng.com/"
//colly的主体是Collector对象,管理网络通信和负责在作业运行时执行附加的回掉函数
c := colly.NewCollector(
// 开启DEBUG
colly.Debugger(&debug.LogDebugger{}),
// 是否开启异步
colly.Async(true),
// 跨域设置
colly.AllowedDomains("www.ifeng.com"),
// 允许重复抓取
colly.AllowURLRevisit(),
// url设置
colly.URLFilters(
regexp.MustCompile(".*"),
),
// 设置UA
colly.UserAgent("Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36"),
)
c.Limit(&colly.LimitRule{
Parallelism: 5, // 并发设置
Delay: time.Second * 3, // 下载延时
RandomDelay: time.Second * 5, // 随机延时
})
// 代理、连接数、上下文机制、超时等配置
c.WithTransport(&http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
})
//发送请求之前的执行函数
c.OnRequest(func(r *colly.Request) {
fmt.Println("这里是发送之前执行的函数")
})
//发送请求错误被回调
c.OnError(func(_ *colly.Response, err error) {
fmt.Print(err)
})
//响应请求之后被回调
c.OnResponse(func(r *colly.Response) {
fmt.Println("Response body length:", len(r.Body))
})
//response之后会调用该函数,分析页面数据
c.OnHTML("p a", func(e *colly.HTMLElement) {
fmt.Println(e.Text)
})
//在OnHTML之后被调用
c.OnScraped(func(r *colly.Response) {
fmt.Println("Finished", r.Request.URL)
})
//这里是执行访问url
c.Visit(mUrl)
//阻塞main进程,防止还未采集完成就已经退出
c.Wait()
}
运行结果如下:

总结一下,回调函数的调用顺序如下:
-
OnRequest在发起请求前被调用
-
OnError请求过程中如果发生错误被调用
-
OnResponse收到回复后被调用
-
OnHTML在OnResponse之后被调用,如果收到的内容是HTML
-
OnScraped在OnHTML之后被调用
通过实战,观察打印日志,让我相信Go的并发性是真的强??
五、colly框架总结
colly回调函数共有如下7种,表格如下:

colly回调函数已经满足:
-
request事前处理回调
-
request请求错误回调
-
收到响应头处理回调
-
成功响应处理回调
-
HTML内容处理回调
-
爬虫结束处理回调
六、colly使用方法
简介
colly
是用 Go 语言编写的功能强大的爬虫框架。它提供简洁的 API,拥有强劲的性能,可以自动处理 cookie&session,还有提供灵活的扩展机制。
首先,我们介绍colly
的基本概念。然后通过几个案例来介绍colly
的用法和特性:拉取 GitHub Treading,拉取百度小说热榜,下载 Unsplash 网站上的图片。
快速使用
本文代码使用 Go Modules。
创建目录并初始化:
mkdir colly && cd colly
go mod init github.com/darjun/go-daily-lib/colly
安装colly
库:
go get -u github.com/gocolly/colly/v2
使用:
package main
import (
"fmt"
"github.com/gocolly/colly/v2"
)
func main() {
c := colly.NewCollector(
colly.AllowedDomains("www.baidu.com" ),
)
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Attr("href")
fmt.Printf("Link found: %q -> %s\n", e.Text, link)
c.Visit(e.Request.AbsoluteURL(link))
})
c.OnRequest(func(r *colly.Request) {
fmt.Println("Visiting", r.URL.String())
})
c.OnResponse(func(r *colly.Response) {
fmt.Printf("Response %s: %d bytes\n", r.Request.URL, len(r.Body))
})
c.OnError(func(r *colly.Response, err error) {
fmt.Printf("Error %s: %v\n", r.Request.URL, err)
})
c.Visit("http://www.baidu.com/")
}
colly
的使用比较简单:
首先,调用colly.NewCollector()
创建一个类型为*colly.Collector
的爬虫对象。由于每个网页都有很多指向其他网页的链接。如果不加限制的话,运行可能永远不会停止。所以上面通过传入一个选项colly.AllowedDomains("www.baidu.com")
限制只爬取域名为www.baidu.com
的网页。
然后我们调用c.OnHTML
方法注册HTML
回调,对每个有href
属性的a
元素执行回调函数。这里继续访问href
指向的 URL。也就是说解析爬取到的网页,然后继续访问网页中指向其他页面的链接。
调用c.OnRequest()
方法注册请求回调,每次发送请求时执行该回调,这里只是简单打印请求的 URL。
调用c.OnResponse()
方法注册响应回调,每次收到响应时执行该回调,这里也只是简单的打印 URL 和响应大小。
调用c.OnError()
方法注册错误回调,执行请求发生错误时执行该回调,这里简单打印 URL 和错误信息。
最后我们调用c.Visit()
开始访问第一个页面。
运行:
$ go run main.go
Visiting http://www.baidu.com/
Response http://www.baidu.com/: 303317 bytes
Link found: "百度首页" -> /
Link found: "设置" -> javascript:;
Link found: "登录" -> https://passport.baidu.com/v2/?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2F&sms=5
Link found: "新闻" -> http://news.baidu.com
Link found: "hao123" -> https://www.hao123.com
Link found: "地图" -> http://map.baidu.com
Link found: "直播" -> https://live.baidu.com/
Link found: "视频" -> https://haokan.baidu.com/?sfrom=baidu-top
Link found: "贴吧" -> http://tieba.baidu.com
...
colly
爬取到页面之后,会使用goquery解析这个页面。然后查找注册的 HTML 回调对应元素选择器(element-selector),将goquery.Selection
封装成一个colly.HTMLElement
执行回调。
colly.HTMLElement
其实就是对goquery.Selection
的简单封装:
type HTMLElement struct {
Name string
Text string
Request *Request
Response *Response
DOM *goquery.Selection
Index int
}
并提供了简单易用的方法:
Attr(k string)
:返回当前元素的属性,上面示例中我们使用e.Attr("href")
获取了href
属性;
ChildAttr(goquerySelector, attrName string)
:返回goquerySelector
选择的第一个子元素的attrName
属性;
ChildAttrs(goquerySelector, attrName string)
:返回goquerySelector
选择的所有子元素的attrName
属性,以[]string
返回;
ChildText(goquerySelector string)
:拼接goquerySelector
选择的子元素的文本内容并返回;
ChildTexts(goquerySelector string)
:返回goquerySelector
选择的子元素的文本内容组成的切片,以[]string
返回。
ForEach(goquerySelector string, callback func(int, *HTMLElement))
:对每个goquerySelector
选择的子元素执行回调callback
;
Unmarshal(v interface{})
:通过给结构体字段指定 goquerySelector 格式的 tag,可以将一个 HTMLElement 对象 Unmarshal 到一个结构体实例中。
这些方法会被频繁地用到。下面我们就通过一些示例来介绍colly
的特性和用法。
GitHub Treading
我之前写过一个拉取GitHub Treading 的 API,用colly
更方便:
type Repository struct {
Author string
Name string
Link string
Desc string
Lang string
Stars int
Forks int
Add int
BuiltBy []string
}
func main() {
c := colly.NewCollector(
colly.MaxDepth(1),
)
repos := make([]*Repository, 0, 15)
c.OnHTML(".Box .Box-row", func (e *colly.HTMLElement) {
repo := &Repository{}
// author & repository name
authorRepoName := e.ChildText("h1.h3 > a")
parts := strings.Split(authorRepoName, "/")
repo.Author = strings.TrimSpace(parts[0])
repo.Name = strings.TrimSpace(parts[1])
// link
repo.Link = e.Request.AbsoluteURL(e.ChildAttr("h1.h3 >a", "href"))
// description
repo.Desc = e.ChildText("p.pr-4")
// language
repo.Lang = strings.TrimSpace(e.ChildText("div.mt-2 > span.mr-3 > span[itemprop]"))
// star & fork
starForkStr := e.ChildText("div.mt-2 > a.mr-3")
starForkStr = strings.Replace(strings.TrimSpace(starForkStr), ",", "", -1)
parts = strings.Split(starForkStr, "\n")
repo.Stars , _=strconv.Atoi(strings.TrimSpace(parts[0]))
repo.Forks , _=strconv.Atoi(strings.TrimSpace(parts[len(parts)-1]))
// add
addStr := e.ChildText("div.mt-2 > span.float-sm-right")
parts = strings.Split(addStr, " ")
repo.Add, _ = strconv.Atoi(parts[0])
// built by
e.ForEach("div.mt-2 > span.mr-3 img[src]", func (index int, img *colly.HTMLElement) {
repo.BuiltBy = append(repo.BuiltBy, img.Attr("src"))
})
repos = append(repos, repo)
})
c.Visit("https://github.com/trending")
fmt.Printf("%d repositories\n", len(repos))
fmt.Println("first repository:")
for _, repo := range repos {
fmt.Println("Author:", repo.Author)
fmt.Println("Name:", repo.Name)
break
}
}
我们用ChildText
获取作者、仓库名、语言、星数和 fork 数、今日新增等信息,用ChildAttr
获取仓库链接,这个链接是一个相对路径,通过调用e.Request.AbsoluteURL()
方法将它转为一个绝对路径。
运行:
$ go run main.go
25 repositories
first repository:
Author: Shopify
Name: dawn
百度小说热榜
网页结构如下:


各部分结构如下:
- 每条热榜各自在一个
div.category-wrap_iQLoo
中;
a
元素下div.index_1Ew5p
是排名;
- 内容在
div.content_1YWBm
中;
- 内容中
a.title_dIF3B
是标题;
- 内容中两个
div.intro_1l0wp
,前一个是作者,后一个是类型;
- 内容中
div.desc_3CTjT
是描述。
由此我们定义结构:
type Hot struct {
Rank string `selector:"a > div.index_1Ew5p"`
Name string `selector:"div.content_1YWBm > a.title_dIF3B"`
Author string `selector:"div.content_1YWBm > div.intro_1l0wp:nth-child(2)"`
Type string `selector:"div.content_1YWBm > div.intro_1l0wp:nth-child(3)"`
Desc string `selector:"div.desc_3CTjT"`
}
tag 中是 CSS 选择器语法,添加这个是为了可以直接调用HTMLElement.Unmarshal()
方法填充Hot
对象。
然后创建Collector
对象:
c := colly.NewCollector()
注册回调:
c.OnHTML("div.category-wrap_iQLoo", func(e *colly.HTMLElement) {
hot := &Hot{}
err := e.Unmarshal(hot)
if err != nil {
fmt.Println("error:", err)
return
}
hots = append(hots, hot)
})
c.OnRequest(func(r *colly.Request) {
fmt.Println("Requesting:", r.URL)
})
c.OnResponse(func(r *colly.Response) {
fmt.Println("Response:", len(r.Body))
})
OnHTML
对每个条目执行Unmarshal
生成Hot
对象。
OnRequest/OnResponse
只是简单输出调试信息。
然后,调用c.Visit()
访问网址:
err := c.Visit("https://top.baidu.com/board?tab=novel")
if err != nil {
fmt.Println("Visit error:", err)
return
}
最后添加一些调试打印:
fmt.Printf("%d hots\n", len(hots))
for _, hot := range hots {
fmt.Println("first hot:")
fmt.Println("Rank:", hot.Rank)
fmt.Println("Name:", hot.Name)
fmt.Println("Author:", hot.Author)
fmt.Println("Type:", hot.Type)
fmt.Println("Desc:", hot.Desc)
break
}
运行输出:
Requesting: https://top.baidu.com/board?tab=novel
Response: 118083
30 hots
first hot:
Rank: 1
Name: 逆天邪神
Author: 作者:火星引力
Type: 类型:玄幻
Desc: 掌天毒之珠,承邪神之血,修逆天之力,一代邪神,君临天下! 查看更多>
Unsplash
我写公众号文章,背景图片基本都是从 unsplash 这个网站获取。unsplash 提供了大量的、丰富的、免费的图片。这个网站有个问题,就是访问速度比较慢。既然学习爬虫,刚好利用程序自动下载图片。
unsplash 首页如下图所示:


网页结构如下:


但是首页上显示的都是尺寸较小的图片,我们点开某张图片的链接:


网页结构如下:


由于涉及三层网页结构(img
最后还需要访问一次),使用一个colly.Collector
对象,OnHTML
回调设置需要格外小心,给编码带来比较大的心智负担。colly
支持多个Collector
,我们采用这种方式来编码:
func main() {
c1 := colly.NewCollector()
c2 := c1.Clone()
c3 := c1.Clone()
c1.OnHTML("figure[itemProp] a[itemProp]", func(e *colly.HTMLElement) {
href := e.Attr("href")
if href == "" {
return
}
c2.Visit(e.Request.AbsoluteURL(href))
})
c2.OnHTML("div._1g5Lu > img[src]", func(e *colly.HTMLElement) {
src := e.Attr("src")
if src == "" {
return
}
c3.Visit(src)
})
c1.OnRequest(func(r *colly.Request) {
fmt.Println("Visiting", r.URL)
})
c1.OnError(func(r *colly.Response, err error) {
fmt.Println("Visiting", r.Request.URL, "failed:", err)
})
}
我们使用 3 个Collector
对象,第一个Collector
用于收集首页上对应的图片链接,然后使用第二个Collector
去访问这些图片链接,最后让第三个Collector
去下载图片。上面我们还为第一个Collector
注册了请求和错误回调。
第三个Collector
下载到具体的图片内容后,保存到本地:
func main() {
// ... 省略
var count uint32
c3.OnResponse(func(r *colly.Response) {
fileName := fmt.Sprintf("images/img%d.jpg", atomic.AddUint32(&count, 1))
err := r.Save(fileName)
if err != nil {
fmt.Printf("saving %s failed:%v\n", fileName, err)
} else {
fmt.Printf("saving %s success\n", fileName)
}
})
c3.OnRequest(func(r *colly.Request) {
fmt.Println("visiting", r.URL)
})
}
上面使用atomic.AddUint32()
为图片生成序号。
运行程序,爬取结果:


异步
默认情况下,colly
爬取网页是同步的,即爬完一个接着爬另一个,上面的 unplash 程序就是如此。这样需要很长时间,colly
提供了异步爬取的特性,我们只需要在构造Collector
对象时传入选项colly.Async(true)
即可开启异步:
c1 := colly.NewCollector(
colly.Async(true),
)
但是,由于是异步爬取,所以程序最后需要等待Collector
处理完成,否则早早地退出main
,程序会退出:
c1.Wait()
c2.Wait()
c3.Wait()
再次运行,速度快了很多?。
第二版
向下滑动 unsplash 的网页,我们发现后面的图片是异步加载的。滚动页面,通过 chrome 浏览器的 network 页签查看请求:


请求路径/photos
,设置per_page
和page
参数,返回的是一个 JSON 数组。所以有了另一种方式:
定义每一项的结构体,我们只保留必要的字段:
type Item struct {
Id string
Width int
Height int
Links Links
}
type Links struct {
Download string
}
然后在OnResponse
回调中解析 JSON,对每一项的Download
链接调用负责下载图像的Collector
的Visit()
方法:
c.OnResponse(func(r *colly.Response) {
var items []*Item
json.Unmarshal(r.Body, &items)
for _, item := range items {
d.Visit(item.Links.Download)
}
})
初始化访问,我们设置拉取 3 页,每页 12 个(和页面请求的个数一致):
for page := 1; page <= 3; page++ {
c.Visit(fmt.Sprintf("https://unsplash.com/napi/photos?page=%d&per_page=12", page))
}
运行,查看下载的图片:


限速
有时候并发请求太多,网站会限制访问。这时就需要使用LimitRule
了。说白了,LimitRule
就是限制访问速度和并发量的:
type LimitRule struct {
DomainRegexp string
DomainGlob string
Delay time.Duration
RandomDelay time.Duration
Parallelism int
}
常用的就Delay/RandomDelay/Parallism
这几个,分别表示请求与请求之间的延迟,随机延迟,和并发数。另外必须指定对哪些域名施行限制,通过DomainRegexp
或DomainGlob
设置,如果这两个字段都未设置Limit()
方法会返回错误。用在上面的例子中:
err := c.Limit(&colly.LimitRule{
DomainRegexp: `unsplash\.com`,
RandomDelay: 500 * time.Millisecond,
Parallelism: 12,
})
if err != nil {
log.Fatal(err)
}
我们设置针对unsplash.com
这个域名,请求与请求之间的随机最大延迟 500ms,最多同时并发 12 个请求。
设置超时
有时候网速较慢,colly
中使用的http.Client
有默认超时机制,我们可以通过colly.WithTransport()
选项改写:
c.WithTransport(&http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
})
扩展
colly
在子包extension
中提供了一些扩展特性,最最常用的就是随机 User-Agent 了。通常网站会通过 User-Agent 识别请求是否是浏览器发出的,爬虫一般会设置这个 Header 把