Go 每日一庫之 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
}
並提供了簡單易用的方法:
這些方法會被頻繁地用到。下面我們就通過一些示例來介紹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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ykMv9JmF8vn8dT9Mc4MQYA