Go 爬蟲之 HTTP 請求 QuickStart

前幾天在 "知乎想法" 談到了一個話題,如何模仿學習,舉了經過 net/http client 模仿 Pyhton 的requests的例子。但並未實踐,難道想法真的只能是想法嗎?固然不是,因而我決定先暫停一週 GO 筆記,來實踐下本身的想法。html

有些新的知識,咱們能夠經過模仿學習git

本文將經過 GO 實現 requests 的 quick start 文檔中的全部例子,系統學習http client的使用。雖然標題是 quick start,但其實內容挺多的。github

快速體驗

首先,咱們來發起一個 GET 請求,代碼很是簡單。以下:golang

func get() {
	r, err := http.Get("https://api.github.com/events")
	if err != nil {
		panic(err)
	}
	defer func() { _ = r.Body.Close() }()

	body, _ := ioutil.ReadAll(r.Body)
	fmt.Printf("%s", body)
}
複製代碼

經過 http.Get 方法,獲取到了一個 Response 和一個 error ,即 r 和 err。經過 r 咱們能獲取響應的信息,err 能夠實現錯誤檢查。json

r.Body 被讀取後須要關閉,能夠defer來作這件事。內容的讀取可經過 ioutil.ReadAll實現。api

請求方法

除了GET,HTTP還有其餘一系列方法,包括POST、PUT、DELETE、HEAD、OPTIONS。快速體驗中的GET是經過一種便捷的方式實現的,它隱藏了不少細節。這裏暫時先不用它。數組

咱們先來介紹通用的方法,以幫咱們實現全部HTTP方法的請求。主要涉及兩個重要的類型,Client 和 Request。bash

Client 便是發送 HTTP 請求的客戶端,請求的執行都是由 Client 發起。它提供了一些便利的請求方法,好比咱們要發起一個Get請求,可經過 client.Get(url) 實現。更通用的方式是經過 client.Do(req) 實現,req 屬於 Request 類型。微信

Request 是用來描述請求信息的結構體,好比請求方法、地址、頭部等信息,咱們均可以經過它來設置。Request 的建立能夠經過 http.NewRequest 實現。cookie

接下來列舉 HTTP 全部方法的實現代碼。

GET

r, err := http.DefaultClient.Do(
	http.NewRequest(http.MethodGet, "https://api.github.com/events", nil))
複製代碼

POST

r, err := http.DefaultClient.Do(
	http.NewRequest(http.MethodPost, "http://httpbin.org/post", nil))
複製代碼

PUT

r, err := http.DefaultClient.Do(
	http.NewRequest(http.MethodPut, "http://httpbin.org/put", nil))
複製代碼

DELETE

r, err := http.DefaultClient.Do(
	http.NewRequest(http.MethodDelete, "http://httpbin.org/delete", nil))
複製代碼

HEAD

r, err := http.DefaultClient.Do(
	http.NewRequest(http.MethodHead, "http://httpbin.org/get", nil))
複製代碼

OPTIONS

r, err := http.DefaultClient.Do(
	http.NewRequest(http.MethodOptions, "http://httpbin.org/get", nil))
複製代碼

上面展現了HTTP全部方法的實現。這裏還幾點須要說明。

DefaultClient,它是 net/http 包提供了默認客戶端,通常的請求咱們無需建立新的 Client,使用默認便可。

GET、POST 和 HEAD 的請求,GO提供了更便捷的實現方式,Request 不用手動建立。

示例代碼,每一個 HTTP 請求方法都有兩種實現。

GET

r, err := http.DefaultClient.Get("http://httpbin.org/get")
r, err := http.Get("http://httpbin.org/get")
複製代碼

POST

bodyJson, _ := json.Marshal(map[string]interface{}{
	"key": "value",
})
r, err := http.DefaultClient.Post(
	"http://httpbin.org/post",
	"application/json",
	strings.NewReader(string(bodyJson)),
)
r, err := http.Post(
	"http://httpbin.org/post",
	"application/json",
	strings.NewReader(string(bodyJson)),
)
複製代碼

這裏順便演示瞭如何向 POST 接口提交 JSON 數據的方式,主要 content-type 的設置,通常JSON接口的 content-type 爲 application/json。

HEAD

r, err := http.DefaultClient.Head("http://httpbin.org/get")
r, err := http.Head("http://httpbin.org/get")
複製代碼

