用Golang寫爬蟲(六) - 使用colly

Golang程式設計發表於2019-07-18

Colly是Golang世界最知名的Web爬蟲框架了,它的API清晰明瞭,高度可配置和可擴充套件,支援分散式抓取,還支援多種儲存後端(如記憶體、Redis、MongoDB等)。這篇文章記錄我學習使用它的的一些感受和理解。

首先安裝它:

❯ go get -u github.com/gocolly/colly/...
複製程式碼

這個go get和之前安裝包不太一樣,最後有...這樣的省略號,它的意思是也獲取這個包的子包和依賴。

從最簡單的例子開始

Colly的文件寫的算是很詳細很完整的了,而且專案下的_examples目錄裡面也有很多爬蟲例子,上手非常容易。先看我的一個例子:

package main

import (
	"fmt"

	"github.com/gocolly/colly"
)

func main() {
	c := colly.NewCollector(
		colly.UserAgent("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"),
	)

	c.OnRequest(func(r *colly.Request) {
		fmt.Println("Visiting", r.URL)
	})

	c.OnError(func(_ *colly.Response, err error) {
		fmt.Println("Something went wrong:", err)
	})

	c.OnResponse(func(r *colly.Response) {
		fmt.Println("Visited", r.Request.URL)
	})

	c.OnHTML(".paginator a", func(e *colly.HTMLElement) {
		e.Request.Visit(e.Attr("href"))
	})

    c.OnScraped(func(r *colly.Response) {
        fmt.Println("Finished", r.Request.URL)
    })

	c.Visit("https://movie.douban.com/top250?start=0&filter=")
}
複製程式碼

這個程式就是去找豆瓣電影Top250的全部連結,如OnHTML方法的第一個函式所描述,找類名是paginator的標籤下的a標籤的href屬性值。

執行一下:

❯ go run colly/doubanCrawler1.go
Visiting https://movie.douban.com/top250?start=0&filter=
Visited https://movie.douban.com/top250?start=0&filter=
Visiting https://movie.douban.com/top250?start=25&filter=
Visited https://movie.douban.com/top250?start=25&filter=
...
Finished https://movie.douban.com/top250?start=25&filter=
Finished https://movie.douban.com/top250?start=0&filter=
複製程式碼

在Colly中主要實體就是一個Collector物件(用colly.NewCollector建立),Collector管理網路通訊和對於響應的回撥執行。Collector在初始化時可以接受多種設定項,例如這個例子裡面我就設定了UserAgent的值。其他的設定項可以去看官方網站。

Collector物件接受多種回撥方法,有不同的作用,按呼叫順序我列出來:

  1. OnRequest。請求前
  2. OnError。請求過程中發生錯誤
  3. OnResponse。收到響應後
  4. OnHTML。如果收到的響應內容是HTML呼叫它。
  5. OnXML。如果收到的響應內容是XML 呼叫它。寫爬蟲基本用不到,所以上面我沒有使用它。
  6. OnScraped。在OnXML/OnHTML回撥完成後呼叫。不過官網寫的是Called after OnXML callbacks,實際上對於OnHTML也有效,大家可以注意一下。

抓取條目ID和標題

還是之前的需求,先看看豆瓣Top250頁面每個條目的部分HTML程式碼:

<ol class="grid_view">
  <li>
    <div class="item">
      <div class="info">
        <div class="hd">
          <a href="https://movie.douban.com/subject/1292052/" class="">
            <span class="title">肖申克的救贖</span>
            <span class="title">&nbsp;/&nbsp;The Shawshank Redemption</span>
            <span class="other">&nbsp;/&nbsp;月黑高飛(港)  /  刺激 1995(臺)</span>
          </a>
          <span class="playable">[可播放]</span>
        </div>
      </div>
    </div>
  </li>
  ....
</ol>
複製程式碼

看看這個程式怎麼寫的:

package main

import (
	"log"
	"strings"

	"github.com/gocolly/colly"
)

