在上一篇中,咱們根據命令行的 URL 參數輸入,抓取對應的網頁內容並保存到本地磁盤,今天來記錄一下如何利用併發,來抓取多個站點的網頁內容。正則表達式
首先,咱們在上一次代碼的基礎上稍做改造,使它可以獲取多個站點的內容。下面代碼中,咱們首先定義好三個 URL,而後逐個發送網絡請求,獲取數據並保存,最後統計消耗的總時間:bash
// fetch.go package main import ( "os" "fmt" "time" "regexp" "net/http" "io/ioutil" ) // 建立正則常量 var RE = regexp.MustCompile("\\w+\\.\\w+$") func main() { urls := []string { "http://www.qq.com", "http://www.163.com", "http://www.sina.com", } // 開始時間 start := time.Now() for _, url := range urls { start := time.Now() // 發送網絡請求 res, err := http.Get(url) if err != nil { fmt.Fprintf(os.Stderr, "fetch: %v\n", err) os.Exit(1) } // 讀取資源數據 body, err := ioutil.ReadAll(res.Body) // 關閉資源 res.Body.Close() if err != nil { fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err) os.Exit(1) } fileName := getFileName(url) // 寫入文件 ioutil.WriteFile(fileName, body, 0644) // 消耗的時間 elapsed := time.Since(start).Seconds() fmt.Printf("%.2fs %s\n", elapsed, fileName) } // 消耗的時間 elapsed := time.Since(start).Seconds() fmt.Printf("%.2fs elapsed\n", elapsed) } // 獲取文件名 func getFileName(url string) string { // 從URL中匹配域名後面部分 return RE.FindString(url) + ".txt" }
在上面代碼中,咱們使用正則表達式來從 URL 中匹配域名後面部分,做爲最終的文件名。關於正則表達式,後續會作總結。網絡
下面來看看程序運行後的控制檯信息:併發
$ ./fetch 0.12s qq.com.txt 0.20s 163.com.txt 0.27s sina.com.txt 0.59s elapsed
從打印信息中能夠看出,最後消耗的總時間等於三次執行的總和。這種方式效率低下,而且不能充分利用計算機資源,下面咱們就對程序進行改造,使其可以併發地執行三個抓取操做:性能
// fetch.go package main import ( "os" "fmt" "time" "regexp" "net/http" "io/ioutil" ) // 建立正則 var RE = regexp.MustCompile("\\w+\\.\\w+$") func main() { urls := []string { "http://www.qq.com", "http://www.163.com", "http://www.sina.com", } // 建立channel ch := make(chan string) // 開始時間 start := time.Now() for _, url := range urls { // 開啓一個goroutine go fetch(url, ch) } for range urls { // 打印channel中的信息 fmt.Println(<-ch) } // 總消耗的時間 elapsed := time.Since(start).Seconds() fmt.Printf("%.2fs elapsed\n", elapsed) } // 根據URL獲取資源內容 func fetch(url string, ch chan<- string) { start := time.Now() // 發送網絡請求 res, err := http.Get(url) if err != nil { // 輸出異常信息 ch <- fmt.Sprint(err) os.Exit(1) } // 讀取資源數據 body, err := ioutil.ReadAll(res.Body) // 關閉資源 res.Body.Close() if err != nil { // 輸出異常信息 ch <- fmt.Sprintf("while reading %s: %v", url, err) os.Exit(1) } // 寫入文件 ioutil.WriteFile(getFileName(url), body, 0644) // 消耗的時間 elapsed := time.Since(start).Seconds() // 輸出單個URL消耗的時間 ch <- fmt.Sprintf("%.2fs %s", elapsed, url) } // 獲取文件名 func getFileName(url string) string { // 從URL中匹配域名部分 return RE.FindString(url) + ".txt" }
上面代碼中,咱們先建立一個 channel,而後對每一個抓取操做開啓一個 goroutine,待抓取程序完成後,經過 channel 發送消息告知主線程,主線程再作相應的處理操做。關於這部分的原理細節,後續再作總結。fetch
咱們運行上面的程序,執行結果以下:url
$ ./fetch 0.10s http://www.qq.com 0.19s http://www.163.com 0.29s http://www.sina.com 0.29s elapsed
從結果中能夠看出,最後消耗的總時間與耗時最長的那個操做等同,可見併發在性能方面帶來的提高是很是可觀的。命令行