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

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

首先安裝它:node

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

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

從最簡單的例子開始

Colly的文檔寫的算是很詳細很完整的了,並且項目下的_examples目錄裏面也有不少爬蟲例子,上手很是容易。先看個人一個例子:github

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屬性值。golang

運行一下:web

❯ 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對象接受多種回調方法,有不一樣的做用,按調用順序我列出來:bash

  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…

完整代碼能夠在這個地址找到。

相關文章
相關標籤/搜索