若是看了源碼,你會發現,http.Get 中調用就是 http.DefaultClient.Get,是同一個意思,只是爲了方便,提供這種調用方法。Head 和 Post 也是如此。

URL參數

經過將鍵/值對置於 URL 中,咱們能夠實現向特定地址傳遞數據。該鍵/值將跟在一個問號的後面,例如 httpbin.org/get?key=val… 手工構建 URL 會比較麻煩,咱們能夠經過 net/http 提供的方法來實現。

舉個栗子,好比你想傳遞 key1=value1 和 key2=value2 到 httpbin.org/get。代碼以下:

req, err := http.NewRequest(http.MethodGet, "http://httpbin.org/get", nil)
if err != nil {
	panic(err)
}

params := make(url.Values)
params.Add("key1", "value1")
params.Add("key2", "value2")

req.URL.RawQuery = params.Encode()

// URL 的具體狀況 http://httpbin.org/get?key1=value1&key2=value2
// fmt.Println(req.URL.String()) 

r, err := http.DefaultClient.Do(req)
複製代碼

url.Values 能夠幫助組織 QueryString,查看源碼發現 url.Values 實際上是 map[string][]string。調用 Encode 方法,將組織的字符串傳遞給請求 req 的 RawQuery。經過 url.Values也能夠設置一個數組參數,相似以下的形式:

httpbin.org/get?key1=va…

怎麼作呢?

params := make(url.Values)
params.Add("key1", "value1")
params.Add("key2", "value2")
params.Add("key2", "value3")
複製代碼

觀察最後一行代碼。其實,只要在 key2 上再增長一個值就能夠了。

響應信息

執行請求成功,如何查看響應信息。要查看響應信息,能夠大概瞭解下,響應一般哪些內容?常見的有主體內容(Body)、狀態信息(Status)、響應頭部(Header)、內容編碼(Encoding)等。

Body

其實,在最開始的時候已經演示Body讀取的過程。響應內容的讀取可經過 ioutil 實現。

body, err := ioutil.ReadAll(r.Body)
複製代碼

響應內容多樣,若是是 json,能夠直接使用 json.Unmarshal 進行解碼,JSON知識不介紹了。

r.Body 實現了 io.ReadeCloser 接口,爲減小資源浪費要及時釋放,能夠經過 defer 實現。

defer func() { _ = r.Body.Close() }()
複製代碼

StatusCode

響應信息中,除了 Body 主體內容,還有其餘信息,好比 status code 和 charset 等。

r.StatusCode
r.Status
複製代碼

r.StatusCode 是 HTTP 返回碼,Status 是返回狀態描述。

Header

響應頭信息經過 Response.Header 便可獲取,要說明的一點是,響應頭的 Key 是不區分大小寫。

r.Header.Get("content-type")
r.Header.Get("Content-Type")
複製代碼

你會發現 content-type 和 Content-Type 獲取的內容是徹底同樣的。

Encoding

如何識別響應內容編碼呢?咱們須要藉助 golang.org/x/net/html/… 包實現。先來定義一個函數,代碼以下:

func determineEncoding(r *bufio.Reader) encoding.Encoding {
	bytes, err := r.Peek(1024)
	if err != nil {
		fmt.Printf("err %v", err)
		return unicode.UTF8
	}

	e, _, _ := charset.DetermineEncoding(bytes, "")

	return e
}
複製代碼

怎麼調用它?

bodyReader := bufio.NewReader(r.Body)
e := determineEncoding(bodyReader)
fmt.Printf("Encoding %v\n", e)

decodeReader := transform.NewReader(bodyReader, e.NewDecoder())
複製代碼

利用 bufio 生成新的 reader,而後利用 determineEncoding 檢測內容編碼,並經過 transform 進行編碼轉化。

圖片下載

若是訪問內容是一張圖片,咱們如何把它下載下來呢?好比以下地址的圖片。

pic2.zhimg.com/v2-5e8b41ca…

其實很簡單,只須要建立新的文件並把響應內容保存進去便可。

f, err := os.Create("as.jpg")
if err != nil {
	panic(err)
}
defer func() { _ = f.Close() }()

_, err = io.Copy(f, r.Body)
if err != nil {
	panic(err)
}
複製代碼

