You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

261 lines
24 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 09 | 反爬虫概述(一):高收益的后端能做哪些基本的事情?
你好我是DS Hunter。从这一讲开始我会给你讲一讲不同研发职能做反爬的方向和技巧。
一般来说公司会将Web站点分为前端、后端两部分也就是前后端分离。必要的时候可能还会做BFF。后端做反爬是一件自然而然的事情很多公司的反爬虫都是后端开始做的。当后端力不从心之后才开始给前端做。前端力不从心了呢就开始由BFF做了。
所以在课程中我们也同样按照这个逻辑先从后端开始说起。我会给你介绍下后端进行规则过滤的基本方法并带你进入正式的key生成与验证斗争中希望你能在这个过程中真正理解后端反爬虫的作用以及成本分析逻辑。另外在这节课的最后我还针对爬虫的计算力进攻进行了拓展如果你有精力也可以随着文字一起思考。
现在,我们就从后端出手的原因开始说起吧。
## 后端出手的原因:服务器压力
我在上面提到了,后端做反爬是一件自然而然的事情。所谓的“自然”,其实是由于服务器(也就是后端)顶不住爬虫的压力才做的。后来慢慢有经验了,才开始做一些类似“提供假信息”、“没有反爬措施的反爬虫系统”这种奇奇怪怪的封杀操作。这个时候,后端出手的原因也慢慢不再仅限于减轻服务器压力,而是开始加入商业上的博弈了。
## 后端反爬虫的具体做法
首先你要知道,线上爬虫其实很多,不仅仅有竞对,还有一些练手的爬虫。这些爬虫特别可怕,它们往往极不专业,很容易把服务器打挂。这些爬虫,你可以尽情封杀。
而另一些竞对爬虫则完全不同,如果真的封杀了,即使胜利了,也会导致更多的问题。说是“皮洛士式的胜利”也不为过。因此两者要使用不同的策略“区别对待”,分开处理。这一讲,我们就主要针对竞对爬虫的骚扰给出具体的解决方案。
这里我为你提供了四种具体做法分别是特征检测模块、key生成模块、验证模块和随机模块。它们像处于一个有输出又有辅助的团队一样相互独立又彼此协作共同成为了反爬虫路上的有力助手。**在实际应用中,我更推荐你综合使用。**
### 特征检测模块
在[05讲](http://time.geekbang.org/column/article/483022)中我提到了多种的特征检测帮助你定位具体检测的内容。精准定位才会事半功倍。这一讲我们就来看看一些在后端进行的特征检测例如HTTP级别检测中的HTTP Header它们的具体实现方式是什么样的。这里的具体实现方式分为三种在线检测离线检测混合检测。我们一个一个来看。
* **在线检测**
在线检测,顾名思义,**用户请求进来的时候,直接就检测特征,当场判定结果。**就像现在的健康码一样,当场出码,当场判定,过期不看。
这里在线检测的做法,其实就是写个算法,针对近期的流量做样本,合并做一个集合。在集合中取特征平均值,最后拿当前流量做比较就可以了。如果后续单个流量过来后,特征不符合统计分布,那么这个流量就是有问题的。
不过这里会存在两个问题:样本本身会是爬虫吗?取平均值的复杂度会不会过高?
针对第一个问题,我们之前提到过,样本要从爬虫少的地方提取,例如订单页,进来就付钱。取付钱接口的流量,相对就纯净很多。如果对方想攻击这个样本,那代价可是太高了。
针对第二个问题取平均值本身是一个O(n)复杂度的事情。因此,当流量高的时候,自然会变慢。不过,变慢的前提是,你使用的“近期的流量”,是最近一段时间的流量,例如最近一分钟。
如果反过来,你的流量池是一个先入先出的队列,那么其实复杂度就没那么麻烦了,因为流量数将是一个定值。而线上系统变慢的情况还是不可避免的。但是,系统性能问题对于很多职能部门来说是不可容忍的:要知道,其他团队费尽心思才节约出一点时间,被你一下子全耗光了,别人一定会和你有争议的。
针对性能问题,我们会有离线检测来解决。
* **离线检测**
离线检测,顾名思义,就是把流量放到线下,抽时间慢慢算,算好了给线上用就可以了。通常,**我们对流量规则的实时性要求并没有特别高的时候,就会采取这种方法。**
这里我们提到了一个新的词,规则。这个词我们后续也会经常使用,也会和其它的词进行搭配,比如规则引擎。什么是规则?这里我们试着下个定义。
> **规则rule**
>
> 使用任何技术手段对线上请求特征按照指定的条件condition或方法callback进行检测验证并执行指定操作的过程。在部分系统里这个也被称为过滤器filter
回到离线检测的部分。其实离线检测和在线检测的方法是一样的但是操作上会有些区别。操作时你可以把固定时间间隔例如一分钟的流量放到一个池里面然后定期跑计算规则例如上面提到的平均值或者IP频次都可以。接着再把规则推到规则引擎去线上系统直接使用规则引擎产生的规则即可。
这里你应该可以看到,效率大大提升了,但是实时性会大幅下降。
回到我们这门课常用的一句话:小孩子才做选择,大人全都要。是的,我们还可以做混合检测。
* **混合检测**
混合检测其实是针对不同的规则,使用不同的检测模式,并统一部署。
有的规则对实时性要求很高。例如IP频次阈值到达100你就封杀。结果因为使用了离线规则一分钟后才起作用那一分钟内人家直接干上个一万次请求这时候你封杀人家人家直接走就可以了呀一万次够用了呀。
有的规则对实时性要求不高但是计算量非常大那么就妥妥地走离线检测。例如HTTP Header同一个爬虫上一分钟犯的错误下一分钟还会犯。这样的情况下抓慢点也没关系亡羊补牢嘛。而只要抓到了对方又没办法立刻变更策略他要改代码反应更慢。因此可以走离线。
此外,离线策略不一定非得是一分钟,也可能是跑个一天,这种大规模策略甚至可以放在业务低谷期来操作,我们可以做一个在线离线混合部署,最大规模降低成本。
也就是说白天业务高峰跑在线策略。晚上业务低谷跑离线Job机器闲着也是闲着低谷期利用起来反而提高了利用率跑完第二天还是可以接着用。成本越低从另一个角度来看也就是收益越大后续和财务谈ROI也更有底气。
### key生成
从这里开始我们讨论的就是key的相关检测了。key生成可以认为是反爬虫的基本操作了市面上几乎所有的反爬虫系统都逃不开这一套下发加密key客户端执行解密然后上传解密后的key服务端验证是否合法。虽然做法千差万别但是原理都没什么创新。
这个做法和我们日常生活里面验证身份的流程非常相似,这相当于给每个人发一个员工卡,上班时要用员工卡验证身份。而我们主要的工作,就是验证员工刷的是不是发的那张卡。
你可能会疑惑为什么验证模块不和key生成模块放在一起呢其实出于服务器压力的分配问题生成key和验证key不一定在同一个集群实现。这样可以实现解耦避免强依赖的产生导致出问题的时候无法关闭系统不得已进行熔断操作。因此我们很可能需要一个独立的验证模块来进行key的验证操作。
那在key生成的这个版块我们只讨论一个问题如何生成一个key
首先生成一个随机数就可以了吗如果生成随机数那么必然要面对存储问题。不然在验证key的时候你如何判断这个是自己生成的那个而不是客户端乱编了一个呢
因此,**我们需要使用的,是一个伪随机的逻辑**。也就是说,要有随机性,但是也要有规律。随机性是对爬虫来说的,有规律是对验证来说的。基于这样的操作方向,我们需要进行的就是 **SKU与时间混合**的操作了。
SKUStock Keeping Unit最小存货单位也就是我们常说的商品ID。我们前面论证过反爬虫方会在指定时间内进行价格的变化那么如何在不断变化的价格中添加一个伪随机的key生成逻辑呢我一步步来说。
首先一个比较简单的key生成办法是
```plain
md5(sku+current_hour)
```
这样我们就实现了当前商品当前小时内key不变那么验证模块也就可以做到针对指定的key。
你可能会觉得一个商品key一直一样是很容易被抓的。但实际上这不一定会被抓因为前端解密key会很复杂。其次指定商品一小时又不变价他没有重复抓的必要。
这里可能会出现的漏洞反而是很容易被抓“规律”。对方多试几次很可能猜出你的后端加密规则怎么办呢我们都知道MD5是公开算法那么其实防止被抓规律的做法就很简单了加盐。
具体加什么呢一个比较阴险的做法是加检测规则。例如加http头。这样每个人就会变得不一样。这个时候我们的伪代码就变成了
```javascript
// fingerprint通过用户特征计算出一个无重复的指纹。
// 用于给md5函数加盐
md5(sku+current_hour+fingerprint(http-header))
```
这样每个用户的header不一样那么每个人的key就不同。
你可能会问但是爬虫一般懒得改header他实际上很可能还是每次拿到一样的key啊
没问题,这个时候,我们上面提到的真随机就可以派上用场了。真随机虽然不好检测,但是他可以用来加盐。具体做法大概是:
```javascript
var left = md5(sku+current_hour+fingerprint(http-header))// 32位
var right = md5(random())// 取一个随机值并md5掉用于混淆key
var result = xor(left, right)+right// 两个key异或作为新key的leftright不变。
```
这里的XOR是一个全字段异或的循环。为什么要这样操作呢因为32位的key就这样变成了64位并且规律性极差。你验证key的时候呢取前32位和后32位再异或回来就能拿到原来的left和right了。**right是随机值丢弃掉****。****left是key验证即可。**这样就实现了,对爬虫随机,对自己有规律。
### 验证模块
验证模块是反爬虫的核心下发的key就是为了传回来做验证。在验证模块我们的核心就是如何快速准确地验证key
key验证其实就是key再次生成的过程。我们知道下发key和验证key发生在同一session内那么它们的上下文就是一致的**我们可以直接再生成一次key与请求带来的key比对即可。**
这时候你就会发现了这种伪随机key的最大好处就是无需存储所以没有存储压力因此效率极高到这里我们就解决了“快速又准确地验证”这个难题。
这里我可以给你拓展一下。其实有些拦截操作还可以放在生成key那里而不是验证key这里。具体怎么做呢举个封杀某个header的例子吧。如果某个HTTP Header有问题那么你在生成key的时候可以有意修改掉一位。也就是说你可以假装不知道这个请求是来自爬虫的然后直接生成一个错误的key给客户端。那么回头这里在不受任何影响直接生成key的时候之前生成的那个key是一定验证不过的。这样对方还以为自己的key破解有问题。
那么拦截操作放在生成key那里和放在验证key这里有什么区别吗
区别很明显生成key和验证key因为算法一致所以可以放在两个集群。而放在生成key那里是可以缓解验证key集群的压力的。要知道虽然你生成key也验证key但是实际上很多低级爬虫是不带key直接打验证服务器的。因此验证模块的压力天生比生成的地方大一些所以我们要减少操作避免崩溃。而生成key的崩溃了我们可以简单地关闭验证模块来实现熔断。而验证模块崩了……你确定你一定关得掉验证模块吗它可崩了啊
### 随机模块
随机模块,也就是随机筛选。它是验证模块的一部分,不过,“做随机筛选”这件事是一个可选项。所以这里给你单独讲解。
在随机模块,可能会出现两种做法。首先,随机跳过,其实就是在校验的时候,我们可能随机跳过一部分请求,不校验。第二种做法,随机封杀,也就是说我们也可能随机封杀一部分请求。至于冤枉真用户?冤枉就冤枉了。两种做法看起来都有些不可思议,但是它们还是有着实际意义的。我们一个个来看。
* **随机跳过**
首先,随机跳过。随机跳过本身并不能拦截爬虫,但是它有一个战略意义,就是让爬虫方困惑,增加破解成本。
对于爬虫方来说反爬虫方给出100%的假数据和给出20%的假数据其实是没什么区别的。哪怕只有5%的假数据,这一整批数据他都不敢用的,毕竟不知道哪些是假的。
这里我给你拓展一个情境,那就是降低假数据概率,会导致一个情况:爬虫方有可能在拿到真数据的那一瞬间,就觉得自己胜利了,然后代码直接上生产。这样生产上实际拿到的数据还是不准的。
这时候你会疑惑一件事情:那,我要是给太少假数据了,岂不是没什么意义?那我给多少假数据合适呢?
我个人的习惯是真数据占比数字通常设置为79.4%,其余的就是假数据了。
这么奇怪的数字是怎么定下来的?
通常来说爬虫方会对自己的代码再三确认也就是说比较谨慎的爬虫工程师试个三次发现没问题也就被deadline给逼上线了不会无休止地测下去的。而79.4%这个数字好就好在,**连续三次真数据的概率,****如果刚好是0.5那就是79.4%****的三次方**那么对方其实测试三次有一半的概率是拿到真数据就会容易误以为自己胜利了。而假数据的概率超过20%,也不低了。
所以这个数字非常合适。当然这是在大规模勾心斗角的爬虫中使用的数据实际上检测到是小爬虫的话你可以不考虑随机跳过的事情100%封杀也没问题。
* **随机封杀**
随机封杀,依然是一个大型爬虫专用的技术,它的关键词只有一个,就是“低”,也就是随机封杀的概率设置会尽可能低一些。为什么呢?
首先,用户是会被误伤的。其次,你可能会说,整体概率设置低了之后,爬虫方被随机被检测到的概率也很低。
但是你要赌的是什么呢?是对方不小心抽检到了这个数据,并开始质疑爬取的数据有问题。而厉害的对手很可能实际爬的没啥问题,他又找不出证据来说明为什么这个数据不对,这很容易引发对方的产品经理和研发之间的争执,最终摩擦起火都有可能。
当然了,如果摩擦起火之类的事情没有发生,那……好像你也没损失什么对不对?所以,概率设置得低,也是可以的。
## 后端反爬虫的效果检测
完成了一系列的反爬虫措施之后,当然要复盘一下效果了。而反爬虫效果检测,一直以来都是个难题。因为你不可能直接去问爬虫:嘿兄弟,你抓到了多少假数据?就算你去问了,爬虫也得问:嘿,兄弟,真数据给我,我验证一下?
所以,我们只能通过一些办法,侧面估算出误伤率以及爬虫占比。
**从技术角度来说,通常有两种方式,打点法和转化率不变法。**前者用来检测误伤率,后者用来检测爬虫量。
所谓打点法,就是在有前后顺序的请求上,一个做反爬,一个不做反爬。举个例子,商品价格页面做反爬,商品付款页面不做反爬,毕竟你也不怕爬虫来给你付款。
这样在价格页面检测到爬虫后就写入一个Cookie相当于给所有爬虫打一个标签。理论上说普通用户是不可能有这个Cookie的。那么我们在付款页面检测这个Cookie能拿到它吗显然是不应该拿到的因为用户是无Cookie的。那么这里检测到多少Cookie就证明有多少用户被误伤了。
而转化率不变法则是另一个操作我们首先定义一下转化率订单量除以UV就是UV的转化率。当然订单量可以先做用户去重这个就看业务的要求了。这里我先假设转化率只与网站的质量有关与其他因素无关或者至少关系很小。大多数情况下商品在全天24小时内转化率不变。
在这个假设下就可以拉一个流量曲线再拉一个订单曲线加权一下试着让两者重合。但是这两者由于爬虫的存在一定是很难重合上的。你可以先想一下你们的低谷期也就是订单为0的时间这个时候不管转化率是多少UV都应该接近0对吧那么我们就可以判定这部分的流量就是全爬虫。
但是扣除掉等量的流量后,订单曲线和流量曲线如果还是很难重合,那么这个不重合的原因,就是不稳定爬虫导致的。这个时候,你就可以一点一点扣流量了:慢慢估算出真实的转化率,进而估算出每个时间段的爬虫。当然,这种做法永远是估算,你不要指望精确计算出爬虫量。
根据上面的打点法和转化率不变法的做法指导,我们可以做离线检测和在线检测了。
### 实时在线检测:打点法
实时在线检测主要用于监控、熔断。而打点法是用于进行误伤检测的,也就是说,它能迅速地识别到用户被误伤、可以做自动熔断反爬虫的功能。因此,这里更推荐用打点法。
需要注意的是由于是实时检测对读取的实时性要求很高就不能走离线库了需要用在线库比如常规的MySQL。不过那服务器顶得住吗
顶得住的。你可以回忆下打点法,实际上拦截打点,是在订单处拦截的,所以数据量是订单量乘以误伤率。这样一来,实际上没有多少数据。而如果真的出现了顶不住的情况,那就证明误伤太多了,可以直接先熔断掉反爬虫再说。
如果说做爬虫最重要的是低调,那做反爬最重要的就是:安全。不惜一切保证安全。
说得更直白一点就是:怂。
### 阶段离线检测:转化率不变法
其实,打点法和转化率不变法都是可以做离线检测的。但是这里主要推荐转化率不变法。
转化率不变本质上是一个计算这个计算需要全天的数据因此全天数据都拿到之后再跑是没问题的。因此计算不需要实时也无需关注查询性能最终走一次Hive查询即可。每天一次按公司最低配置要求就行。此外即使存储丢失了也不是什么大事一切给成本让路。
至于计算时间,和上面特征检测部分的离线检测一样,是可以放在晚上的。同时,整个的效果检测也是可以使用在线离线混部的方式的。也就是说,你可以把统计功能和线上机器部署到一起,晚上用线上机器的剩余计算力进行离线检测。
## 小结
最后,我来给你做个总结。这节课,我们针对后端的反爬虫动作进行了详细的探讨。首先,后端因为天生忍受爬虫压力,主动进行了反爬虫的研发任务。那么针对爬虫进攻的目的与做法的不同,我们的应对策略也是不同的。
在中间的部分我们一共提到了四种后端反爬虫的具体做法它们彼此独立但又相互配合。除了常规的特征检测我们一定要在后端做掉以外还可以通过后端生成key并验证key来实现请求的身份校验。当然一些勾心斗角的价格操作即[02](http://time.geekbang.org/column/article/480844)讲中那些奇奇怪怪的价格迷惑方式)也可以在这里处理掉。而第四种反爬虫做法,“随机模块”,也是你的一个反爬备选项。
当然,别忘了在进行了那么多的反爬虫动作之后,后续的反爬虫效果检测也是后端义不容辞的责任之一。
这里,我也给你提供了一张这一讲的脑图来进行复习。针对四种动作的“独立又配合”的性质,我给每个动作增加了一个小的标签,希望能够帮助你理解它们之间微妙的关联。
![](https://static001.geekbang.org/resource/image/3e/9e/3e78fd1570c1b522d16a327f13f9a09e.jpg?wh=9000x4633)
下一讲,我们可以看下,前端这个反爬虫的主战场,是如何与爬虫正面刚的。
## 思考题
好了,又到了愉快的思考题时间!还是三选一的老规矩,你可以任选一个问题在留言区和我一起讨论。
1. 关于随机跳过爬虫的问题,会导致拦截率的下降,如何与老板解释这个事情呢?
2. 除了MD5伪随机的方式你还能想到什么办法实现不存储key
3. 在线离线混合部署你认为是应该部署到同一台机器上还是应该用两个Docker共享资源它们的优缺点分别是什么
期待你在评论区的分享,我会及时回复你。今天诗句的下方,也有关于计算力进攻的探讨,如果你有精力,也可以和我一起在评论区讨论。反爬无定式,我们一起探索。
![](https://static001.geekbang.org/resource/image/04/b6/0403a8f8cbbf11c4d3f4997fafc155b6.jpg?wh=1500x1615)
## 扩展:难以破解的计算力进攻
爬虫方的计算力攻击是个很尴尬的事情,目前来讲,并且没有真正完美的防范措施。
我们都知道,反爬是需要消耗计算力的。虽然和爬虫相比,我们可以做到少消耗资源,但是还是不能做到不消耗资源。
举个例子生成key的服务虽然我们上面介绍的MD5伪随机已经是优化过的方案了最早是走存储key的可以做到真随机。
但是如果爬虫知道了这个逻辑他拼命地访问生成key的接口生成key访问之后也不验证单纯地拼命去消耗。这样的进攻会导致大量的key被生成、存储但是没有人消费最终积压在存储系统里压爆反爬虫系统。
也许你会继续问: 这样对爬虫又什么好处吗?
当然有,反爬虫系统挂了,结果就是会自动熔断,系统以无反爬的状态运行。就像每天调戏保安,保安累得全睡着了,小偷不就可以随便进去偷东西了吗?
所以这也是我们采取MD5的伪随机key的主要原因。
除此之外,你可以压测一下自己的反爬系统,看一下哪里是系统瓶颈,任何一环被打挂了,爬虫都能肆意妄为,这是一个很可怕的事情。
也许你会说我们是不是可以对这一切做一些防范比如拉取key的接口做些什么措施让爬虫无法来乱搞事情
嗯?有没有感觉这是个递归的死结?你说的是不是**给反爬虫系统做一个反爬虫系统?那么这个给反爬虫系统做保护的反爬虫系统,又由谁来保护呢?**
所以,我们可以做一些基本的防护,比如规则封锁。但是指望完全防护住,注定是一个不现实的想法,需要早早放弃。