colly源碼學習 colly 是一個golang寫的網絡爬蟲。它使用起來非常順手。看了一下它的源碼,品質也是非常好的。本文就閱讀一下它的源碼。
使用示例
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/")
}
從Visit開始說起
首先,要做一個爬蟲,我們就需要有一個結構體 Collector, 所有的邏輯都是圍繞這個Collector來進行的。
這個Collector在“爬取”一個URL的時候,我們使用的是Collector.Visit方法。這個Visit方法具體有幾個步驟:
- 組裝Request
- 擷取Response
- Response解析HTML/XML
- 結束頁面抓取
- 在任何一個步驟都有可能出現錯誤
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)
}
}
}
Collector的元件模型
colly的Collector的建立也是很有意思的,我們可以看看它的New方法
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函數中進行定義了。
這個設計模式很适合的是元件化的需求場景,如果一個背景有不同元件,我按需加載這些元件,基本上可以參照這種邏輯:
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的Debugger邏輯
建立完成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流程和回調函數的設計也非常合理。
原文位址https://www.cnblogs.com/yjf512/p/10441678.html