func main() {
	c := colly.NewCollector(
		colly.Async(true),
		colly.UserAgent("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"),
	)

	c.Limit(&colly.LimitRule{DomainGlob:  "*.douban.*", Parallelism: 5})

	c.OnRequest(func(r *colly.Request) {
		log.Println("Visiting", r.URL)
	})

	c.OnError(func(_ *colly.Response, err error) {
		log.Println("Something went wrong:", err)
	})

	c.OnHTML(".hd", func(e *colly.HTMLElement) {
		log.Println(strings.Split(e.ChildAttr("a", "href"), "/")[4],
			strings.TrimSpace(e.DOM.Find("span.title").Eq(0).Text()))
    })

	c.OnHTML(".paginator a", func(e *colly.HTMLElement) {
		e.Request.Visit(e.Attr("href"))
	})

	c.Visit("https://movie.douban.com/top250?start=0&filter=")
	c.Wait()
}
複製程式碼

如果你有心執行上面的那個例子,可以感受到抓取時同步的,比較慢。而這次在colly.NewCollector裡面加了一項colly.Async(true),表示抓取時非同步的。在Colly裡面非常方便控制併發度,只抓取符合某個(些)規則的URLS,有一句c.Limit(&colly.LimitRule{DomainGlob: "*.douban.*", Parallelism: 5}),表示限制只抓取域名是douban(域名字尾和二級域名不限制)的地址,當然還支援正則匹配某些符合的 URLS,具體的可以看官方文件。

另外Limit方法中也限制了併發是5。為什麼要控制併發度呢?因為抓取的瓶頸往往來自對方網站的抓取頻率的限制,如果在一段時間內達到某個抓取頻率很容易被封,所以我們要控制抓取的頻率。另外為了不給對方網站帶來額外的壓力和資源消耗,也應該控制你的抓取機制。

這個例子裡面沒有OnResponse方法,主要是裡面沒有實際的邏輯。但是多用了Wait方法,這是因為在Async為true時需要等待協程都完成再結束。但是呢,有2個OnHTML方法,一個用來確認都訪問那些頁面,另外一個裡面就是抓取條目資訊的邏輯了。也就是這部分:

c.OnHTML(".hd", func(e *colly.HTMLElement) {
    log.Println(strings.Split(e.ChildAttr("a", "href"), "/")[4],
        strings.TrimSpace(e.DOM.Find("span.title").Eq(0).Text()))
})
複製程式碼

Colly的HTML解析庫用的是goquery,所以寫起來遵循goquery的語法就可以了。ChildAttr方法可以獲得元素對應屬性的值,另外一個沒有列出來的ChildText,用於獲得元素的文字內容。但是我們這個例子中類名為title的span標籤有2個,用ChildText回直接返回2個標籤的全部的值,但是Colly又沒有提供ChildTexts方法(有ChildAttrs),所以只能看原始碼看ChildText實現改成了strings.TrimSpace(e.DOM.Find("span.title").Eq(0).Text()),這樣就可以拿到第一個符合的文字了。

在Colly中使用XPath

如果你不喜歡goquery這種形式,當然也可以切換HTML解析方案,看我這個例子:

import "github.com/antchfx/htmlquery"

c.OnResponse(func(r *colly.Response) {
    doc, err := htmlquery.Parse(strings.NewReader(string(r.Body)))
    if err != nil {
        log.Fatal(err)
    }
    nodes := htmlquery.Find(doc, `//ol[@class="grid_view"]/li//div[@class="hd"]`)
    for _, node := range nodes {
        url := htmlquery.FindOne(node, "./a/@href")
        title := htmlquery.FindOne(node, `.//span[@class="title"]/text()`)
        log.Println(strings.Split(htmlquery.InnerText(url), "/")[4],
            htmlquery.InnerText(title))
    }
})
複製程式碼

這次我改在OnResponse方法裡面獲得條目ID和標題。htmlquery.Parse需要接受一個實現io.Reader介面的物件,所以用了strings.NewReader(string(r.Body))。其他的程式碼是之前 用Golang寫爬蟲(五) - 使用XPath裡面寫過的,直接拷貝過來就可以了。

後記

試用Colly後就喜歡上了它,你呢?

程式碼地址

本文原文地址: strconv.com/posts/use-c…

完整程式碼可以在這個地址找到。

相關文章