Colly是Golang世界最知名的Web爬蟲框架了,它的API清晰明瞭,高度可配置和可擴充套件,支援分散式抓取,還支援多種儲存後端(如記憶體、Redis、MongoDB等)。這篇文章記錄我學習使用它的的一些感受和理解。
首先安裝它:
❯ go get -u github.com/gocolly/colly/...
複製程式碼
這個go get
和之前安裝包不太一樣,最後有...
這樣的省略號,它的意思是也獲取這個包的子包和依賴。
從最簡單的例子開始
Colly的文件寫的算是很詳細很完整的了,而且專案下的_examples目錄裡面也有很多爬蟲例子,上手非常容易。先看我的一個例子:
package main
import (
"fmt"
"github.com/gocolly/colly"
)
func main() {
c := colly.NewCollector(
colly.UserAgent("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"),
)
c.OnRequest(func(r *colly.Request) {
fmt.Println("Visiting", r.URL)
})
c.OnError(func(_ *colly.Response, err error) {
fmt.Println("Something went wrong:", err)
})
c.OnResponse(func(r *colly.Response) {
fmt.Println("Visited", r.Request.URL)
})
c.OnHTML(".paginator a", func(e *colly.HTMLElement) {
e.Request.Visit(e.Attr("href"))
})
c.OnScraped(func(r *colly.Response) {
fmt.Println("Finished", r.Request.URL)
})
c.Visit("https://movie.douban.com/top250?start=0&filter=")
}
複製程式碼
這個程式就是去找豆瓣電影Top250的全部連結,如OnHTML方法的第一個函式所描述,找類名是paginator的標籤下的a標籤的href屬性值。
執行一下:
❯ go run colly/doubanCrawler1.go
Visiting https://movie.douban.com/top250?start=0&filter=
Visited https://movie.douban.com/top250?start=0&filter=
Visiting https://movie.douban.com/top250?start=25&filter=
Visited https://movie.douban.com/top250?start=25&filter=
...
Finished https://movie.douban.com/top250?start=25&filter=
Finished https://movie.douban.com/top250?start=0&filter=
複製程式碼
在Colly中主要實體就是一個Collector物件(用colly.NewCollector建立),Collector管理網路通訊和對於響應的回撥執行。Collector在初始化時可以接受多種設定項,例如這個例子裡面我就設定了UserAgent的值。其他的設定項可以去看官方網站。
Collector物件接受多種回撥方法,有不同的作用,按呼叫順序我列出來:
- OnRequest。請求前
- OnError。請求過程中發生錯誤
- OnResponse。收到響應後
- OnHTML。如果收到的響應內容是HTML呼叫它。
- OnXML。如果收到的響應內容是XML 呼叫它。寫爬蟲基本用不到,所以上面我沒有使用它。
- OnScraped。在OnXML/OnHTML回撥完成後呼叫。不過官網寫的是
Called after OnXML callbacks
,實際上對於OnHTML也有效,大家可以注意一下。
抓取條目ID和標題
還是之前的需求,先看看豆瓣Top250頁面每個條目的部分HTML程式碼:
<ol class="grid_view">
<li>
<div class="item">
<div class="info">
<div class="hd">
<a href="https://movie.douban.com/subject/1292052/" class="">
<span class="title">肖申克的救贖</span>
<span class="title"> / The Shawshank Redemption</span>
<span class="other"> / 月黑高飛(港) / 刺激 1995(臺)</span>
</a>
<span class="playable">[可播放]</span>
</div>
</div>
</div>
</li>
....
</ol>
複製程式碼
看看這個程式怎麼寫的:
package main
import (
"log"
"strings"
"github.com/gocolly/colly"
)
func main() {
c := colly.NewCollector(
colly.Async(true),
colly.UserAgent("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"),
)
c.Limit(&colly.LimitRule{DomainGlob: "*.douban.*", Parallelism: 5})
c.OnRequest(func(r *colly.Request) {
log.Println("Visiting", r.URL)
})
c.OnError(func(_ *colly.Response, err error) {
log.Println("Something went wrong:", err)
})
c.OnHTML(".hd", func(e *colly.HTMLElement) {
log.Println(strings.Split(e.ChildAttr("a", "href"), "/")[4],
strings.TrimSpace(e.DOM.Find("span.title").Eq(0).Text()))
})
c.OnHTML(".paginator a", func(e *colly.HTMLElement) {
e.Request.Visit(e.Attr("href"))
})
c.Visit("https://movie.douban.com/top250?start=0&filter=")
c.Wait()
}
複製程式碼
如果你有心執行上面的那個例子,可以感受到抓取時同步的,比較慢。而這次在colly.NewCollector
裡面加了一項colly.Async(true)
,表示抓取時非同步的。在Colly裡面非常方便控制併發度,只抓取符合某個(些)規則的URLS,有一句c.Limit(&colly.LimitRule{DomainGlob: "*.douban.*", Parallelism: 5})
,表示限制只抓取域名是douban(域名字尾和二級域名不限制)的地址,當然還支援正則匹配某些符合的 URLS,具體的可以看官方文件。
另外Limit方法中也限制了併發是5。為什麼要控制併發度呢?因為抓取的瓶頸往往來自對方網站的抓取頻率的限制,如果在一段時間內達到某個抓取頻率很容易被封,所以我們要控制抓取的頻率。另外為了不給對方網站帶來額外的壓力和資源消耗,也應該控制你的抓取機制。
這個例子裡面沒有OnResponse方法,主要是裡面沒有實際的邏輯。但是多用了Wait方法,這是因為在Async為true時需要等待協程都完成再結束。但是呢,有2個OnHTML方法,一個用來確認都訪問那些頁面,另外一個裡面就是抓取條目資訊的邏輯了。也就是這部分:
c.OnHTML(".hd", func(e *colly.HTMLElement) {
log.Println(strings.Split(e.ChildAttr("a", "href"), "/")[4],
strings.TrimSpace(e.DOM.Find("span.title").Eq(0).Text()))
})
複製程式碼
Colly的HTML解析庫用的是goquery,所以寫起來遵循goquery的語法就可以了。ChildAttr方法可以獲得元素對應屬性的值,另外一個沒有列出來的ChildText,用於獲得元素的文字內容。但是我們這個例子中類名為title的span標籤有2個,用ChildText回直接返回2個標籤的全部的值,但是Colly又沒有提供ChildTexts方法(有ChildAttrs),所以只能看原始碼看ChildText實現改成了strings.TrimSpace(e.DOM.Find("span.title").Eq(0).Text())
,這樣就可以拿到第一個符合的文字了。
在Colly中使用XPath
如果你不喜歡goquery這種形式,當然也可以切換HTML解析方案,看我這個例子:
import "github.com/antchfx/htmlquery"
c.OnResponse(func(r *colly.Response) {
doc, err := htmlquery.Parse(strings.NewReader(string(r.Body)))
if err != nil {
log.Fatal(err)
}
nodes := htmlquery.Find(doc, `//ol[@class="grid_view"]/li//div[@class="hd"]`)
for _, node := range nodes {
url := htmlquery.FindOne(node, "./a/@href")
title := htmlquery.FindOne(node, `.//span[@class="title"]/text()`)
log.Println(strings.Split(htmlquery.InnerText(url), "/")[4],
htmlquery.InnerText(title))
}
})
複製程式碼
這次我改在OnResponse方法裡面獲得條目ID和標題。htmlquery.Parse
需要接受一個實現io.Reader介面的物件,所以用了strings.NewReader(string(r.Body))
。其他的程式碼是之前 用Golang寫爬蟲(五) - 使用XPath裡面寫過的,直接拷貝過來就可以了。
後記
試用Colly後就喜歡上了它,你呢?
程式碼地址
本文原文地址: strconv.com/posts/use-c…
完整程式碼可以在這個地址找到。