一日一技:反爬蟲的極致手段,幾行代碼直接炸了爬蟲服務(wù)器
作為一個(gè)站長,你是不是對爬蟲不勝其煩?爬蟲天天來爬,速度又快,頻率又高,服務(wù)器的大量資源被白白浪費(fèi)。
看這篇文章的你有福了,我們今天一起來報(bào)復(fù)一下爬蟲,直接把爬蟲的服務(wù)器給干死機(jī)。
本文有一個(gè)前提:你已經(jīng)知道某個(gè)請求是爬蟲發(fā)來的了,你不滿足于單單屏蔽對方,而是想搞死對方。
很多人的爬蟲是使用Requests來寫的,如果你閱讀過Requests的文檔,那么你可能在文檔中的Binary Response Content[1]這一小節(jié),看到這樣一句話:
The gzip and deflate transfer-encodings are automatically decoded for you.
(Request)會自動為你把gzip和deflate轉(zhuǎn)碼后的數(shù)據(jù)進(jìn)行解碼
網(wǎng)站服務(wù)器可能會使用gzip壓縮一些大資源,這些資源在網(wǎng)絡(luò)上傳輸?shù)臅r(shí)候,是壓縮后的二進(jìn)制格式??蛻舳耸盏椒祷匾院螅绻l(fā)現(xiàn)返回的Headers里面有一個(gè)字段叫做Content-Encoding,其中的值包含gzip,那么客戶端就會先使用gzip對數(shù)據(jù)進(jìn)行解壓,解壓完成以后再把它呈現(xiàn)到客戶端上面。瀏覽器自動就會做這個(gè)事情,用戶是感知不到這個(gè)事情發(fā)生的。而requests、Scrapy這種網(wǎng)絡(luò)請求庫或者爬蟲框架,也會幫你做這個(gè)事情,因此你不需要手動對網(wǎng)站返回的數(shù)據(jù)解壓縮。
這個(gè)功能原本是一個(gè)方便開發(fā)者的功能,但我們可以利用這個(gè)功能來做報(bào)復(fù)爬蟲的事情。
我們首先寫一個(gè)客戶端,來測試一下返回gzip壓縮數(shù)據(jù)的方法。
我首先在硬盤上創(chuàng)建一個(gè)文本文件text.txt,里面有兩行內(nèi)容,如下圖所示:
然后,我是用gzip命令把它壓縮成一個(gè).gz文件:
cat text.txt | gzip > data.gz
接下來,我們使用FastAPI寫一個(gè)HTTP服務(wù)器server.py:
from fastapi import FastAPI, Response
from fastapi.responses import FileResponse
app = FastAPI()
@app.get('/')
def index():
resp = FileResponse('data.gz')
return resp
然后使用命令uvicorn server:app啟動這個(gè)服務(wù)。
接下來,我們使用requests來請求這個(gè)接口,會發(fā)現(xiàn)返回的數(shù)據(jù)是亂碼,如下圖所示:
返回的數(shù)據(jù)是亂碼,這是因?yàn)榉?wù)器沒有告訴客戶端,這個(gè)數(shù)據(jù)是gzip壓縮的,因此客戶端只有原樣展示。由于壓縮后的數(shù)據(jù)是二進(jìn)制內(nèi)容,強(qiáng)行轉(zhuǎn)成字符串就會變成亂碼。
現(xiàn)在,我們稍微修改一下server.py的代碼,通過Headers告訴客戶端,這個(gè)數(shù)據(jù)是經(jīng)過gzip壓縮的:
from fastapi import FastAPI, Response
from fastapi.responses import FileResponse
app = FastAPI()
@app.get('/')
def index():
resp = FileResponse('data.gz')
resp.headers['Content-Encoding'] = 'gzip' # 說明這是gzip壓縮的數(shù)據(jù)
return resp
修改以后,重新啟動服務(wù)器,再次使用requests請求,發(fā)現(xiàn)已經(jīng)可以正常顯示數(shù)據(jù)了:
這個(gè)功能已經(jīng)展示完了,那么我們怎么利用它呢?這就不得不提到壓縮文件的原理了。
文件之所以能壓縮,是因?yàn)槔锩嬗写罅恐貜?fù)的元素,這些元素可以通過一種更簡單的方式來表示。壓縮的算法有很多種,其中最常見的一種方式,我們用一個(gè)例子來解釋。假設(shè)有一個(gè)字符串,它長成下面這樣:
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
我們可以用5個(gè)字符來表示:192個(gè)1。這就相當(dāng)于把192個(gè)字符壓縮成了5個(gè)字符,壓縮率高達(dá)97.4%。
如果我們可以把一個(gè)1GB的文件壓縮成1MB,那么對服務(wù)器來說,僅僅是返回了1MB的二進(jìn)制數(shù)據(jù),不會造成任何影響。但是對客戶端或者爬蟲來說,它拿到這個(gè)1MB的數(shù)據(jù)以后,就會在內(nèi)存中把它還原成1GB的內(nèi)容。這樣一瞬間爬蟲占用的內(nèi)存就增大了1GB。如果我們再進(jìn)一步增大這個(gè)原始數(shù)據(jù),那么很容易就可以把爬蟲所在的服務(wù)器內(nèi)存全部沾滿,輕者服務(wù)器直接殺死爬蟲進(jìn)程,重則爬蟲服務(wù)器直接死機(jī)。
你別以為這個(gè)壓縮比聽起來很夸張,其實(shí)我們使用很簡單的一行命令就可以生成這樣的壓縮文件。
如果你用的是Linux,那么請執(zhí)行命令:
dd if=/dev/zero bs=1M count=1000 | gzip > boom.gz
如果你的電腦是macOS,那么請執(zhí)行命令:
dd if=/dev/zero bs=1048576 count=1000 | gzip > boom.gz
執(zhí)行過程如下圖所示:
生成的這個(gè)boom.gz文件只有995KB。但是如果我們使用gzip -d boom.gz對這個(gè)文件解壓縮,就會發(fā)現(xiàn)生成了一個(gè)1GB的boom文件,如下圖所示:
只要大家把命令里面的count=1000改成一個(gè)更大的數(shù)字,就能得到更大的文件。
我現(xiàn)在把count改成10,給大家做一個(gè)演示(不敢用1GB的數(shù)據(jù)來做測試,害怕我的Jupyter崩潰)。生成的boom.gz文件只有10KB:
服務(wù)器返回一個(gè)10KB的二進(jìn)制數(shù)據(jù),沒有任何問題。
現(xiàn)在我們用requests去請求這個(gè)接口,然后查看一下resp這個(gè)對象占用的內(nèi)存大?。?/p>
可以看到,由于requests自動會對返回的數(shù)據(jù)解壓縮,因此最終獲得的resp對象竟然有10MB這么大。
如果大家想使用這個(gè)方法,一定要先確定這個(gè)請求是爬蟲發(fā)的,再使用。否則被你干死的不是爬蟲而是真實(shí)用戶就麻煩了。
本文的寫作過程中,參考了文章網(wǎng)站gzip炸彈 – 王春偉的技術(shù)博客[2],特別感謝原作者。
參考文獻(xiàn)
[1] Binary Response Content: https://2.python-requests.org/en/master/user/quickstart/#binary-response-content
[2] 網(wǎng)站gzip炸彈 – 王春偉的技術(shù)博客: http://da.dadaaierer.com/?p=577