Golang: 併發抓取網頁內容

在上一篇中,咱們根據命令行的 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

從結果中能夠看出,最後消耗的總時間與耗時最長的那個操做等同,可見併發在性能方面帶來的提高是很是可觀的。命令行

相關文章
相關標籤/搜索