作为站长,你是否被爬虫惹恼了?大量资源被浪费。看到这篇文章的你有福了,今天就来报复爬虫吧,直接干掉爬虫的服务器。这篇文章有一个前提:你已经知道一个请求是爬虫发出的,你不满足于简单的屏蔽对方,而是想干掉对方。很多人的爬虫都是用Requests写的。如果您已阅读请求文档,您可能会在文档的二进制响应内容[1]部分看到这句话:Thegzipanddeflatetransfer-encodingsareAutomaticallydecodedforyou。(Request)将自动解码gzip并为您压缩转码后的数据。网站服务器可能会使用gzip压缩一些大的资源。这些资源在网络上传输时,都是压缩的二进制格式。客户端收到返回后,如果发现返回的Headers中有一个名为Content-Encoding的字段,其值包含gzip,那么客户端会先用gzip解压数据,然后呈现给客户端以上解压完成后。浏览器会自动执行此操作,用户不会意识到这一点。而requests、Scrapy等网络请求库或者爬虫框架也会帮你做这件事,所以你不需要手动解压网站返回的数据。这个功能本来是开发者方便的功能,但是我们可以利用这个功能来报复爬虫。我们先写一个客户端来测试返回gzip压缩数据的方法。我先在硬盘上建立一个文本文件text.txt,里面有两行,如下图所示:然后,我用gzip命令把它压缩成一个.gz文件:cattext.txt|gzip>data.gz接下来我们使用FastAPI写一个HTTP服务器server.py:fromfastapiimportFastAPI,Responsefromfastapi.responsesimportFileResponseapp=FastAPI()@app.get('/')defindex():resp=FileResponse('data.gz')returnresp然后使用命令uvicornserver:app启动服务。接下来我们使用requests请求这个接口,会发现返回的数据是乱码,如下图:返回的数据是乱码,因为服务端没有告诉客户端数据是经过gzip压缩的,所以客户只能按原样显示。由于压缩后的数据是二进制内容,如果强行转成字符串,就会变成乱码。现在,我们稍微修改一下server.py的代码,通过Headers告诉客户端这个数据是用gzip压缩的:fromfastapiimportFastAPI,Responsefromfastapi.responsesimportFileResponseapp=FastAPI()@app.get('/')defindex():resp=FileResponse('data.gz')resp.headers['Content-Encoding']='gzip'#表示这是gzip压缩后的数据returnresp修改后,重启服务器,使用requests再次,发现数据可以正常显示了:这个功能已经显示出来了,那我们怎么用呢?这就不得不提到压缩文件的原理了。文件之所以能被压缩,是因为里面有很多重复的元素,可以用更简单的方式来表示。压缩算法有很多种,其中一种最常见的方式,我们用一个例子来解释。假设有一个字符串,它长成下面这样:111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111我们可以用5个字符来表示:192个1。Thisisequivalenttocompressing192charactersinto5characters,andthecompressionrateisashighas97.4%.如果我们可以将一个1GB的文件压缩成1MB,那么对于服务器来说,只返回1MB的二进制数据,没有任何影响。但是对于客户端或者爬虫来说,拿到1MB的数据后,会在内存中恢复到1GB。这样一来,爬虫占用的内存瞬间增加了1GB。如果我们进一步增加原始数据,很容易将爬虫所在服务器的内存填满。轻则服务器直接杀掉爬虫进程,重则爬虫服务器直接崩溃。不要觉得这个压缩比听起来很夸张。其实我们可以通过非常简单的一行命令生成这样一个压缩文件。如果您使用的是Linux,请执行命令:ddif=/dev/zerobs=1Mcount=1000|gzip>boom.gz如果你的电脑是macOS,那么请执行命令:ddif=/dev/zerobs=1048576count=1000|gzip>boom.gz执行过程如下图所示:生成的boom.gz文件只有995KB。但是如果我们使用gzip-dboom.gz来解压这个文件,我们会发现生成了一个1GB的boom文件,如下图所示:只要把命令中的count=1000改大一些,可以获得更大的文件。我现在把计数改成10给大家演示一下(我不敢用1GB的数据来测试,怕我的Jupyter崩溃)。生成的boom.gz文件只有10KB:服务器毫无问题地返回了10KB的二进制数据。现在我们使用requests来请求这个接口,然后查看resp对象占用的内存大小:可以看到,由于requests会自动对返回的数据进行解压,所以最终的resp对象有10MB那么大。如果要使用该方法,在使用前必须先确认请求是由爬虫发出的。不然不是爬虫而是真用户被你干掉就麻烦了。在写这篇文章的过程中,参考了文章网站gzip炸弹——王春伟的技术博客[2],在此感谢原作者。参考文献[1]BinaryRequests内容:https://2.python-requests.org/en/master/user/quickstart/#binary-response-content[2]网站gzip炸弹-王春伟的技术博客:http://da.dadaaierer.com/?p=577
