本文首發於 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。
對不起,又迫害小破站了,我回去就衝大會員去。別打我;-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維護。