當前位置: 妍妍網 > 碼農

哈哈,有人爬我網站,我把他教育了一頓!

2024-03-08碼農

作為一個站長,你是不是對爬蟲不勝其煩? 爬蟲天天來爬,速度又快,頻率又高,伺服器的大量資源被白白浪費。

看這篇文章的你有福了,我們今天一起來報復一下爬蟲,直接把爬蟲的伺服器給幹當機。

本文有一個前提:你已經知道某個請求是爬蟲發來的了,你不滿足於單單遮蔽對方,而是想搞死對方。

很多人的爬蟲是使用Requests來寫的,如果你閱讀過Requests的文件,那麽你可能在文件中的 Binary Response Content [1] 這一小節,看到這樣一句話:

The gzip and deflate transfer-encodings are automatically decoded for you.
(Request)會自動為你把gzip和deflate轉碼後的數據進行解碼

網站伺服器可能會使用 gzip 壓縮一些大資源,這些資源在網路上傳輸的時候,是壓縮後的二進制格式。客戶端收到返回以後,如果發現返回的Headers裏面有一個欄位叫做 Content-Encoding ,其中的值包含 gzip ,那麽客戶端就會先使用 gzip 對數據進行解壓,解壓完成以後再把它呈現到客戶端上面。瀏覽器自動就會做這個事情,使用者是感知不到這個事情發生的。而 requests Scrapy 這種網路請求庫或者爬蟲框架,也會幫你做這個事情,因此你不需要手動對網站返回的數據解壓縮。

這個功能原本是一個方便開發者的功能,但我們可以利用這個功能來做報復爬蟲的事情。

我們首先寫一個客戶端,來測試一下返回 gzip 壓縮數據的方法。

我首先在硬碟上建立一個文字檔案 text.txt ,裏面有兩行內容,如下圖所示:

然後,我是用 gzip 命令把它壓縮成一個 .gz 檔:

cat text.txt | gzip > data.gz

接下來,我們使用FastAPI寫一個HTTP伺服器 server.py

from fastapi import FastAPI, Response
from fastapi.responses import FileResponse

app = FastAPI()

@app.get('/')
defindex():
resp = FileResponse('data.gz')
return resp

然後使用命令 uvicorn server:app 啟動這個服務。

接下來,我們使用requests來請求這個介面,會發現返回的數據是亂碼,如下圖所示:

返回的數據是亂碼,這是因為伺服器沒有告訴客戶端,這個數據是 gzip 壓縮的,因此客戶端只有原樣展示。由於壓縮後的數據是二進制內容,強行轉成字串就會變成亂碼。

現在,我們稍微修改一下 server.py 的程式碼,透過Headers告訴客戶端,這個數據是經過 gzip 壓縮的:

from fastapi import FastAPI, Response
from fastapi.responses import FileResponse

app = FastAPI()

@app.get('/')
defindex():
resp = FileResponse('data.gz')
resp.headers['Content-Encoding'] = 'gzip'# 說明這是gzip壓縮的數據
return resp

修改以後,重新啟動伺服器,再次使用requests請求,發現已經可以正常顯示數據了:

這個功能已經展示完了,那麽我們怎麽利用它呢?這就不得不提到壓縮檔的原理了。

檔之所以能壓縮,是因為裏面有大量重復的元素,這些元素可以透過一種更簡單的方式來表示。壓縮的演算法有很多種,其中最常見的一種方式,我們用一個例子來解釋。假設有一個字串,它長成下面這樣:

1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111

我們可以用5個字元來表示: 192個1 。這就相當於把192個字元壓縮成了5個字元,壓縮率高達97.4%。

如果我們可以把一個1GB的檔壓縮成1MB,那麽對伺服器來說,僅僅是返回了1MB的二進制數據,不會造成任何影響。但是對客戶端或者爬蟲來說,它拿到這個1MB的數據以後,就會在記憶體中把它還原成1GB的內容。這樣一瞬間爬蟲占用的記憶體就增大了1GB。如果我們再進一步增大這個原始數據,那麽很容易就可以把爬蟲所在的伺服器記憶體全部沾滿,輕者伺服器直接殺死爬蟲行程,重則爬蟲伺服器直接當機。

你別以為這個壓縮比聽起來很誇張,其實我們使用很簡單的一行命令就可以生成這樣的壓縮檔。

如果你用的是Linux,那麽請執行命令:

dd if=/dev/zero bs=1M count=1000 | gzip > boom.gz

如果你的電腦是macOS,那麽請執行命令:

dd if=/dev/zero bs=1048576 count=1000 | gzip > boom.gz

執行過程如下圖所示:

生成的這個 boom.gz 檔只有995KB。但是如果我們使用 gzip -d boom.gz 對這個檔解壓縮,就會發現生成了一個1GB的 boom 檔,如下圖所示:

只要大家把命令裏面的 count=1000 改成一個更大的數位,就能得到更大的檔。

我現在把 count 改成 10 ,給大家做一個演示(不敢用1GB的數據來做測試,害怕我的Jupyter崩潰)。生成的 boom.gz 檔只有10KB:

伺服器返回一個10KB的二進制數據,沒有任何問題。

現在我們用requests去請求這個介面,然後檢視一下 resp 這個物件占用的記憶體大小:

可以看到,由於requests自動會對返回的數據解壓縮,因此最終獲得的resp物件竟然有10MB這麽大。

如果大家想使用這個方法,一定要先確定這個請求是爬蟲發的,再使用。否則被你幹死的不是爬蟲而是真實使用者就麻煩了。

本文的寫作過程中,參考了文章 網站gzip炸彈 – 王春偉的技術部落格 [2] ,特別感謝原作者。

參考文獻

[1] Binary Response Content: https://2.python-requests.org/en/master/user/quickstart/#binary-response-content

[2] 網站gzip炸彈 – 王春偉的技術部落格: http://da.dadaaierer.com/?p=577

👇🏻 點選下方閱讀原文,獲取魚皮往期編程幹貨。

往期推薦