共计 9781 个字符,预计需要花费 25 分钟才能阅读完成。
简介
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 把本人伪装成浏览器。应用也比较简单:
import "github.com/gocolly/colly/v2/extensions"
func main() {c := colly.NewCollector()
extensions.RandomUserAgent(c)
}
随机 User-Agent 实现也很简略,就是从一些事后定义好的 User-Agent 数组中随机一个设置到 Header 中:
func RandomUserAgent(c *colly.Collector) {c.OnRequest(func(r *colly.Request) {r.Headers.Set("User-Agent", uaGens[rand.Intn(len(uaGens))]())
})
}
实现本人的扩大也不难,例如咱们每次申请时须要设置一个特定的 Header,扩大能够这么写:
func MyHeader(c *colly.Collector) {c.OnRequest(func(r *colly.Request) {r.Headers.Set("My-Header", "dj")
})
}
用 Collector
对象调用 MyHeader()
函数即可:
MyHeader(c)
总结
colly
是 Go 语言中最风行的爬虫框架,反对丰盛的个性。本文对一些罕用个性做了介绍,并辅之以实例。限于篇幅,一些高级个性未能波及,例如队列,存储等。对爬虫感兴趣的可去深刻理解。
大家如果发现好玩、好用的 Go 语言库,欢送到 Go 每日一库 GitHub 上提交 issue😄
参考
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
- Go 每日一库之 goquery:https://darjun.github.io/2020/10/11/godailylib/goquery/
- 用 Go 实现一个 GitHub Trending API:https://darjun.github.io/2021/06/16/github-trending-api/
- colly GitHub:https://github.com/gocolly/colly
我
我的博客:https://darjun.github.io
欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~