美文网首页golanggo 爬虫
Golang爬虫全攻略

Golang爬虫全攻略

作者: 王南北丶 | 来源:发表于2019-05-20 15:46 被阅读0次

    本文地址:https://www.jianshu.com/p/4e53d4727152

    一、简介

    Golang诞生已经超过十个年头了,发展得愈发完善,其简单方便的协程并发机制使得其在爬虫领域有着一定的天赋。

    首先我们来看一看,Golang相对于Python这个爬虫领域的传统强者,有哪些优点和缺点。

    优点:

    • 完善简便的协程并发机制
    • 并发数量大
    • 占用资源少
    • 运行速度更快
    • 部署方便

    缺点:

    • 数据处理比较繁琐
    • 成熟工具不是很多
    • 资料较少
    • 实现相同逻辑需要的代码更多

    由于Golang本身静态语言的特性,和其特别的异常处理方式等等原因,在发起较复杂的请求时需要的代码量自然会比Python多不少,但是其并发的数量也是远超Python的,所以两者应用的场景并不十分相同,我们可以根据需要灵活的选择。

    在刚刚接触Golang的http包时,觉得其非常的方便,发起请求只需要一行代码:

    http.Get("https://www.baidu.com")
    

    就算与Python的requests在便利方面也不遑多让,然而在Golang勾起了我的兴趣,并深入接触后,我发现并非如此。最简单的http.Get方法只能发起最简单的请求,一旦要设置headers、cookies等属性时,需要写的代码会成几何倍数上升,而设置代理或者管理重定向等操作,会更加复杂。

    这个摸索的过程中最痛苦的是,在网上能找到资料非常的稀少,大多数时候只能阅读官方文档和阅读net标准库的源码。所幸Go语言的特性使得阅读Go源码是一件比较简单的事,相对于其他语言来说。

    所以本篇文章的目的,是为了让那些使用Golang的朋友,对如何使用Golang发起请求有一个比较全面的了解。

    注1:Golang中文官网的文档版本比较低,有些地方与最新版本不同,有条件的同学可以爬爬梯子,去golang.org英文官网看文档。

    注2:文中代码为了简洁,省略掉了异常处理的部分,实际使用时需要按情况加上。


    二、简单请求

    Golang中的net包封装了大部分网络相关的功能,我们基本不需要借助其他库就能实现我们的爬虫需求。其中最为常用的是httpurl,使用前可以根据我们的需要进行导入:

    import (
        "net/http"
        "net/url"
    )
    

    http提供了一些非常方便的接口,可以实现最简单的请求,例如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"}})
    

    可以看到,我们非常简单的就发起了请求并获得了响应,这里需要注意一点的是,获得的响应body需要我们手动关闭:

    resp, err := http.Get("http://example.com/")
    if err != nil {
        // 处理异常
    }
    defer resp.Body.Close()  // 函数结束时关闭Body
    body, err := ioutil.ReadAll(resp.Body)  // 读取Body
    // ...
    

    这样的请求方式是非常方便的,但是当我们需要定制我们请求的其他参数时,就必须要使用其他组件了。


    三、Client

    Clienthttp包内部发起请求的组件,使用它,我们才可以去控制请求的超时、重定向和其他的设置。以下是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没多大差别,我们需要使用另一个方法来定制请求的Header、请求体、证书验证等参数,这就是RequestDo

    3.1. 设置超时

    这是一张说明Client超时的控制范围的图:

    client-timeout.png

    这其中,设置起来最方便的是http.Client.Timeout,可以在创建Client时通过字段设置,其计算的范围包括连接(Dial)到读完response body为止。

    http.Client会自动跟随重定向,重定向时间也会记入http.Client.Timeout,这点一定要注意。

    client := &http.Client{
        Timeout: 15 * time.Second
    }
    

    还有一些更细粒度的超时控制:

    • net.Dialer.Timeout 限制建立TCP连接的时间
    • http.Transport.TLSHandshakeTimeout 限制 TLS握手的时间
    • http.Transport.ResponseHeaderTimeout 限制读取response header的时间
    • http.Transport.ExpectContinueTimeout 限制client在发送包含 Expect: 100-continue的header到收到继续发送body的response之间的时间等待。

    如果需要使用这些超时,需要到Transport中去设置,方法如下所示:

    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,
        },
    }
    

    可以看到这其中没有单独控制Do方法超时时间的设置,如果需要的话可以使用context自行实现。

    3.2. 控制重定向

    在Client的字段中,有一个CheckRedirect,此字段就是用来控制重定向的函数,如果没有定义此字段的话,将会使用默认的defaultCheckRedirect方法。

    默认的转发策略是最多转发10次。

    在转发的过程中,某一些包含安全信息的Header,比如AuthorizationWWW-AuthenticateCookie等,如果转发是跨域的,那么这些Header不会复制到新的请求中。

    http的重定向判断会默认处理以下状态码的请求:

    • 301 (Moved Permanently)
    • 302 (Found)
    • 303 (See Other)
    • 307 (Temporary Redirect)
    • 308 (Permanent Redirect)

    301、302和303请求将会改用Get访问新的请求,而307和308会使用原有的请求方式。

    那么,我们如何去控制重定向的次数,甚至是禁止重定向呢?这里其实就需要我们自己去实现一个CheckRedirect函数了,首先我们来看看默认的defaultCheckRedirect方法:

    func defaultCheckRedirect(req *Request, via []*Request) error {
        if len(via) >= 10 {
            return errors.New("stopped after 10 redirects")
        }
        return nil
    }
    

    第一个参数req是即将转发的request,第二个参数 via是已经请求过的requests。可以看到其中的逻辑是判断请求过的request数量,大于等于10的时候返回一个error,这也说明默认的最大重定向次数为10次,当此函数返回error时,即是重定向结束的时候。

    所以如果需要设置重定向次数,那么复制一份这个函数,修改函数名字和其中if判断的数字,然后在生成Client时设定到Client即可:

    client := &http.Client{
        CheckRedirect: yourCheckRedirect,
    }
    

    或者:

    client := &http.Client{}
    client.CheckRedirect = yourCheckRedirect
    

    禁止重定向则可以把判断数字修改为0。最好相应地修改errors中提示的信息。

    3.3. CookieJar管理

    可以看到Client结构体中还有一个Jar字段,类型为CookieJar,这是Client用来管理Cookie的对象。

    如果在生成Client时,没有给这个字段赋值,使其为nil的话,那么之后Client发起的请求将只会带上Request对象中指定的Cookie,请求响应中由服务器返回的Cookie也不会被保存。所以如果需要自动管理Cookie的话,我们还需要生成并设定一个CookieJar对象:

    options := cookiejar.Options{
        PublicSuffixList: publicsuffix.List
    }
    jar, err := cookiejar.New(&options)
    client := &http.Client{
        Jar: jar,
    }
    

    这里的publicsuffix.List是一个域的公共后缀列表,是一个可选的选项,设置为nil代表不启用。但是不启用的情况下会使Cookie变得不安全:意味着foo.com的HTTP服务器可以为bar.com设置cookie。所以一般来说最好启用。

    如果嫌麻烦不想启用PublicSuffixList,可以将其设置为nil,如下即可:

    jar, err := cookiejar.New(nil)
    client := &http.Client{
        Jar: jar,
    }
    

    publicsuffix.List的实现位于golang.org/x/net/publicsuffix,需要额外下载,使用的时候也需要导入:

    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,而应该使用http提供的NewRequest方法来生成Request,此方法中做了一些生成Request的默认设置,以下是NewRequest的函数签名:

    func NewRequest(method, url string, body io.Reader) (*Request, error)
    

    参数中methodurl两个是必备参数,而body参数,在使用没有body的请求方法时,传入nil即可。

    配置好Request之后,使用Client对象的Do方法,就可以将Request发送出去,以下是示例:

    req, err := NewRequest("GET", "https://www.baidu.com", nil)
    resp, err := client.Do(req)
    

    4.1. Method

    请求方法,必备的参数,如果为空字符则表示Get请求。

    注:Go的HTTP客户端不支持CONNECT请求方法。

    4.2. URL

    一个被解析过的url结构体。

    4.3. Proto

    HTTP协议版本。

    在Go中,HTTP请求会默认使用HTTP1.1,而HTTPS请求会默认首先使用HTTP2.0,如果目标服务器不支持,握手失败后才会改用HTTP1.1

    如果希望强制使用HTTP2.0的协议,那么需要使用golang.org/x/net/http2这个包所提供的功能。

    4.4. 发起Post请求

    如果要使用Request发起Post请求,提交表单的话,可以用到它的PostForm字段,这是一个类型为url.Values的字段,以下为示例:

    req, err := NewRequest("Post", "https://www.baidu.com", nil)
    req.PostForm.Add("key", "value")
    

    如果你Post提交的不是表单数据,那么你需要将其封装成io.Reader类型,并在NewRequest函数中传递进去。

    4.4. 设置Header

    Header的类型是http.Header,其中包含着之前请求中返回的header和client发送的header。

    可以使用这种方式设置Header:

    req, err := NewRequest("Get", "https://www.baidu.com", nil)
    req.Header.Add("key", "value")
    

    Header还有一些SetDel等方法可以使用。

    4.5. 添加Cookie

    前文我们已经介绍了如何在Client中启用一直使用的CookieJar,使用它可以自动管理获得的Cookie。

    但很多时候我们也需要给特定的请求手动设置Cookie,这个时候就可以使用Request对象的AddCookie方法,这是其函数签名:

    func (r *Request) AddCookie(c *Cookie)
    

    要注意的是,其传入的参数是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
    }
    

    其中只有NameValue是必须的,所以以下是添加Cookie的示例:

    c := &http.Cookie{
        Name:  "key",
        Value: "value",
    }
    req.AddCookie(c)
    

    五、Transport

    TransportClient中的一个类型,用于控制传输过程,是Client实际发起请求的底层实现。如果没有给这个字段初始化相应的值,那么将会使用默认的DefaultTransport

    Transport承担起了Client中连接池的功能,它会将建立的连接缓存下来,这可能会在访问大量不同网站时,留下太多打开的连接,这可以使用Transport中的方法进行关闭。

    首先来看一下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
    }
    

    由于TransportClient内部请求的实际发起者,所以内容会比较多,1.6之后的版本也添加了许多新的字段,这里我们来讲解常见的一些字段。

    5.1. 拨号

    由于Client中设置的Timeout范围比较宽,而在生产环境中我们可能需要更为精细的超时控制,在Dial拨号中可以设置几个超时时间。

    在较新的版本中,Dial这个字段已经不再被推荐使用,取而代之的是DialContext,设置这个字段,需要借助于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
    }
    

    这其中需要我们设置的并不多,主要是Timeout和KeepAlive。Timeout是Dial这个过程的超时时间,而KeepAlive是连接池中连接的超时时间,如下所示:

    trans := &http.Transport{
        DialContext: (&net.Dialer{
            Timeout: 30 * time.Second,
            KeepAlive: 30 * time.Second,
        }).DialContext,
    }
    

    5.2. 设置代理

    Transport第一个Proxy字段是用来设置代理,支持HTTP、HTTPS、SOCKS5三种代理方式,首先我们来看看如何设置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")
    }
    

    设置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函数将会返回一个Dialer对象,其传入的参数分别为协议、IP端口、账号密码、Dialer,如果代理不需要账号密码验证的话,第三个字段可以设置为nil

    5.3. 连接控制

    众所周知,HTTP1.0协议使用的是短连接,而HTTP1.1默认使用的是长连接,使用长连接则可以复用连接,减少建立连接的开销。

    Transport中实现了连接池的功能,可以将连接保存下来以便下次访问此域名,其中也对连接的数量做出了一定的限制。

    DisableKeepAlives这个字段可以用来关闭长连接,默认值为false,如果有特殊的需求,需要使用短连接,可以设置此字段为true:

    trans := &http.Transport{
        ...
        DisableKeepAlives: true,
    }
    

    除此之外,还可以控制连接的数量和保持时间:

    1. MaxConnsPerHost int - 每个域名下最大连接数量,包括正在拨号的、活跃的、空闲的的连接。

      值为0表示不限制数量。

    2. MaxIdleConns int - 空闲连接的最大数量。

      DefaultTransport中的默认值为100,在需要发起大量连接时偏小,可以根据需求自行设定。

      值为0表示不限制数量。

    3. MaxIdleConnsPerHost int - 每个域名下空闲连接的最大数量。

      值为0则会使用默认的数量,每个域名下只能有两个空闲连接。在对单个网站发起大量连接时,两个连接可能会不够,可以酌情增加此数值。

    4. IdleConnTimeout time.Duration - 空闲连接的超时时间,从每一次空闲开始算。DefaultTransport中的默认值为90秒。

      值为0表示不限制。

    由于Transport负担起了连接池的功能,所以在并发使用时,最好将Transport与Client一起复用,不然可能会造成发起过量的长连接,浪费系统资源。


    六、其他

    6.1. 设置url参数

    在Go的请求方式中,没有给我们提供可以直接设置url参数的方法,所以需要我们自己在url地址中进行拼接。

    url包中提供了一个url.Values类型,其本质的类型为:map[string][]string,可以让我们拼接参数更加简单,如下所示:

    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
    

    七、总结

    总的来说,Go语言中内置的标准库功能是比较完善的,如果要写一个客户端的话,基本不需要用到标准库之外的内容,其可以控制的请求细节也比较多。

    但相较于Python的Requests这类库,需要写的代码依然要多非常多,再加上特别的异常处理机制,在请求过程中要写大量的异常检查语句。需要使用的朋友可以考虑先将请求和异常处理的部分封装以后使用。


    八、示例

    以下是发起Get请求的一个例子:

    // 生成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")
    // 发起请求
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println(err)
    }
    // 设定关闭响应体
    defer resp.Body.Close()
    // 读取响应体
    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/

    相关文章

      网友评论

        本文标题:Golang爬虫全攻略

        本文链接:https://www.haomeiwen.com/subject/wdbkzqtx.html