colly
是用 Go 语言编写的功能强大的爬虫框架。它提供简洁的 API,拥有强劲的性能,能够自动处理 cookie&session,还有提供灵活的扩展机制。javascript
首先,咱们介绍colly
的基本概念。而后经过几个案例来介绍colly
的用法和特性:拉取 GitHub Treading,拉取百度小说热榜,下载 Unsplash 网站上的图片。java
本文代码使用 Go Modules。git
建立目录并初始化:github
$ mkdir colly && cd colly $ go mod init github.com/darjun/go-daily-lib/colly
安装colly
库:golang
$ go get -u github.com/gocolly/colly/v2
使用:chrome
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
的使用比较简单:json
首先,调用colly.NewCollector()
建立一个类型为*colly.Collector
的爬虫对象。因为每一个网页都有不少指向其余网页的连接。若是不加限制的话,运行可能永远不会中止。因此上面经过传入一个选项colly.AllowedDomains("www.baidu.com")
限制只爬取域名为www.baidu.com
的网页。api
而后咱们调用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 的 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 首页以下图所示:
网页结构以下:
可是首页上显示的都是尺寸较小的图片,咱们点开某张图片的连接:
网页结构以下:
因为涉及三层网页结构(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😄
欢迎关注个人微信公众号【GoUpUp】,共同窗习,一块儿进步~