天天看點

070-并發爬蟲(一)

關于爬蟲(Spider/Crawler),相信你多多少少接觸過。比如你老闆要你從某網站擷取一批企業的黃頁資訊,這時候爬蟲就派上用場了。而本文,我們的任務是編寫一個網頁抓取程式,提取網頁中的 url. 我們的程式可以繼續通路抓取到的 url,深度抓取更多的 url.

1. 連結抓取器

關于這個程式,我就不多廢口舌了,你也不必過多的去研究它,掌握用法即可,我們的目的是學習并發。這個程式位于 gopl/goroutine/link 目錄下。

package link

import (
    "io/ioutil"
    "net/http"
    "regexp"
    "strings"
)

// 從指定的種子位址提取 url
func ExtractLinks(seed string) ([]string, error) {
    var urls []string

    pattern := regexp.MustCompile(`<a\b[^>]+\bhref="([^"]*)"[^>]*>[\s\S]*?</a>`)
    resp, err := http.Get(seed)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    buf, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    res := pattern.FindAllStringSubmatch(string(buf), -1)

    for _, e := range res {
        if len(e) != 2 {
            continue
        }
        if strings.HasPrefix(e[1], "http") {
            urls = append(urls, e[1])
        }
    }
    return urls, nil      

下面是一個簡單的測試示例:

// +build ignore

package main

import (
    "fmt"
    "gopl/goroutine/link"
    "log"
    "os"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage:\n\tgo run main.go <url>")
        os.Exit(1)
    }
    urls, err := link.ExtractLinks(os.Args[1])
    if err != nil {
        log.Print(err)
        return
    }
    for _, url := range urls {
        fmt.Printf("%v\n", url)
    }
}      

接下來運作 ​

​go run main.go 'http://www.baidu.com/s?wd=goroutine'​

​,你的螢幕會列印類似這樣的結果:

070-并發爬蟲(一)

圖1 連結抓取器(隻展示部分結果)

2. 深度連結抓取器

看到圖 1 的結果了吧,上面是我們抓取的 ​​http://www.baidu.com/s?wd=goroutine​​ 這個網頁的連結。但是作為一隻“優秀”的爬蟲,這點結果根本不能滿足,我們希望繼續通路抓取到的連結,然後擷取更多的連結。思路非常簡單:

  • 每個 goroutine 從一個種子 url 提取所有的連結,儲存到任務清單
  • 主協程從任務清單提取已經抓取的 url,并啟動新的協程繼續抓取

下面是這個深度連結抓取器的代碼,在路徑 gopl/goroutine/concurrence 下面:

// crawler01.go
package main

import (
    "fmt"
    "gopl/goroutine/link"
    "log"
    "os"
)

// url 抓取,傳回抓取到的所有 url
func crawl(url string) []string {
    fmt.Println(url)
    urls, err := link.ExtractLinks(url)
    if err != nil {
        log.Print(fmt.Sprintf("\x1b[31m%v\x1b[0m", err))
    }
    return urls
}

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage:\n\tgo run crawl.go <url>")
        os.Exit(1)
    }

    // 任務隊列
    workList := make(chan []string)
    // 記錄該 url 是否已經通路過,防止無限循環
    seen := make(map[string]bool)

    go func() {
        workList <- os.Args[1:]
    }()

    for list := range workList {
        for _, url := range list {
            if seen[url] {
                continue
            }
            seen[url] = true
            go func(url string) {
                workList <- crawl(url)
            }(url)
        }
    }
}      

接下來,運作一下看看,我們把結果重定向到 links.go 檔案中,錯誤會輸出到螢幕上。

$ go run crawler01.go 'http://www.baidu.com/s?wd=goroutine'      

要不了一會兒,你的螢幕應該就會出現:

070-并發爬蟲(一)

圖2 輸出錯誤

這個程式幾乎永遠不會停止,使用 CTRL C 終止它吧。另外我們的 links.txt 檔案也抓取了不少 url 了。

2.1 為什麼出現 too many open files

這是檔案描述符被耗盡了。一般你的系統都有同時打開的最大檔案描述符個數的限制。

070-并發爬蟲(一)

圖3 檔案描述符限制

在我的系統中,這個限制是 7168。而每通路一個 url 都要建立一個 tcp 連結,意味着消耗掉一個描述符,是以這個程式理論上最大可以同時打開 7168 個 url。

但是我們的程式太 TM 并行了,開啟的 goroutine 遠遠超出了這個限制。如何解決?什麼?修改系統最大描述符限制的上限?别開開玩笑了,goroutine 是可以輕松開到上萬的,這樣會把你的系統搞死。

3. 總結

  • 練習連結抓取器
  • 練習并發的深度連結抓取器
  • 為什麼程式會出現 too many open files 的錯誤