colly是一個golang寫的網絡爬蟲。它使用起來很是順手。看了一下它的源碼,質量也是很是好的。本文就閱讀一下它的源碼。html
func main() { c := colly.NewCollector() // Find and visit all links c.OnHTML("a[href]", func(e *colly.HTMLElement) { e.Request.Visit(e.Attr("href")) }) c.OnRequest(func(r *colly.Request) { fmt.Println("Visiting", r.URL) }) c.Visit("http://go-colly.org/") }
首先,要作一個爬蟲,咱們就須要有一個結構體 Collector, 全部的邏輯都是圍繞這個Collector來進行的。golang
這個Collector在「爬取」一個URL的時候,咱們使用的是Collector.Visit方法。這個Visit方法具體有幾個步驟:web
colly能讓你在每一個步驟制定你須要執行的邏輯,並且這個邏輯不必定要是單個,能夠是多個。好比你能夠在Response獲取完成,解析爲HTML以後使用OnHtml增長邏輯。這個也是咱們最常使用的函數。它的實現原理以下:設計模式
type HTMLCallback func(*HTMLElement) type htmlCallbackContainer struct { Selector string Function HTMLCallback } type Collector struct { ... htmlCallbacks []*htmlCallbackContainer // 這個htmlCallbacks就是用戶註冊的HTML回調邏輯地址 ... } // 用戶使用的註冊函數,註冊的是一個htmlCallbackContainer,裏面包含了DOM選擇器,和選擇後的回調方法 func (c *Collector) OnHTML(goquerySelector string, f HTMLCallback) { ... if c.htmlCallbacks == nil { c.htmlCallbacks = make([]*htmlCallbackContainer, 0, 4) } c.htmlCallbacks = append(c.htmlCallbacks, &htmlCallbackContainer{ Selector: goquerySelector, Function: f, }) ... } // 系統在獲取HTML的DOM以後作的操做,將htmlCallbacks拆解出來一個個調用函數 func (c *Collector) handleOnHTML(resp *Response) error { ... doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(resp.Body)) ... for _, cc := range c.htmlCallbacks { i := 0 doc.Find(cc.Selector).Each(func(_ int, s *goquery.Selection) { for _, n := range s.Nodes { e := NewHTMLElementFromSelectionNode(resp, s, n, i) ... cc.Function(e) } }) } return nil } // 這個是Visit的主流程,在合適的地方增長handleOnHTML的邏輯。 func (c *Collector) fetch(u, method string, depth int, requestData io.Reader, ctx *Context, hdr http.Header, req *http.Request) error { ... err = c.handleOnHTML(response) ... return err }
總體這個代碼的模式我以爲是很巧妙的,簡要來講就是在結構體中存儲回調函數,回調函數的註冊用OnXXX開放出去,內部在合適的地方進行回調函數的嵌套執行。數組
這個代碼模式能夠徹底記住,適合的場景是有注入邏輯的需求,能夠增長類庫的擴展性。網絡
好比咱們設計一個ORM,想在Save或者Update的時候能夠注入一些邏輯,使用這個代碼模式大體就是這樣邏輯:架構
// 這種模型適合流式,而後每一個步驟進行設計 type SaveCallback func(*Resource) type UpdateCallback func(string, *Resource) type UpdateCallbackContainer struct { Id string Function UpdateCallback } type Resource struct { Id string saveCallbacks []SaveCallback updateCallbacks []*UpdateCallbackContainer } func (r *Resource) OnSave(f SaveCallback) { if r.saveCallbacks == nil { r.saveCallbacks = make([]SaveCallback, 0, 4) } r.saveCallbacks = append(r.saveCallbacks, f) } func (r *Resource) Save() { // Do Something if r.saveCallbacks != nil { for _, f := range r.saveCallbacks { f(r) } } } func (r *Resource) OnUpdate(id string, f UpdateCallback) { if r.updateCallbacks == nil { r.updateCallbacks = make([]*UpdateCallbackContainer, 0, 4) } r.updateCallbacks = append(r.updateCallbacks, &UpdateCallbackContainer{ id, f}) } func (r *Resource) Update() { // Do something id := r.Id if r.updateCallbacks != nil { for _, c := range r.updateCallbacks { c.Function(id, r) } } }
colly的Collector的建立也是頗有意思的,咱們能夠看看它的New方法app
func NewCollector(options ...func(*Collector)) *Collector { c := &Collector{} c.Init() for _, f := range options { f(c) } ... return c } func UserAgent(ua string) func(*Collector) { return func(c *Collector) { c.UserAgent = ua } } func main() { c := NewCollector( colly.UserAgent("Chrome") ) }
參數是一個返回函數func(*Collector)的可變數組。而後它的組件就能夠以參數的形式在New函數中進行定義了。框架
這個設計模式很適合的是組件化的需求場景,若是一個後臺有不一樣組件,我按需加載這些組件,基本上能夠參照這種邏輯:ide
type Admin struct { SideBar string } func NewAdmin(options ...func(*Admin)) *Admin { ad := &Admin{} for _, f := range options { f(ad) } return ad } func SideBar(sidebar string) func(*Admin) { return func(admin *Admin) { admin.SideBar = sidebar } }
建立完成Collector,可是在各類地方是須要進行「調試」的,這裏的調試colly設計爲能夠是日誌記錄,也能夠是開啓一個web進行實時顯示。
這個是怎麼作到的呢?也是很是巧妙的使用了事件模型。
基本上核心代碼以下:
package admin import ( "io" "log" ) type Event struct { Type string RequestID int Message string } type Debugger interface { Init() error Event(*Event) } type LogDebugger struct { Output io.Writer logger *log.Logger } func (l *LogDebugger) Init() error { l.logger = log.New(l.Output, "", 1) return nil } func (l *LogDebugger) Event(e *Event) { l.logger.Printf("[%6d - %s] %q\n", e.RequestID, e.Type, e.Message) } func createEvent( requestID, collectorID uint32) *debug.Event { return &debug.Event{ RequestID: requestID, Type: eventType, } } c.debugger.Event(createEvent("request", r.ID, c.ID, map[string]string{ "url": r.URL.String(), }))
設計了一個Debugger的接口,裏面的Init其實能夠根據須要是否存在,最核心的是一個Event函數,它接收一個Event結構指針,全部調試信息相關的調試類型,調試請求ID,調試信息等均可以存在這個Event裏面。
在須要記錄的地方,建立一個Event事件,而且經過debugger進行輸出到調試器中。
colly的debugger還有個驚喜,它支持web方式的查看,咱們查看裏面的debug/webdebugger.go
type WebDebugger struct { Address string initialized bool CurrentRequests map[uint32]requestInfo RequestLog []requestInfo } type requestInfo struct { URL string Started time.Time Duration time.Duration ResponseStatus string ID uint32 CollectorID uint32 } func (w *WebDebugger) Init() error { ... if w.Address == "" { w.Address = "127.0.0.1:7676" } w.RequestLog = make([]requestInfo, 0) w.CurrentRequests = make(map[uint32]requestInfo) http.HandleFunc("/", w.indexHandler) http.HandleFunc("/status", w.statusHandler) log.Println("Starting debug webserver on", w.Address) go http.ListenAndServe(w.Address, nil) return nil } func (w *WebDebugger) Event(e *Event) { switch e.Type { case "request": w.CurrentRequests[e.RequestID] = requestInfo{ URL: e.Values["url"], Started: time.Now(), ID: e.RequestID, CollectorID: e.CollectorID, } case "response", "error": r := w.CurrentRequests[e.RequestID] r.Duration = time.Since(r.Started) r.ResponseStatus = e.Values["status"] w.RequestLog = append(w.RequestLog, r) delete(w.CurrentRequests, e.RequestID) } }
看到沒,重點是經過Init函數把http server啓動起來,而後經過Event收集當前信息,而後經過某個路由handler再展現在web上。
這個設計比其餘的各類Logger的設計感受又優秀了一點。
看下來colly代碼,基本上代碼仍是很是清晰,不復雜的。我以爲上面三個地方看明白了,基本上這個爬蟲框架的架構設計就很清晰了,剩下的是具體的代碼實現的部分,能夠慢慢看。
colly的整個框架給個人感受是很乾練,沒有什麼廢話和過分設計,該定義爲結構的地方就定義爲結構了,好比Colletor,這裏它並無設計爲很複雜的Collector接口啥的。可是在該定義爲接口的地方,好比Debugger,就定義爲了接口。並且colly也充分考慮了使用者的擴展性。幾個OnXXX流程和回調函數的設計也很是合理。