r 即 Response,利用 os 建立了新的文件,而後再經過 io.Copy 將響應的內容保存進文件中。

定製請求頭

如何爲請求定製請求頭呢?Request 其實已經提供了相應的方法,經過 req.Header.Add 便可完成。

舉個例子,假設咱們將要訪問 httpbin.org/get,但這個地址針對 user-agent 設置了發爬策略。咱們須要修改默認的 user-agent。

示例代碼:

req, err := http.NewRequest(http.MethodGet, "http://httpbin.org/get", nil)
if err != nil {
	panic(err)
}

req.Header.Add("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0)")
複製代碼

如上即可完成任務。

複雜的POST請求

前面已經展現過了向 POST 接口提交 JSON 數據的方式。接下來介紹下另外幾種向 POST 接口提交數據的方式,即表單提交和文件提交。

表單提交

表單提交是一個很經常使用的功能,故而在 net/http 中,除了提供標準的用法外,還給咱們提供了簡化的方法。

咱們先來介紹個標準的實現方法。

舉個例子,假設要向 httpbin.org/post 提交 name 爲 poloxue 和 password 爲 123456 的表單。

payload := make(url.Values)
payload.Add("name", "poloxue")
payload.Add("password", "123456")
req, err := http.NewRequest(
	http.MethodPost,
	"http://httpbin.org/post",
	strings.NewReader(payload.Encode()),
)
if err != nil {
	panic(err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

r, err := http.DefaultClient.Do(req)
複製代碼

POST 的 payload 是形如 name=poloxue&password=123456 的字符串,故而咱們能夠經過 url.Values 進行組織。

提交給 NewRequest 的內容必須是實現 Reader 接口的類型,因此須要 strings.NewReader轉化下。

Form 表單提交的 content-type 要是 application/x-www-form-urlencoded,也要設置下。

複雜的方式介紹完了。接着再介紹簡化的方式,其實表單提交只需調用 http.PostForm 便可完成。示例代碼以下:

payload := make(url.Values)
payload.Add("name", "poloxue")
payload.Add("password", "123456")
r, err := http.PostForm("http://httpbin.org/post", form)
複製代碼

竟是如此的簡單。

提交文件

文件提交應該是 HTTP 請求中較爲複雜的內容了。其實說難也不難,區別於其餘的請求,咱們要花些精力來讀取文件,組織提交POST的數據。

舉個例子,假設如今我有一個圖片文件,名爲 as.jpg,路徑在 /Users/polo 目錄下。如今要將這個圖片提交給 httpbin.org/post。

咱們要先組織 POST 提交的內容,代碼以下:

filename := "/Users/polo/as.jpg"

f, err := os.Open(filename)
if err != nil {
	panic(err)
}
defer func() { _ = f.Close() }()

uploadBody := &bytes.Buffer{}
writer := multipart.NewWriter(uploadBody)

fWriter, err := writer.CreateFormFile("uploadFile", filename)
if err != nil {
	fmt.Printf("copy file writer %v", err)
}

_, err = io.Copy(fWriter, f)
if err != nil {
	panic(err)
}

fieldMap := map[string]string{
	"filename": filename,
}
for k, v := range fieldMap {
	_ = writer.WriteField(k, v)
}

err = writer.Close()
if err != nil {
	panic(err)
}
複製代碼

我認爲,數據組織分爲幾步完成,以下:

第一步,打開將要上傳的文件,使用 defer f.Close() 作好資源釋放的準備; 第二步,建立存儲上傳內容的 bytes.Buffer,變量名爲 uploadBody; 第三步,經過 multipart.NewWriter 建立 writer,用於向 buffer中寫入文件提供的內容; 第四步,經過writer.CreateFormFile 建立上傳文件並經過 io.Copy 向其中寫入內容; 最後,經過 writer.WriteField 添加其餘的附加信息,注意最後要把 writer 關閉; 至此,文件上傳的數據就組織完成了。接下來,只需調用 http.Post 方法便可完成文件上傳。

r, err := http.Post("http://httpbin.org/post", writer.FormDataContentType(), uploadBody)
複製代碼

有一點要注意,請求的content-type須要設置,而經過 writer.FormDataContentType() 即能得到上傳文件的類型。

到此,文件提交也完成了,不知道有沒有很是簡單的感受。

Cookie

主要涉及兩部份內容,即讀取響應的 cookie 與設置請求的 cookie。響應的 cookie 獲取方式很是簡單,直接調用 r.Cookies 便可。

重點來講說,如何設置請求 cookie。cookie設置有兩種方式,一種設置在 Client 上,另外一種是設置在 Request 上。

Client 上設置 Cookie

直接看示例代碼:

cookies := make([]*http.Cookie, 0)

cookies = append(cookies, &http.Cookie{
	Name:   "name",
	Value:  "poloxue",
	Domain: "httpbin.org",
	Path:   "/cookies",
})
cookies = append(cookies, &http.Cookie{
	Name:   "id",
	Value:  "10000",
	Domain: "httpbin.org",
	Path:   "/elsewhere",
})

url, err := url.Parse("http://httpbin.org/cookies")
if err != nil {
	panic(err)
}

jar, err := cookiejar.New(nil)
if err != nil {
	panic(err)
}
jar.SetCookies(url, cookies)

client := http.Client{Jar: jar}

r, err := client.Get("http://httpbin.org/cookies")
複製代碼

代碼中,咱們首先建立了 http.Cookie 切片,而後向其中添加了 2 個 Cookie 數據。這裏經過 cookiejar,保存了 2 個新建的 cookie。

此次咱們不能再使用默認的 DefaultClient 了,而是要建立新的 Client,並將保存 cookie 信息的 cookiejar 與 client 綁定。接下里,只須要使用新建立的 Client 發起請求便可。

請求上設置 Cookie

請求上的 cookie 設置,經過 req.AddCookie便可實現。示例代碼:

req, err := http.NewRequest(http.MethodGet, "http://httpbin.org/cookies", nil)
if err != nil {
	panic(err)
}

req.AddCookie(&http.Cookie{
	Name:   "name",
	Value:  "poloxue",
	Domain: "httpbin.org",
	Path:   "/cookies",
})

r, err := http.DefaultClient.Do(req)
複製代碼

挺簡單的,沒什麼要介紹的。

cookie 設置 Client 和 設置在 Request 上有何區別?一個最易想到的區別就是,Request 的 cookie 只是當次請求失效,而 Client 上的 cookie 是隨時有效的,只要你用的是這個新建立的 Client。

重定向和請求歷史

默認狀況下,全部類型請求都會自動處理重定向。

Python 的 requests 包中 HEAD 請求是不重定向的,但測試結果顯示 net/http 的 HEAD 是自動重定向的。

net/http 中的重定向控制能夠經過 Client 中的一個名爲 CheckRedirect 的成員控制,它是函數類型。定義以下:

type Client struct {
	...
	CheckRedirect func(req *Request, via []*Request) error
	...
}
複製代碼

接下來,咱們來看看怎麼使用。

假設咱們要實現的功能:爲防止發生循環重定向,重定向次數定義不能超過 10 次,並且要記錄歷史 Response。

示例代碼:

var r *http.Response
history := make([]*http.Response, 0)

client := http.Client{
	CheckRedirect: func(req *http.Request, hrs []*http.Request) error {
		if len(hrs) >= 10 {
			return errors.New("redirect to many times")
		}

		history = append(history, req.Response)
		return nil
	},
}

r, err := client.Get("http://github.com")
複製代碼

首先建立了 http.Response 切片的變量,名稱爲 history。接着在 http.Client 中爲 CheckRedirect 賦予一個匿名函數,用於控制重定向的行爲。CheckRedirect 函數的第一個參數表示下次將要請求的 Request,第二個參數表示已經請求過的 Request。

當發生重定向時,當前的 Request 會保存上次請求的 Response,故而此處能夠將 req.Response 追加到 history 變量中。

超時設置

Request 發出後,若是服務端遲遲沒有響應,那豈不是很尷尬。那麼咱們就會想,可否爲請求設置超時規則呢?毫無疑問,固然能夠。

超時能夠分爲鏈接超時和響應讀取超時,這些均可以設置。但正常狀況下,並不想有那麼明確的區別,那麼也能夠設置個總超時。

總超時

總的超時時間的設置是綁定在 Client 的一個名爲 Timeout 的成員之上,Timeout 是 time.Duration。

假設這是超時時間爲 10 秒,示例代碼:

client := http.Client{
	Timeout:   time.Duration(10 * time.Second),
}
複製代碼

鏈接超時

鏈接超時可經過 Client 中的 Transport 實現。Transport 中有個名爲 Dial 的成員函數,可用設置鏈接超時。Transport 是 HTTP 底層的數據運輸者。

假設設置鏈接超時時間爲 2 秒,示例代碼:

t := &http.Transport{
	Dial: func(network, addr string) (net.Conn, error) {
		timeout := time.Duration(2 * time.Second)
		return net.DialTimeout(network, addr, timeout)
	},
}
複製代碼

在 Dial 的函數中,咱們經過 net.DialTimeout 進行網絡鏈接,實現了鏈接超時功能。

讀取超時

讀取超時也要經過 Client 的 Transport 設置,好比設置響應的讀取爲 8 秒。

示例代碼:

t := &http.Transport{
	ResponseHeaderTimeout: time.Second * 8,
}
綜合全部,Client 的建立代碼以下:

t := &http.Transport{
	Dial: func(network, addr string) (net.Conn, error) {
		timeout := time.Duration(2 * time.Second)
		return net.DialTimeout(network, addr, timeout)
	},
	ResponseHeaderTimeout: time.Second * 8,
}
client := http.Client{
	Transport: t,
	Timeout:   time.Duration(10 * time.Second),
}
複製代碼

除了上面的幾個超時設置,Transport 還有其餘一些關於超時的設置,能夠看下 Transport 的定義,還有發現三個與超時相關的定義:

// IdleConnTimeout is the maximum amount of time an idle
// (keep-alive) connection will remain idle before closing
// itself.
// Zero means no limit.
IdleConnTimeout time.Duration

// ResponseHeaderTimeout, if non-zero, specifies the amount of
// time to wait for a server's response headers after fully // writing the request (including its body, if any). This // time does not include the time to read the response body. ResponseHeaderTimeout time.Duration // ExpectContinueTimeout, if non-zero, specifies the amount of // time to wait for a server's first response headers after fully
// writing the request headers if the request has an
// "Expect: 100-continue" header. Zero means no timeout and
// causes the body to be sent immediately, without
// waiting for the server to approve.
// This time does not include the time to send the request header.
ExpectContinueTimeout time.Duration
複製代碼

分別是 IdleConnTimeout (鏈接空閒超時時間,keep-live 開啓)、TLSHandshakeTimeout (TLS 握手時間)和 ExpectContinueTimeout(彷佛已含在 ResponseHeaderTimeout 中了,看註釋)。

到此,完成了超時的設置。相對於 Python requests 確實是複雜不少。

請求代理

代理仍是挺重要的,特別對於開發爬蟲的同窗。那 net/http 怎麼設置代理?這個工做仍是要依賴 Client 的成員 Transport 實現,這個 Transport 仍是挺重要的。

Transport 有個名爲 Proxy 的成員,具體看看怎麼使用吧。假設咱們要經過設置代理來請求谷歌的主頁,代理地址爲 http://127.0.0.1:8087

示例代碼:

proxyUrl, err := url.Parse("http://127.0.0.1:8087")
if err != nil {
	panic(err)
}
t := &http.Transport{
	Proxy:           http.ProxyURL(proxyUrl),
	TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := http.Client{
	Transport: t,
	Timeout:   time.Duration(10 * time.Second),
}

r, err := client.Get("https://google.com")
複製代碼

主要關注 http.Transport 建立的代碼。兩個參數,分時 Proxy 和 TLSClientConfig,分別用於設置代理和禁用 https 驗證。我發現其實不設置 TLSClientConfig 也能夠請求成功,具體緣由沒仔細研究。

錯誤處理

錯誤處理其實都不用怎麼介紹,GO中的通常錯誤主要是檢查返回的error,HTTP 請求也是如此,它會視狀況返回相應錯誤信息,好比超時、網絡鏈接失敗等。

示例代碼中的錯誤都是經過 panic 拋出去的,真實的項目確定不是這樣的,咱們須要記錄相關日誌,時刻作好錯誤恢復工做。

總結

本文以 Python 的 requests 文檔爲指導方向,整理了 requests 快速入門文檔中的案例在 GO 的是如何實現的。要說明的是, GO 其實也提供了對應於 requests 的克隆版本,github地址。暫時我也尚未看,有興趣的朋友能夠去研究一下。


波羅學的微信公衆號
相關文章
相關標籤/搜索