Go 爬蟲之 colly 從入門到不放棄指南
最近發現知乎上感興趣的問題越來越少,於是準備聚合下其他平臺技術問答,比如 segmentfault、stackoverflow 等。
要完成這個工作,肯定是離不開爬蟲的。我就順便抽時間研究了 Go 的一款爬蟲框架 colly。
概要介紹
colly 是 Go 實現的比較有名的一款爬蟲框架,而且 Go 在高併發和分佈式場景的優勢也正是爬蟲技術所需要的。它的主要特點是輕量、快速,設計非常優雅,並且分佈式的支持也非常簡單,易於擴展。
如何學習
爬蟲最有名的框架應該就是 Python 的 scrapy,很多人最早接觸的爬蟲框架就是它,我也不例外。它的文檔非常齊全,擴展組件也很豐富。當我們要設計一款爬蟲框架時,常會參考它的設計。之前看到一些文章介紹 Go 中也有類似 scrapy 的實現。
相比而言,colly 的學習資料就少的可憐了。剛看到它的時候,我總會情不自禁想借鑑我的 scrapy 使用經驗,但結果發現這種生搬硬套並不可行。
到此,我們自然地想到去找些文章閱讀,但結果是 colly 相關文章確實有點少,能找到的基本都是官方提供的,而且看起來似乎不是那麼完善。沒辦法,慢慢啃吧!官方的學習資料通常都會有三處,分別是文檔、案例和源碼。
今天,暫時先從官方文檔角度吧!正文開始。
官方文檔
官方文檔 [1] 介紹着重使用方法,如果是有爬蟲經驗的朋友,掃完一遍文檔很快。我花了點時間將官網文檔的按自己的思路整理了一版。
主體內容不多,涉及安裝 [2]、快速開始 [3]、如何配置 [4]、調試 [5]、分佈式爬蟲 [6]、存儲 [7]、運用多收集器 [8]、配置優化 [9]、擴展 [10]。
其中的每篇文檔都很短小,甚至是少的基本都不用翻頁滾動。
如何安裝 [11]
colly 的安裝和其他的 Go 庫安裝一樣簡單。如下:
go get -u github.com/gocolly/colly
一行命令搞定。So easy!
快速開始 [12]
我們來通過一個 hello word 案例快速體驗下 colly 的使用。步驟如下:
第一步,導入 colly。
import"github.com/gocolly/colly"
第二步,創建 collector。
c := colly.NewCollector()
第三步,事件監聽,通過 callback 執行事件處理。
// Find and visit all links
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Attr("href")
// Print link
fmt.Printf("Link found: %q -> %s\n", e.Text, link)
// Visit link found on page
// Only those links are visited which are in AllowedDomains
c.Visit(e.Request.AbsoluteURL(link))
})
c.OnRequest(func(r *colly.Request) {
fmt.Println("Visiting", r.URL)
})
我們順便列舉一下 colly 支持的事件類型,如下:
-
OnRequest 請求執行之前調用
-
OnResponse 響應返回之後調用
-
OnHTML 監聽執行 selector
-
OnXML 監聽執行 selector
-
OnHTMLDetach,取消監聽,參數爲 selector 字符串
-
OnXMLDetach,取消監聽,參數爲 selector 字符串
-
OnScraped,完成抓取後執行,完成所有工作後執行
-
OnError,錯誤回調
最後一步,c.Visit() 正式啓動網頁訪問。
c.Visit("http://go-colly.org/")
案例的完整代碼在 colly 源碼的 _example 目錄下 basic[13] 中提供。
如何配置 [14]
colly 是一款配置靈活的框架,提供了大量的可供開發人員配置的選項。默認情況下,每個選項都提供了較優的默認值。
如下是採用默認創建的 collector。
c := colly.NewCollector()
配置創建的 collector,比如設置 useragent 和允許重複訪問。代碼如下:
c2 := colly.NewCollector(
colly.UserAgent("xy"),
colly.AllowURLRevisit(),
)
我們也可以創建後再改變配置。
c2 := colly.NewCollector()
c2.UserAgent = "xy"
c2.AllowURLRevisit = true
collector 的配置可以在爬蟲執行到任何階段改變。一個經典的例子,通過隨機改變 user-agent,可以幫助我們實現簡單的反爬。
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func RandomString() string {
b := make([]byte, rand.Intn(10)+10)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
returnstring(b)
}
c := colly.NewCollector()
c.OnRequest(func(r *colly.Request) {
r.Headers.Set("User-Agent", RandomString())
})
前面說過,collector 默認已經爲我們選擇了較優的配置,其實它們也可以通過環境變量改變。這樣,我們就可以不用爲了改變配置,每次都得重新編譯了。環境變量配置是在 collector 初始化時生效,正式啓動後,配置是可以被覆蓋的。
支持的配置項,如下:
ALLOWED_DOMAINS (字符串切片),允許的域名,比如 []string{"segmentfault.com", "zhihu.com"}
CACHE_DIR (string) 緩存目錄
DETECT_CHARSET (y/n) 是否檢測響應編碼
DISABLE_COOKIES (y/n) 禁止 cookies
DISALLOWED_DOMAINS (字符串切片),禁止的域名,同 ALLOWED_DOMAINS 類型
IGNORE_ROBOTSTXT (y/n) 是否忽略 ROBOTS 協議
MAX_BODY_SIZE (int) 響應最大
MAX_DEPTH (int - 0 means infinite) 訪問深度
PARSE_HTTP_ERROR_RESPONSE (y/n) 解析 HTTP 響應錯誤
USER_AGENT (string)
它們都是些非常容易理解的選項。
我們再來看看 HTTP 的配置,都是些常用的配置,比如代理、各種超時時間等。
c := colly.NewCollector()
c.WithTransport(&http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second, // 超時時間
KeepAlive: 30 * time.Second, // keepAlive 超時時間
DualStack: true,
}).DialContext,
MaxIdleConns: 100, // 最大空閒連接數
IdleConnTimeout: 90 * time.Second, // 空閒連接超時
TLSHandshakeTimeout: 10 * time.Second, // TLS 握手超時
ExpectContinueTimeout: 1 * time.Second,
}
調試 [15]
在用 scrapy 的時候,它提供了非常好用的 shell 幫助我們非常方便地實現 debug。但非常可惜 colly 中並沒有類似功能,這裏的 debugger 主要是指運行時的信息收集。
debugger 是一個接口,我們只要實現它其中的兩個方法,就可完成運行時信息的收集。
type Debugger interface {
// Init initializes the backend
Init() error
// Event receives a new collector event.
Event(e *Event)
}
源碼中有個典型的案例,LogDebugger[16]。我們只需提供相應的 io.Writer 類型變量,具體如何使用呢?
一個案例,如下:
package main
import (
"log"
"os"
"github.com/gocolly/colly"
"github.com/gocolly/colly/debug"
)
func main() {
writer, err := os.OpenFile("collector.log", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
panic(err)
}
c := colly.NewCollector(colly.Debugger(&debug.LogDebugger{Output: writer}), colly.MaxDepth(2))
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
if err := e.Request.Visit(e.Attr("href")); err != nil {
log.Printf("visit err: %v", err)
}
})
if err := c.Visit("http://go-colly.org/"); err != nil {
panic(err)
}
}
運行完成,打開 collector.log 即可查看輸出內容。
分佈式 [17]
分佈式爬蟲,可以從幾個層面考慮,分別是代理層面、執行層面和存儲層面。
代理層面
通過設置代理池,我們可以將下載任務分配給不同節點執行,有助於提供爬蟲的網頁下載速度。同時,這樣還能有效降低因爬取速度太快而導致 IP 被禁的可能性。
colly 實現代理 IP 的代碼如下:
package main
import (
"github.com/gocolly/colly"
"github.com/gocolly/colly/proxy"
)
func main() {
c := colly.NewCollector()
if p, err := proxy.RoundRobinProxySwitcher(
"socks5://127.0.0.1:1337",
"socks5://127.0.0.1:1338",
"http://127.0.0.1:8080",
); err == nil {
c.SetProxyFunc(p)
}
// ...
}
proxy.RoundRobinProxySwitcher 是 colly 內置的通過輪詢方式實現代理切換的函數。當然,我們也可以完全自定義。
比如,一個代理隨機切換的案例,如下:
var proxies []*url.URL = []*url.URL{
&url.URL{Host: "127.0.0.1:8080"},
&url.URL{Host: "127.0.0.1:8081"},
}
func randomProxySwitcher(_ *http.Request) (*url.URL, error) {
return proxies[random.Intn(len(proxies))], nil
}
// ...
c.SetProxyFunc(randomProxySwitcher)
不過需要注意,此時的爬蟲仍然是中心化的,任務只在一個節點上執行。
執行層面
這種方式通過將任務分配給不同的節點執行,實現真正意義的分佈式。
如果實現分佈式執行,首先需要面對一個問題,如何將任務分配給不同的節點,實現不同任務節點之間的協同工作呢?
首先,我們選擇合適的通信方案。常見的通信協議有 HTTP、TCP,一種無狀態的文本協議、一個是面向連接的協議。除此之外,還可選擇的有種類豐富的 RPC 協議,比如 Jsonrpc、facebook 的 thrift、google 的 grpc 等。
文檔提供了一個 HTTP 服務示例代碼,負責接收請求與任務執行。如下:
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/gocolly/colly"
)
type pageInfo struct {
StatusCode int
Links map[string]int
}
func handler(w http.ResponseWriter, r *http.Request) {
URL := r.URL.Query().Get("url")
if URL == "" {
log.Println("missing URL argument")
return
}
log.Println("visiting", URL)
c := colly.NewCollector()
p := &pageInfo{Links: make(map[string]int)}
// count links
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Request.AbsoluteURL(e.Attr("href"))
if link != "" {
p.Links[link]++
}
})
// extract status code
c.OnResponse(func(r *colly.Response) {
log.Println("response received", r.StatusCode)
p.StatusCode = r.StatusCode
})
c.OnError(func(r *colly.Response, err error) {
log.Println("error:", r.StatusCode, err)
p.StatusCode = r.StatusCode
})
c.Visit(URL)
// dump results
b, err := json.Marshal(p)
if err != nil {
log.Println("failed to serialize response:", err)
return
}
w.Header().Add("Content-Type", "application/json")
w.Write(b)
}
func main() {
// example usage: curl -s 'http://127.0.0.1:7171/?url=http://go-colly.org/'
addr := ":7171"
http.HandleFunc("/", handler)
log.Println("listening on", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}
這裏並沒有提供調度器的代碼,不過實現不算複雜。任務完成後,服務會將相應的鏈接返回給調度器,調度器負責將新的任務發送給工作節點繼續執行。
如果需要根據節點負載情況決定任務執行節點,還需要服務提供監控 API 獲取節點性能數據幫助調度器決策。
存儲層面
我們已經通過將任務分配到不同節點執行實現了分佈式。但部分數據,比如 cookies、訪問的 url 記錄等,在節點之間需要共享。默認情況下,這些數據是保存內存中的,只能是每個 collector 獨享一份數據。
我們可以通過將數據保存至 redis、mongo 等存儲中,實現節點間的數據共享。colly 支持在任何存儲間切換,只要相應存儲實現 colly/storage.Storage[18] 接口中的方法。
其實,colly 已經內置了部分 storage 的實現,查看 storage[19]。下一節也會談到這個話題。
存儲 [20]
前面剛提過這個話題,我們具體看看 colly 已經支持的 storage 有哪些吧。
InMemoryStorage[21],即內存,colly 的默認存儲,我們可以通過 collector.SetStorage() 替換。
RedisStorage[22],或許是因爲 redis 在分佈式場景下使用更多,官方提供了使用案例 [23]。
其他還有 Sqlite3Storage[24] 和 MongoStorage[25]。
多收集器 [26]
我們前面演示的爬蟲都是比較簡單的,處理邏輯都很類似。如果是一個複雜的爬蟲,我們可以通過創建不同的 collector 負責不同任務的處理。
如何理解這段話呢?舉個例子吧。
如果大家寫過一段時間爬蟲,肯定遇到過父子頁面抓取的問題,通常父頁面的處理邏輯與子頁面是不同的,並且通常父子頁面間還有數據共享的需求。用過 scrapy 應該知道,scrapy 通過在 request 綁定回調函數實現不同頁面的邏輯處理,而數據共享是通過在 request 上綁定數據實現將父頁面數據傳遞給子頁面。
研究之後,我們發現 scrapy 的這種方式 colly 並不支持。那該怎麼做?這就是我們要解決的問題。
對於不同頁面的處理邏輯,我們可以定義創建多個收集器,即 collector,不同 collector 負責處理不同的頁面邏輯。
c := colly.NewCollector(
colly.UserAgent("myUserAgent"),
colly.AllowedDomains("foo.com", "bar.com"),
)
// Custom User-Agent and allowed domains are cloned to c2
c2 := c.Clone()
通常情況下,父子頁面的 collector 是相同的。上面的示例中,子頁面的 collector c2 通過 clone,將父級 collector 的配置也都複製了下來。
而父子頁面之間的數據傳遞,可以通過 Context 實現在不同 collector 之間傳遞。注意這個 Context 只是 colly 實現的數據共享的結構,並非 Go 標準庫中的 Context。
c.OnResponse(func(r *colly.Response) {
r.Ctx.Put("Custom-header", r.Headers.Get("Custom-Header"))
c2.Request("GET", "https://foo.com/", nil, r.Ctx, nil)
})
如此一來,我們在子頁面中就可以通過 r.Ctx 獲取到父級傳入的數據了。關於這個場景,我們可以查看官方提供的案例 coursera_courses[27]。
配置優化 [28]
colly 的默認配置針對是少量站點的優化配置。如果你是針對大量站點的抓取,還需要一些改進。
持久化存儲
默認情況下,colly 中的 cookies 和 url 是保存在內存中,我們要換成可持久化的存儲。前面介紹過,colly 已經實現一些常用的可持久化的存儲組件。
啓用異步加快任務執行
colly 默認會阻塞等待請求執行完成,這將會導致等待執行任務數越來越大。我們可以通過設置 collector 的 Async 選項爲 true 實現異步處理,從而避免這個問題。如果採用這種方式,記住增加 c.Wait(),否則程序會立刻退出。
禁止或限制 KeepAlive 連接
colly 默認開啓 KeepAlive 增加爬蟲的抓取速度。但是,這對打開的文件描述符有要求,對於長時間運行的任務,進程非常容易就能達到最大描述符的限制。
禁止 HTTP 的 KeepAlive 的示例代碼,如下。
c := colly.NewCollector()
c.WithTransport(&http.Transport{
DisableKeepAlives: true,
})
擴展 [29]
colly 提供了一些擴展,主要與爬蟲相關的常用功能,如 referer、random_user_agent、url_length_filter 等。源碼路徑在 colly/extensions/[30] 下。
通過一個示例瞭解它們的使用方法,如下:
import (
"log"
"github.com/gocolly/colly"
"github.com/gocolly/colly/extensions"
)
func main() {
c := colly.NewCollector()
visited := false
extensions.RandomUserAgent(c)
extensions.Referrer(c)
c.OnResponse(func(r *colly.Response) {
log.Println(string(r.Body))
if !visited {
visited = true
r.Request.Visit("/get?q=2")
}
})
c.Visit("http://httpbin.org/get")
}
只需將 collector 傳入擴展函數中即可。這麼簡單就搞定了啊。
那麼,我們能不能自己實現一個擴展呢?
在使用 scrapy 的時候,我們如果要實現一個擴展需要提前瞭解不少概念,仔細閱讀它的文檔。但 colly 在文檔中壓根也並沒有相關說明啊。腫麼辦呢?看樣子只能看源碼了。
我們打開 referer 插件的源碼,如下:
package extensions
import (
"github.com/gocolly/colly"
)
// Referer sets valid Referer HTTP header to requests.
// Warning: this extension works only if you use Request.Visit
// from callbacks instead of Collector.Visit.
func Referer(c *colly.Collector) {
c.OnResponse(func(r *colly.Response) {
r.Ctx.Put("_referer", r.Request.URL.String())
})
c.OnRequest(func(r *colly.Request) {
if ref := r.Ctx.Get("_referer"); ref != "" {
r.Headers.Set("Referer", ref)
}
})
}
在 collector 上增加一些事件回調就實現一個擴展。這麼簡單的源碼,完全不用文檔說明就可以實現一個自己的擴展了。 當然,如果仔細觀察,我們會發現,其實它的思路和 scrapy 是類似的,都是通過擴展 request 和 response 的回調實現,而 colly 之所以如此簡潔主要得益於它優雅的設計和 Go 簡單的語法。
總結
讀完 colly 的官方文檔會發現,雖然它的文檔簡陋無比,但應該介紹的內容基本上都涉及到了。如果有部分未涉及的內容,我也在本文之中做了相關的補充。之前在使用 Go 的 elastic[31] 包時,同樣也是文檔少的可憐,但簡單讀下源碼,就能立刻明白了該如何去使用它。
或許這就是 Go 的大道至簡吧。
最後,如果大家在使用 colly 時遇到什麼問題,官方的 example 絕對是最佳實踐,建議可以抽時間一讀。
補充說明
平時我除了寫技術博客,還經常在各問答平臺回答技術問題,這些內容我都會聚合到自己的微信公衆號中。
有興趣的朋友可掃碼關注!
參考資料
[1]
官方文檔: http://go-colly.org/docs/
[2]
安裝: http://go-colly.org/docs/introduction/install/
[3]
快速開始: http://go-colly.org/docs/introduction/start/
[4]
如何配置: http://go-colly.org/docs/introduction/configuration/
[5]
調試: http://go-colly.org/docs/introduction/configuration/
[6]
分佈式爬蟲: http://go-colly.org/docs/best_practices/distributed/
[7]
存儲: http://go-colly.org/docs/best_practices/storage/
[8]
運用多收集器: http://go-colly.org/docs/best_practices/multi_collector/
[9]
配置優化: http://go-colly.org/docs/best_practices/crawling/
[10]
擴展: http://go-colly.org/docs/best_practices/extensions/
[11]
如何安裝: http://go-colly.org/docs/introduction/install/
[12]
快速開始: http://go-colly.org/docs/introduction/start/
[13]
basic: _https://github.com/gocolly/colly/blob/master/examples/basic/basic.go
[14]
如何配置: http://go-colly.org/docs/introduction/configuration/
[15]
調試: http://go-colly.org/docs/best_practices/debugging/
[16]
LogDebugger: https://github.com/gocolly/colly/blob/master/debug/logdebugger.go
[17]
分佈式: http://go-colly.org/docs/best_practices/distributed/
[18]
colly/storage.Storage: https://godoc.org/github.com/gocolly/colly/storage#Storage
[19]
storage: http://go-colly.org/docs/best_practices/storage/
[20]
存儲: http://go-colly.org/docs/best_practices/storage/
[21]
InMemoryStorage: https://github.com/gocolly/colly/blob/master/storage/storage.go
[22]
RedisStorage: https://github.com/gocolly/redisstorage
[23]
使用案例: http://go-colly.org/docs/examples/redis_backend/
[24]
Sqlite3Storage: https://github.com/velebak/colly-sqlite3-storage
[25]
MongoStorage: https://github.com/zolamk/colly-mongo-storage
[26]
多收集器: http://go-colly.org/docs/best_practices/multi_collector/
[27]
coursera_courses: http://go-colly.org/docs/examples/coursera_courses/
[28]
配置優化: http://go-colly.org/docs/best_practices/crawling/
[29]
擴展: http://go-colly.org/docs/best_practices/extensions/
[30]
colly/extensions/: https://github.com/gocolly/colly/tree/master/extensions
[31]
elastic: https://github.com/olivere/elastic
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Rdzpn3pEswRz3aGox4zRsg