本文地址:http://www.reibang.com/p/4e53d4727152
一暴区、簡介
Golang誕生已經(jīng)超過十個(gè)年頭了耙饰,發(fā)展得愈發(fā)完善,其簡單方便的協(xié)程并發(fā)機(jī)制使得其在爬蟲領(lǐng)域有著一定的天賦桑谍。
首先我們來看一看铺董,Golang相對于Python這個(gè)爬蟲領(lǐng)域的傳統(tǒng)強(qiáng)者,有哪些優(yōu)點(diǎn)和缺點(diǎn)棱貌。
優(yōu)點(diǎn):
- 完善簡便的協(xié)程并發(fā)機(jī)制
- 并發(fā)數(shù)量大
- 占用資源少
- 運(yùn)行速度更快
- 部署方便
缺點(diǎn):
- 數(shù)據(jù)處理比較繁瑣
- 成熟工具不是很多
- 資料較少
- 實(shí)現(xiàn)相同邏輯需要的代碼更多
由于Golang本身靜態(tài)語言的特性玖媚,和其特別的異常處理方式等等原因,在發(fā)起較復(fù)雜的請求時(shí)需要的代碼量自然會比Python多不少婚脱,但是其并發(fā)的數(shù)量也是遠(yuǎn)超Python的今魔,所以兩者應(yīng)用的場景并不十分相同,我們可以根據(jù)需要靈活的選擇障贸。
在剛剛接觸Golang的http包時(shí)错森,覺得其非常的方便,發(fā)起請求只需要一行代碼:
http.Get("https://www.baidu.com")
就算與Python的requests
在便利方面也不遑多讓篮洁,然而在Golang勾起了我的興趣涩维,并深入接觸后,我發(fā)現(xiàn)并非如此袁波。最簡單的http.Get
方法只能發(fā)起最簡單的請求瓦阐,一旦要設(shè)置headers、cookies等屬性時(shí)篷牌,需要寫的代碼會成幾何倍數(shù)上升睡蟋,而設(shè)置代理或者管理重定向等操作,會更加復(fù)雜枷颊。
這個(gè)摸索的過程中最痛苦的是戳杀,在網(wǎng)上能找到資料非常的稀少,大多數(shù)時(shí)候只能閱讀官方文檔和閱讀net
標(biāo)準(zhǔn)庫的源碼夭苗。所幸Go語言的特性使得閱讀Go源碼是一件比較簡單的事信卡,相對于其他語言來說。
所以本篇文章的目的听诸,是為了讓那些使用Golang的朋友坐求,對如何使用Golang發(fā)起請求有一個(gè)比較全面的了解。
注1:Golang中文官網(wǎng)的文檔版本比較低晌梨,有些地方與最新版本不同桥嗤,有條件的同學(xué)可以爬爬梯子,去golang.org英文官網(wǎng)看文檔仔蝌。
注2:文中代碼為了簡潔泛领,省略掉了異常處理的部分,實(shí)際使用時(shí)需要按情況加上敛惊。
二渊鞋、簡單請求
Golang中的net
包封裝了大部分網(wǎng)絡(luò)相關(guān)的功能,我們基本不需要借助其他庫就能實(shí)現(xiàn)我們的爬蟲需求。其中最為常用的是http
和url
锡宋,使用前可以根據(jù)我們的需要進(jìn)行導(dǎo)入:
import (
"net/http"
"net/url"
)
http
提供了一些非常方便的接口儡湾,可以實(shí)現(xiàn)最簡單的請求,例如Get执俩、Post徐钠、Head:
resp, err := http.Get("http://example.com/")
...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...
resp, err := http.PostForm("http://example.com/form",
url.Values{"key": {"Value"}, "id": {"123"}})
可以看到,我們非常簡單的就發(fā)起了請求并獲得了響應(yīng)役首,這里需要注意一點(diǎn)的是尝丐,獲得的響應(yīng)body需要我們手動關(guān)閉:
resp, err := http.Get("http://example.com/")
if err != nil {
// 處理異常
}
defer resp.Body.Close() // 函數(shù)結(jié)束時(shí)關(guān)閉Body
body, err := ioutil.ReadAll(resp.Body) // 讀取Body
// ...
這樣的請求方式是非常方便的,但是當(dāng)我們需要定制我們請求的其他參數(shù)時(shí)衡奥,就必須要使用其他組件了爹袁。
三、Client
Client
是http
包內(nèi)部發(fā)起請求的組件矮固,使用它失息,我們才可以去控制請求的超時(shí)、重定向和其他的設(shè)置乏屯。以下是Client的定義:
type Client struct {
Transport RoundTripper
CheckRedirect func(req *Request, via []*Request) error
Jar CookieJar
Timeout time.Duration // Go 1.3
}
首先是生成Client對象:
client := &http.Client{}
Client也有一些簡便的請求方法根时,如:
resp, err := client.Get("http://example.com")
但這種方法與直接使用http.Get
沒多大差別,我們需要使用另一個(gè)方法來定制請求的Header辰晕、請求體蛤迎、證書驗(yàn)證等參數(shù),這就是Request
和Do
含友。
3.1. 設(shè)置超時(shí)
這是一張說明Client超時(shí)的控制范圍的圖:
這其中替裆,設(shè)置起來最方便的是http.Client.Timeout
,可以在創(chuàng)建Client時(shí)通過字段設(shè)置窘问,其計(jì)算的范圍包括連接(Dial)到讀完response body為止辆童。
http.Client
會自動跟隨重定向,重定向時(shí)間也會記入http.Client.Timeout
惠赫,這點(diǎn)一定要注意把鉴。
client := &http.Client{
Timeout: 15 * time.Second
}
還有一些更細(xì)粒度的超時(shí)控制:
-
net.Dialer.Timeout
限制建立TCP連接的時(shí)間 -
http.Transport.TLSHandshakeTimeout
限制 TLS握手的時(shí)間 -
http.Transport.ResponseHeaderTimeout
限制讀取response header的時(shí)間 -
http.Transport.ExpectContinueTimeout
限制client在發(fā)送包含Expect: 100-continue
的header到收到繼續(xù)發(fā)送body的response之間的時(shí)間等待。
如果需要使用這些超時(shí)儿咱,需要到Transport
中去設(shè)置庭砍,方法如下所示:
c := &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
可以看到這其中沒有單獨(dú)控制Do
方法超時(shí)時(shí)間的設(shè)置,如果需要的話可以使用context
自行實(shí)現(xiàn)混埠。
3.2. 控制重定向
在Client的字段中怠缸,有一個(gè)CheckRedirect
,此字段就是用來控制重定向的函數(shù)钳宪,如果沒有定義此字段的話揭北,將會使用默認(rèn)的defaultCheckRedirect
方法扳炬。
默認(rèn)的轉(zhuǎn)發(fā)策略是最多轉(zhuǎn)發(fā)10次。
在轉(zhuǎn)發(fā)的過程中搔体,某一些包含安全信息的Header恨樟,比如Authorization
、WWW-Authenticate
嫉柴、Cookie
等厌杜,如果轉(zhuǎn)發(fā)是跨域的,那么這些Header不會復(fù)制到新的請求中计螺。
http
的重定向判斷會默認(rèn)處理以下狀態(tài)碼的請求:
- 301 (Moved Permanently)
- 302 (Found)
- 303 (See Other)
- 307 (Temporary Redirect)
- 308 (Permanent Redirect)
301、302和303請求將會改用Get訪問新的請求瞧壮,而307和308會使用原有的請求方式登馒。
那么,我們?nèi)绾稳タ刂浦囟ㄏ虻拇螖?shù)咆槽,甚至是禁止重定向呢陈轿?這里其實(shí)就需要我們自己去實(shí)現(xiàn)一個(gè)CheckRedirect
函數(shù)了,首先我們來看看默認(rèn)的defaultCheckRedirect
方法:
func defaultCheckRedirect(req *Request, via []*Request) error {
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
}
第一個(gè)參數(shù)req
是即將轉(zhuǎn)發(fā)的request秦忿,第二個(gè)參數(shù) via
是已經(jīng)請求過的requests麦射。可以看到其中的邏輯是判斷請求過的request數(shù)量灯谣,大于等于10的時(shí)候返回一個(gè)error
潜秋,這也說明默認(rèn)的最大重定向次數(shù)為10次,當(dāng)此函數(shù)返回error
時(shí)胎许,即是重定向結(jié)束的時(shí)候峻呛。
所以如果需要設(shè)置重定向次數(shù),那么復(fù)制一份這個(gè)函數(shù)辜窑,修改函數(shù)名字和其中if判斷的數(shù)字钩述,然后在生成Client時(shí)設(shè)定到Client即可:
client := &http.Client{
CheckRedirect: yourCheckRedirect,
}
或者:
client := &http.Client{}
client.CheckRedirect = yourCheckRedirect
禁止重定向則可以把判斷數(shù)字修改為0。最好相應(yīng)地修改errors中提示的信息穆碎。
3.3. CookieJar管理
可以看到Client結(jié)構(gòu)體中還有一個(gè)Jar
字段牙勘,類型為CookieJar
,這是Client用來管理Cookie的對象所禀。
如果在生成Client時(shí)方面,沒有給這個(gè)字段賦值,使其為nil
的話北秽,那么之后Client發(fā)起的請求將只會帶上Request對象中指定的Cookie葡幸,請求響應(yīng)中由服務(wù)器返回的Cookie也不會被保存。所以如果需要自動管理Cookie的話贺氓,我們還需要生成并設(shè)定一個(gè)CookieJar對象:
options := cookiejar.Options{
PublicSuffixList: publicsuffix.List
}
jar, err := cookiejar.New(&options)
client := &http.Client{
Jar: jar,
}
這里的publicsuffix.List
是一個(gè)域的公共后綴列表蔚叨,是一個(gè)可選的選項(xiàng)床蜘,設(shè)置為nil
代表不啟用。但是不啟用的情況下會使Cookie變得不安全:意味著foo.com的HTTP服務(wù)器可以為bar.com設(shè)置cookie蔑水。所以一般來說最好啟用邢锯。
如果嫌麻煩不想啟用PublicSuffixList
,可以將其設(shè)置為nil
搀别,如下即可:
jar, err := cookiejar.New(nil)
client := &http.Client{
Jar: jar,
}
而publicsuffix.List
的實(shí)現(xiàn)位于golang.org/x/net/publicsuffix丹擎,需要額外下載,使用的時(shí)候也需要導(dǎo)入:
import "golang.org/x/net/publicsuffix"
四歇父、 Request
這是Golang源碼中Request定義的字段蒂培,可以看到非常的多,有興趣的可以去源碼或者官方文檔看有注釋的版本榜苫,本文只介紹一些比較重要的字段护戳。
type Request struct {
Method string
URL *url.URL
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
Header Header
Body io.ReadCloser
GetBody func() (io.ReadCloser, error)
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
Trailer Header
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
Cancel <-chan struct{}
Response *Response
}
在這里不推薦直接生成Request,而應(yīng)該使用http提供的NewRequest
方法來生成Request垂睬,此方法中做了一些生成Request的默認(rèn)設(shè)置媳荒,以下是NewRequest
的函數(shù)簽名:
func NewRequest(method, url string, body io.Reader) (*Request, error)
參數(shù)中method
和url
兩個(gè)是必備參數(shù),而body
參數(shù)驹饺,在使用沒有body的請求方法時(shí)钳枕,傳入nil
即可。
配置好Request之后赏壹,使用Client對象的Do
方法鱼炒,就可以將Request發(fā)送出去,以下是示例:
req, err := NewRequest("GET", "https://www.baidu.com", nil)
resp, err := client.Do(req)
4.1. Method
請求方法卡儒,必備的參數(shù)田柔,如果為空字符則表示Get請求。
注:Go的HTTP客戶端不支持CONNECT
請求方法骨望。
4.2. URL
一個(gè)被解析過的url結(jié)構(gòu)體硬爆。
4.3. Proto
HTTP協(xié)議版本。
在Go中擎鸠,HTTP請求會默認(rèn)使用HTTP1.1
缀磕,而HTTPS請求會默認(rèn)首先使用HTTP2.0
,如果目標(biāo)服務(wù)器不支持劣光,握手失敗后才會改用HTTP1.1
袜蚕。
如果希望強(qiáng)制使用HTTP2.0
的協(xié)議,那么需要使用golang.org/x/net/http2
這個(gè)包所提供的功能绢涡。
4.4. 發(fā)起Post請求
如果要使用Request發(fā)起Post請求牲剃,提交表單的話,可以用到它的PostForm
字段雄可,這是一個(gè)類型為url.Values
的字段凿傅,以下為示例:
req, err := NewRequest("Post", "https://www.baidu.com", nil)
req.PostForm.Add("key", "value")
如果你Post提交的不是表單數(shù)據(jù)缠犀,那么你需要將其封裝成io.Reader
類型,并在NewRequest
函數(shù)中傳遞進(jìn)去聪舒。
4.4. 設(shè)置Header
Header的類型是http.Header
辨液,其中包含著之前請求中返回的header和client發(fā)送的header。
可以使用這種方式設(shè)置Header:
req, err := NewRequest("Get", "https://www.baidu.com", nil)
req.Header.Add("key", "value")
Header還有一些Set
箱残、Del
等方法可以使用滔迈。
4.5. 添加Cookie
前文我們已經(jīng)介紹了如何在Client中啟用一直使用的CookieJar,使用它可以自動管理獲得的Cookie被辑。
但很多時(shí)候我們也需要給特定的請求手動設(shè)置Cookie燎悍,這個(gè)時(shí)候就可以使用Request對象的AddCookie
方法,這是其函數(shù)簽名:
func (r *Request) AddCookie(c *Cookie)
要注意的是盼理,其傳入的參數(shù)是Cookie類型间涵,,以下是此類型包含的屬性:
type Cookie struct {
Name string
Value string
Path string
Domain string
Expires time.Time
RawExpires string
MaxAge int
Secure bool
HttpOnly bool
Raw string
Unparsed []string
}
其中只有Name
和Value
是必須的榜揖,所以以下是添加Cookie的示例:
c := &http.Cookie{
Name: "key",
Value: "value",
}
req.AddCookie(c)
五、Transport
Transport
是Client
中的一個(gè)類型抗蠢,用于控制傳輸過程举哟,是Client實(shí)際發(fā)起請求的底層實(shí)現(xiàn)。如果沒有給這個(gè)字段初始化相應(yīng)的值迅矛,那么將會使用默認(rèn)的DefaultTransport
妨猩。
Transport承擔(dān)起了Client中連接池的功能,它會將建立的連接緩存下來秽褒,這可能會在訪問大量不同網(wǎng)站時(shí)壶硅,留下太多打開的連接,這可以使用Transport中的方法進(jìn)行關(guān)閉销斟。
首先來看一下Transport
的定義:
type Transport struct {
Proxy func(*Request) (*url.URL, error)
DialContext func(ctx context.Context, network, addr string) (net.Conn, error) // Go 1.7
Dial func(network, addr string) (net.Conn, error)
DialTLS func(network, addr string) (net.Conn, error) // Go 1.4
TLSClientConfig *tls.Config
TLSHandshakeTimeout time.Duration // Go 1.3
DisableKeepAlives bool
DisableCompression bool
MaxIdleConns int // Go 1.7
MaxIdleConnsPerHost int
MaxConnsPerHost int // Go 1.11
IdleConnTimeout time.Duration // Go 1.7
ResponseHeaderTimeout time.Duration // Go 1.1
ExpectContinueTimeout time.Duration // Go 1.6
TLSNextProto map[string]func(authority string, c *tls.Conn) RoundTripper // Go 1.6
ProxyConnectHeader Header // Go 1.8
MaxResponseHeaderBytes int64 // Go 1.7
}
由于Transport
是Client
內(nèi)部請求的實(shí)際發(fā)起者庐椒,所以內(nèi)容會比較多,1.6之后的版本也添加了許多新的字段蚂踊,這里我們來講解常見的一些字段约谈。
5.1. 撥號
由于Client中設(shè)置的Timeout范圍比較寬,而在生產(chǎn)環(huán)境中我們可能需要更為精細(xì)的超時(shí)控制犁钟,在Dial
撥號中可以設(shè)置幾個(gè)超時(shí)時(shí)間棱诱。
在較新的版本中,Dial
這個(gè)字段已經(jīng)不再被推薦使用涝动,取而代之的是DialContext
迈勋,設(shè)置這個(gè)字段,需要借助于net.Dialer
醋粟,以下是其定義:
type Dialer struct {
Timeout time.Duration
Deadline time.Time
LocalAddr Addr
DualStack bool
FallbackDelay time.Duration
KeepAlive time.Duration
Resolver *Resolver
Cancel <-chan struct{}
Control func(network, address string, c syscall.RawConn) error
}
這其中需要我們設(shè)置的并不多靡菇,主要是Timeout和KeepAlive重归。Timeout是Dial這個(gè)過程的超時(shí)時(shí)間,而KeepAlive是連接池中連接的超時(shí)時(shí)間镰官,如下所示:
trans := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
}
5.2. 設(shè)置代理
Transport第一個(gè)Proxy
字段是用來設(shè)置代理提前,支持HTTP、HTTPS泳唠、SOCKS5三種代理方式狈网,首先我們來看看如何設(shè)置HTTP和HTTPS代理:
package main
import (
"net/url"
"net/http"
)
func main() {
proxyURL, _ := url.Parse("https://127.0.0.1:1080")
trans := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
client := &http.Client{
Transport: trans,
}
client.Get("https://www.google.com")
}
設(shè)置SOCKS5代理則需要借助golang.org/x/net/proxy
:
package main
import (
"net/url"
"net/http"
"golang.org/x/net/proxy"
)
func main() {
dialer, err := proxy.SOCKS5("tcp", "127.0.0.1:8080",
&proxy.Auth{User:"username", Password:"password"},
&net.Dialer {
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
},
)
trans := &http.Transport{
DialContext: dialer.DialContext
}
client := &http.Client{
Transport: trans,
}
client.Get("https://www.google.com")
}
這里的proxy.SOCKS5
函數(shù)將會返回一個(gè)Dialer
對象,其傳入的參數(shù)分別為協(xié)議笨腥、IP端口拓哺、賬號密碼、Dialer
脖母,如果代理不需要賬號密碼驗(yàn)證的話士鸥,第三個(gè)字段可以設(shè)置為nil
。
5.3. 連接控制
眾所周知谆级,HTTP1.0協(xié)議使用的是短連接烤礁,而HTTP1.1默認(rèn)使用的是長連接,使用長連接則可以復(fù)用連接肥照,減少建立連接的開銷脚仔。
Transport
中實(shí)現(xiàn)了連接池的功能,可以將連接保存下來以便下次訪問此域名舆绎,其中也對連接的數(shù)量做出了一定的限制鲤脏。
DisableKeepAlives
這個(gè)字段可以用來關(guān)閉長連接,默認(rèn)值為false吕朵,如果有特殊的需求猎醇,需要使用短連接,可以設(shè)置此字段為true:
trans := &http.Transport{
...
DisableKeepAlives: true,
}
除此之外努溃,還可以控制連接的數(shù)量和保持時(shí)間:
-
MaxConnsPerHost int
- 每個(gè)域名下最大連接數(shù)量硫嘶,包括正在撥號的、活躍的茅坛、空閑的的連接音半。值為0表示不限制數(shù)量。
-
MaxIdleConns int
- 空閑連接的最大數(shù)量贡蓖。DefaultTransport中的默認(rèn)值為100曹鸠,在需要發(fā)起大量連接時(shí)偏小,可以根據(jù)需求自行設(shè)定斥铺。
值為0表示不限制數(shù)量彻桃。
-
MaxIdleConnsPerHost int
- 每個(gè)域名下空閑連接的最大數(shù)量。值為0則會使用默認(rèn)的數(shù)量晾蜘,每個(gè)域名下只能有兩個(gè)空閑連接邻眷。在對單個(gè)網(wǎng)站發(fā)起大量連接時(shí)霞丧,兩個(gè)連接可能會不夠臀突,可以酌情增加此數(shù)值料祠。
-
IdleConnTimeout time.Duration
- 空閑連接的超時(shí)時(shí)間储玫,從每一次空閑開始算。DefaultTransport中的默認(rèn)值為90秒驯镊。值為0表示不限制葫督。
由于Transport負(fù)擔(dān)起了連接池的功能,所以在并發(fā)使用時(shí)板惑,最好將Transport與Client一起復(fù)用橄镜,不然可能會造成發(fā)起過量的長連接,浪費(fèi)系統(tǒng)資源冯乘。
六洽胶、其他
6.1. 設(shè)置url參數(shù)
在Go的請求方式中,沒有給我們提供可以直接設(shè)置url參數(shù)的方法裆馒,所以需要我們自己在url地址中進(jìn)行拼接姊氓。
url
包中提供了一個(gè)url.Values
類型,其本質(zhì)的類型為:map[string][]string
喷好,可以讓我們拼接參數(shù)更加簡單他膳,如下所示:
URL := "http://httpbin.org/get"
params := url.Values{
"key1": {"value"},
"key2": {"value2", "value3"},
}
URL = URL + "&" + params.Encode()
fmt.Println(URL)
// 輸出為:http://httpbin.org/get&key1=value&key2=value2&key2=value3
七、總結(jié)
總的來說绒窑,Go語言中內(nèi)置的標(biāo)準(zhǔn)庫功能是比較完善的,如果要寫一個(gè)客戶端的話舔亭,基本不需要用到標(biāo)準(zhǔn)庫之外的內(nèi)容些膨,其可以控制的請求細(xì)節(jié)也比較多。
但相較于Python的Requests這類庫钦铺,需要寫的代碼依然要多非常多订雾,再加上特別的異常處理機(jī)制,在請求過程中要寫大量的異常檢查語句矛洞。需要使用的朋友可以考慮先將請求和異常處理的部分封裝以后使用洼哎。
八、示例
以下是發(fā)起Get請求的一個(gè)例子:
// 生成client客戶端
client := &http.Client{}
// 生成Request對象
req, err := http.NewRequest("Get", "http://httpbin.org/get", nil)
if err != nil {
fmt.Println(err)
}
// 添加Header
req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36")
// 發(fā)起請求
resp, err := client.Do(req)
if err != nil {
fmt.Println(err)
}
// 設(shè)定關(guān)閉響應(yīng)體
defer resp.Body.Close()
// 讀取響應(yīng)體
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
}
fmt.Println(string(body))
參考:
https://www.cnblogs.com/WingPig/p/5929138.html
http://mengqi.info/html/2015/201506062329-socks5-proxy-client-in-golang.html
https://colobu.com/2016/07/01/the-complete-guide-to-golang-net-http-timeouts/