collyソース学習

7972 ワード

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のコンポーネントモデル
    ColllyのCollectorの作成も興味深いので、新しい方法を見てみましょう.
    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
    ", 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関数で、それは1つの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コードを見ると、基本的にコードは非常にはっきりしていて、複雑ではありません.上の3つの点で分かったと思いますが、基本的にこの爬虫類フレームワークのアーキテクチャ設計ははっきりしていて、残りは具体的なコード実装の部分で、ゆっくり見ることができます.
    collyのフレームワーク全体が洗練されていて、くだらない話や過剰な設計はありません.この構造として定義されている場所は構造として定義されています.例えば、Colletor、ここでは複雑なCollectorインタフェースとして設計されていません.しかし、このインタフェースとして定義されている場所、例えばDebuggerは、インタフェースとして定義されています.またcollyも利用者の拡張性を十分に考慮している.いくつかのOnXXXプロセスとコールバック関数の設計も非常に合理的です.