Golang 爬虫教程 | 解决反爬问题 | 做一个文明的爬虫

2,603 阅读7分钟

本文首发于 imagician.net/archives/93… 。欢迎到我的博客 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()
}

运行上述代码会不停的访问 www.bilibili.com/video/BV1tJ… 这个地址。利用Goribot自带的错误记录工具,很快B站就封禁了我……可以看到下面图片里B站返回的HTTP 403 Access Forbidden

HTTP 403 Access Forbidden

对不起,又迫害小破站了,我回去就冲大会员去。别打我;-D。

侵入式的反爬手段

很多网站上展示的内容,本身就是其产品,包含价值。这类网站会设置一些参数(比如Token)来更精确的鉴别机器。

侵入式的反爬手段

图为例,某站的一个Ajax请求就带有令牌Token、签名Signature、以及Cookie里设置了浏览器标识。

此类技术反爬相当于声明了此信息禁止爬取,这类技术不再本文讨论范围内。

遵守“礼仪”

后文中出现的举例以net/httpGoribot为主,因为那个库是我写的

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/robot…

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维护。