博客 / 詳情

返回

Golang 爬蟲教程 | 解決反爬問題 | 做一個文明的爬蟲

本文首發於 https://imagician.net/archives/93/ 。歡迎到我的博客 https://imagician.net/ 瞭解更多。

前排提示:本文是一個入門級教程,講述基本的爬蟲與服務器關係。諸如無頭瀏覽器、js挖取等技術暫不討論。

面對大大小小的爬蟲應用,反爬是一個經久不衰的問題。網站會進行一些限制措施,以阻止簡單的程序無腦的獲取大量頁面,這會對網站造成極大的請求壓力。

要注意的是,本文在這裏説的是,爬取公開的信息。比如,文章的標題,作者,發佈時間。既不是隱私,也不是付費的數字產品。網站有時會對有價值的數字產品進行保護,使用更復雜的方式也避免被爬蟲“竊取”。這類信息不僅難以爬取,而且不應該被爬取。

網站對公開內容設置反爬是因為網站把訪問者當做“人類”,人類會很友善的訪問一個又一個頁面,在頁面間跳轉,同時還有登錄、輸入、刷新等操作。機器像是“見了鬼”一股腦的“Duang Duang Duang Duang”不停請求某一個Ajax接口,不帶登錄,沒有上下文,加大服務器壓力和各種流量、帶寬、存儲開銷。

比如B站的反爬

package main

import (
    "github.com/zhshch2002/goribot"
    "os"
    "strings"
)

func main() {
    s := goribot.NewSpider(goribot.SpiderLogError(os.Stdout))
    var h goribot.CtxHandlerFun
    h= func(ctx *goribot.Context) {
        if !strings.Contains(ctx.Resp.Text,"按時間排序"){
            ctx.AddItem(goribot.ErrorItem{
                Ctx: ctx,
                Msg: "",
            })
            ctx.AddTask(goribot.GetReq("https://www.bilibili.com/video/BV1tJ411V7eg"),h)
            ctx.AddTask(goribot.GetReq("https://www.bilibili.com/video/BV1tJ411V7eg"),h)
            ctx.AddTask(goribot.GetReq("https://www.bilibili.com/video/BV1tJ411V7eg"),h)
            ctx.AddTask(goribot.GetReq("https://www.bilibili.com/video/BV1tJ411V7eg"),h)
        }
    }
    s.AddTask(goribot.GetReq("https://www.bilibili.com/video/BV1tJ411V7eg"),h)
    s.Run()
}

運行上述代碼會不停的訪問 https://www.bilibili.com/vide... 這個地址。利用Goribot自帶的錯誤記錄工具,很快B站就封禁了我……可以看到下面圖片裏B站返回的HTTP 403 Access Forbidden

HTTP 403 Access Forbidden

對不起,又迫害小破站了,我回去就衝大會員去。別打我;-D。

侵入式的反爬手段

很多網站上展示的內容,本身就是其產品,包含價值。這類網站會設置一些參數(比如Token)來更精確的鑑別機器。

侵入式的反爬手段

圖為例,某站的一個Ajax請求就帶有令牌Token、簽名Signature、以及Cookie裏設置了瀏覽器標識。

此類技術反爬相當於聲明瞭此信息禁止爬取,這類技術不再本文討論範圍內。

遵守“禮儀”

後文中出現的舉例以net/http和Goribot為主, 因為那個庫是我寫的

Goribot提供了許多工具,是一個輕量的爬蟲框架,具體瞭解請見文檔。

go get -u github.com/zhshch2002/goribot

遵守robots.txt

robots.txt是一種存放於網站根目錄下(也就是/robots.txt)的一個文本文件,也就是txt。這個文件描述了蜘蛛可以爬取哪些頁面,不可以爬取哪些。注意這裏説的是允許,robots.txt只是一個約定,沒有別的用處。

但是,一個不遵守robots.txt的爬蟲瞎訪問那些不允許的頁面,很顯然是不正常的(前提是那些被不允許的頁面不是爬取的目標,只是無意訪問到)。這些被robots.txt限制的頁面通常更敏感,因為那些可能是網站的重要頁面。

我們限制自己的爬蟲不訪問那些頁面,可以有效地避免某些規則的觸發。

Goribot中對robots.txt的支持使用了github.com/slyrz/robots。

s := goribot.NewSpider(
    goribot.RobotsTxt("https://github.com", "Goribot"),
)

這裏創建了一個爬蟲,並加載了一個robots.txt插件。其中"Goribot"是爬蟲名字,在robots.txt文件裏對不同名字的爬蟲可以設置不同的規則,此參數與之相對。"https://github.com"是獲取robots.txt的地址,因為前文説過robots.txt只能設置在網站根目錄,且作用域只有同host下的頁面,這裏只需設置根目錄的URL即可。

控制併發、速率

想像一下,你寫了一個爬蟲,只會訪問一個頁面,然後解析HTML。這個程序放在一個死循環裏,循環中不停創建新線程。嗯,聽起來不錯。

對於網站服務器來看,有一個IP,開始很高頻請求,而且流量帶寬越來越大,一回神3Gbps!!!?你這是訪問是來DDos的?果斷ban IP。

