本文首发于 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维护。
有疑问加站长微信联系(非本文作者)