關于爬蟲(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'
,你的螢幕會列印類似這樣的結果:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5CMzQTN5AzMzYGNjJjYzgDMzYzXwAzNxETM2IzLchDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
圖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'
要不了一會兒,你的螢幕應該就會出現:
圖2 輸出錯誤
這個程式幾乎永遠不會停止,使用 CTRL C 終止它吧。另外我們的 links.txt 檔案也抓取了不少 url 了。
2.1 為什麼出現 too many open files
這是檔案描述符被耗盡了。一般你的系統都有同時打開的最大檔案描述符個數的限制。
圖3 檔案描述符限制
在我的系統中,這個限制是 7168。而每通路一個 url 都要建立一個 tcp 連結,意味着消耗掉一個描述符,是以這個程式理論上最大可以同時打開 7168 個 url。
但是我們的程式太 TM 并行了,開啟的 goroutine 遠遠超出了這個限制。如何解決?什麼?修改系統最大描述符限制的上限?别開開玩笑了,goroutine 是可以輕松開到上萬的,這樣會把你的系統搞死。
3. 總結
- 練習連結抓取器
- 練習并發的深度連結抓取器
- 為什麼程式會出現 too many open files 的錯誤