之後,你就得到了爬蟲收集到的一堆HTTP 403 Access Forbidden

當然上述只是誇張的例子,沒有人家有那麼大的帶寬……啊,好像加拿大白嫖王家裏就有。而且也沒人那麼寫程序。

控制請求的併發並加上延時,可以很大程度減少對服務器壓力,雖然請求速度變慢了。但我們是來收集數據的,不是來把網站打垮的。

在Goribot中可以這樣設置:

s := goribot.NewSpider(
    goribot.Limiter(false, &goribot.LimitRule{
        Glob: "httpbin.org",
        Rate:        2, // 請求速率限制(同host下每秒2個請求,過多請求將阻塞等待)
    }),
)

Limiter在Goribot中是一個較為複雜的擴展,能夠控制速率、併發、白名單以及隨機延時。更多內容請參考使用文檔。

技術手段

網站把所有請求者當做人處理,把不像人的行為的特徵作為檢測的手段。於是我們可以使程序模擬人(以及瀏覽器)的行為,來避免反爬機制。

UA

作為一個爬蟲相關的開發者,UA肯定不陌生,或者叫User-Agent用户代理。比如你用Chrome訪問量GitHub的網站,HTTP請求中的UA就是由Chrome瀏覽器填寫,併發送到網站服務器的。UA的字面意思,用户代理,也就是説用户通過什麼工具來訪問網站。(畢竟用户不能自己直接去寫HTTP報文吧,開發者除外;-D)

網站可以通過鑑別UA來簡單排除一些機器發出的請求。比如Golang原生的net/http包中會自動設置一個UA,標明請求由Golang程序發出,很多網站就會過濾這樣的請求。

在Golang原生的net/http包中,可以這樣設置UA:(其中"User-Agent"大小寫不敏感)

r, _ := http.NewRequest("GET", "https://github.com", nil)
r.Header.Set("User-Agent", "Goribot")

在Goribot中可以通過鏈式操作設置請求時的UA:

goribot.GetReq("https://github.com").SetHeader("User-Agent", "Goribot")

總是手動設置UA很煩人,而且每次都要編一個UA來假裝自己是瀏覽器。於是我們有自動隨機UA設置插件:

s := goribot.NewSpider(
    goribot.RandomUserAgent(),
)

Referer

Referer是包含在請求頭裏的,表示“我是從哪個URL跳轉到這個請求的?”簡稱“我從哪裏來?”。如果你的程序一直髮出不包含Referer或者其為空的請求,服務器就會發現“誒,小老弟,你從哪來的?神秘花園嗎?gun!”然後你就有了HTTP 403 Access Forbidden

在Golang原生的net/http包中,可以這樣設置Referer:

r, _ := http.NewRequest("GET", "https://github.com", nil)
r.Header.Set("Referer", "https://www.google.com")

在Goribot中可裝配Referer自動填充插件來為新發起的請求填上上一個請求的地址:

s := goribot.NewSpider(
    goribot.RefererFiller(),
)

Cookie

Cookie應該很常見,各種網站都用Cookie來存儲賬號等登錄信息。Cookie本質上是網站服務器保存在客户端瀏覽器上的鍵值對數據,關於Cookie的具體知識可以百度或者谷歌。

創建Goribot爬蟲時會順帶一個Cookie Jar,自動管理爬蟲運行時的Cookie信息。我們可以為請求設置Cookie來模擬人在瀏覽器登錄時的效果。

使用Golang原生的net/http,並啓用Cookie Jar,用Cookie設置登錄:

package main

// 代碼來自 https://studygolang.com/articles/10842 ,非常感謝

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "net/http/cookiejar"
    //    "os"
    "net/url"
    "time"
)

func main() {
    //Init jar
    j, _ := cookiejar.New(nil)
    // Create client
    client := &http.Client{Jar: j}

    //開始修改緩存jar裏面的值
    var clist []*http.Cookie
    clist = append(clist, &http.Cookie{
        Name:    "BDUSS",
        Domain:  ".baidu.com",
        Path:    "/",
        Value:   "cookie  值xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        Expires: time.Now().AddDate(1, 0, 0),
    })
    urlX, _ := url.Parse("http://zhanzhang.baidu.com")
    j.SetCookies(urlX, clist)

    fmt.Printf("Jar cookie : %v", j.Cookies(urlX))
    
    // Fetch Request
    resp, err = client.Do(req)
    if err != nil {
        fmt.Println("Failure : ", err)
    }

    respBody, _ := ioutil.ReadAll(resp.Body)

    // Display Results
    fmt.Println("response Status : ", resp.Status)
    fmt.Println("response Body : ", string(respBody))
    fmt.Printf("response Cookies :%v", resp.Cookies())
}

在Goribot中可以這樣:

s.AddTask(goribot.GetReq("https://www.bilibili.com/video/BV1tJ411V7eg").AddCookie(&http.Cookie{
        Name:    "BDUSS",
        Value:   "cookie  值xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        Expires: time.Now().AddDate(1, 0, 0),
    }),handlerFunc)

如此在稍後的s.Run()中,這一請求將會被設置Cookie且後續Cookie由Cookie Jar維護。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.