爬蟲神器,大幅提升爬蟲效率!
這是「進(jìn)擊的Coder」的第 575?篇技術(shù)分享作者:崔慶才在做爬蟲的時候,我們往往可能這些情況:
- 網(wǎng)站比較復(fù)雜,會碰到很多重復(fù)請求。
- 有時候爬蟲意外中斷了,但我們沒有保存爬取狀態(tài),再次運(yùn)行就需要重新爬取。
還有諸如此類的問題。
那怎么解決這些重復(fù)爬取的問題呢?大家很可能都想到了“緩存”,也就是說,爬取過一遍就直接跳過爬取。
那一般怎么做呢?
比如我寫一個邏輯,把已經(jīng)爬取過的 URL 保存到文件或者數(shù)據(jù)庫里面,每次爬取之前檢查一下是不是在列表或數(shù)據(jù)庫里面就好了。
是的,這個思路沒問題,但有沒有想過這些問題:
- 寫入到文件或者數(shù)據(jù)庫可能是永久性的,如果我想控制緩存的有效時間,那就還得有個過期時間控制。
- 這個緩存根據(jù)什么來判斷?如果僅僅是 URL 本身夠嗎?還有 Request Method、Request Headers 呢,如果它們不一樣了,那還要不要用緩存?
- 如果我們有好多項目,難道都沒有一個通用的解決方案嗎?
的確是些問題,實(shí)現(xiàn)起來確實(shí)要考慮很多問題。
不過不用擔(dān)心,今天給大家介紹一個神器,可以幫助我們通通解決如上的問題。
介紹
它就是 requests-cache,是 requests 庫的一個擴(kuò)展包,利用它我們可以非常方便地實(shí)現(xiàn)請求的緩存,直接得到對應(yīng)的爬取結(jié)果。
- GitHub:https://github.com/reclosedev/requests-cache
- PyPi:https://pypi.org/project/requests-cache/
- 官方文檔:https://requests-cache.readthedocs.io/en/stable/index.html
下面我們來介紹下它的使用。
安裝
安裝非常簡單,使用 pip3 即可:
pip3?install?requests-cache
安裝完畢之后我們來了解下它的基本用法。
基本用法
下面我們首先來看一個基礎(chǔ)實(shí)例:
import?requests
import?time
start?=?time.time()
session?=?requests.Session()
for?i?in?range(10):
????session.get('http://httpbin.org/delay/1')
????print(f'Finished?{i?+?1}?requests')
end?=?time.time()
print('Cost?time',?end?-?start)
這里我們請求了一個網(wǎng)站,是 http://httpbin.org/delay/1,這個網(wǎng)站模擬了一秒延遲,也就是請求之后它會在 1 秒之后才會返回響應(yīng)。
這里請求了 10 次,那就至少得需要 10 秒才能完全運(yùn)行完畢。
運(yùn)行結(jié)果如下:
Finished?1?requests
Finished?2?requests
Finished?3?requests
Finished?4?requests
Finished?5?requests
Finished?6?requests
Finished?7?requests
Finished?8?requests
Finished?9?requests
Finished?10?requests
Cost?time?13.17966604232788
可以看到,這里一共用了13 秒。
那如果我們用上 requests-cache 呢?結(jié)果會怎樣?
代碼改寫如下:
import?requests_cache
import?time
start?=?time.time()
session?=?requests_cache.CachedSession('demo_cache')
for?i?in?range(10):
????session.get('http://httpbin.org/delay/1')
????print(f'Finished?{i?+?1}?requests')
end?=?time.time()
print('Cost?time',?end?-?start)
這里我們聲明了一個 CachedSession,將原本的 Session 對象進(jìn)行了替換,還是請求了 10 次。
運(yùn)行結(jié)果如下:
Finished?1?requests
Finished?2?requests
Finished?3?requests
Finished?4?requests
Finished?5?requests
Finished?6?requests
Finished?7?requests
Finished?8?requests
Finished?9?requests
Finished?10?requests
Cost?time?1.6248838901519775
可以看到,一秒多就爬取完畢了!
發(fā)生了什么?
這時候我們可以發(fā)現(xiàn),在本地生成了一個 demo_cache.sqlite 的數(shù)據(jù)庫。
我們打開之后可以發(fā)現(xiàn)里面有個 responses 表,里面多了一個 key-value 記錄,如圖所示:

我們可以可以看到,這個 key-value 記錄中的 key 是一個 hash 值,value 是一個 Blob 對象,里面的內(nèi)容就是 Response 的結(jié)果。
可以猜到,每次請求都會有一個對應(yīng)的 key 生成,然后 requests-cache 把對應(yīng)的結(jié)果存儲到了 SQLite 數(shù)據(jù)庫中了,后續(xù)的請求和第一次請求的 URL 是一樣的,經(jīng)過一些計算它們的 key 也都是一樣的,所以后續(xù) 2-10 請求就立馬返回了。
是的,利用這個機(jī)制,我們就可以跳過很多重復(fù)請求了,大大節(jié)省爬取時間。
Patch 寫法
但是,剛才我們在寫的時候把 requests 的 session 對象直接替換了。有沒有別的寫法呢?比如我不影響當(dāng)前代碼,只在代碼前面加幾行初始化代碼就完成 requests-cache 的配置呢?
當(dāng)然是可以的,代碼如下:
import?time
import?requests
import?requests_cache
requests_cache.install_cache('demo_cache')
start?=?time.time()
session?=?requests.Session()
for?i?in?range(10):
????session.get('http://httpbin.org/delay/1')
????print(f'Finished?{i?+?1}?requests')
end?=?time.time()
print('Cost?time',?end?-?start)
這次我們直接調(diào)用了 requests-cache 庫的 install_cache 方法就好了,其他的 requests 的 Session 照常使用即可。
我們再運(yùn)行一遍:
Finished?1?requests
Finished?2?requests
Finished?3?requests
Finished?4?requests
Finished?5?requests
Finished?6?requests
Finished?7?requests
Finished?8?requests
Finished?9?requests
Finished?10?requests
Cost?time?0.018644094467163086
這次比上次更快了,為什么呢?因為這次所有的請求都命中了 Cache,所以很快返回了結(jié)果。
后端配置
剛才我們知道了,requests-cache 默認(rèn)使用了 SQLite 作為緩存對象,那這個能不能換???比如用文件,或者其他的數(shù)據(jù)庫呢?
自然是可以的。
比如我們可以把后端換成本地文件,那可以這么做:
import?time
import?requests
import?requests_cache
requests_cache.install_cache('demo_cache',?backend='filesystem')
start?=?time.time()
session?=?requests.Session()
for?i?in?range(10):
????session.get('http://httpbin.org/delay/1')
????print(f'Finished?{i?+?1}?requests')
end?=?time.time()
print('Cost?time',?end?-?start)
這里我們添加了一個 backend 參數(shù),然后指定為 filesystem,這樣運(yùn)行之后本地就會生成一個 demo_cache 的文件夾用作緩存,如果不想用緩存的話把這個文件夾刪了就好了。
當(dāng)然我們還可以更改緩存文件夾的位置,比如:
requests_cache.install_cache('demo_cache',?backend='filesystem',?use_temp=True)
這里添加一個 use_temp 參數(shù),緩存文件夾便會使用系統(tǒng)的臨時目錄,而不會在代碼區(qū)創(chuàng)建緩存文件夾。
當(dāng)然也可以這樣:
requests_cache.install_cache('demo_cache',?backend='filesystem',?use_cache_dir=True)
這里添加一個 use_cache_dir 參數(shù),緩存文件夾便會使用系統(tǒng)的專用緩存文件夾,而不會在代碼區(qū)創(chuàng)建緩存文件夾。
另外除了文件系統(tǒng),requests-cache 也支持其他的后端,比如 Redis、MongoDB、GridFS 甚至內(nèi)存,但也需要對應(yīng)的依賴庫支持,具體可以參見下表:
| Backend | Class | Alias | Dependencies |
|---|---|---|---|
| SQLite | SQLiteCache | 'sqlite' | |
| Redis | RedisCache | 'redis' | redis-py |
| MongoDB | MongoCache | 'mongodb' | pymongo |
| GridFS | GridFSCache | 'gridfs' | pymongo |
| DynamoDB | DynamoDbCache | 'dynamodb' | boto3 |
| Filesystem | FileCache | 'filesystem' | |
| Memory | BaseCache | 'memory' |
比如使用 Redis 就可以改寫如下:
backend?=?requests_cache.RedisCache(host='localhost',?port=6379)
requests_cache.install_cache('demo_cache',?backend=backend)
更多詳細(xì)配置可以參考官方文檔:https://requests-cache.readthedocs.io/en/stable/user_guide/backends.html#backends。
Filter
當(dāng)然,我們有時候也想指定有些請求不緩存,比如只緩存 POST 請求,不緩存 GET 請求,那可以這樣來配置:
import?time
import?requests
import?requests_cache
requests_cache.install_cache('demo_cache2',?allowable_methods=['POST'])
start?=?time.time()
session?=?requests.Session()
for?i?in?range(10):
????session.get('http://httpbin.org/delay/1')
????print(f'Finished?{i?+?1}?requests')
end?=?time.time()
print('Cost?time?for?get',?end?-?start)
start?=?time.time()
for?i?in?range(10):
????session.post('http://httpbin.org/delay/1')
????print(f'Finished?{i?+?1}?requests')
end?=?time.time()
print('Cost?time?for?post',?end?-?start)
這里我們添加了一個 allowable_methods 指定了一個過濾器,只有 POST 請求會被緩存,GET 請求就不會。
看下運(yùn)行結(jié)果:
Finished?1?requests
Finished?2?requests
Finished?3?requests
Finished?4?requests
Finished?5?requests
Finished?6?requests
Finished?7?requests
Finished?8?requests
Finished?9?requests
Finished?10?requests
Cost?time?for?get?12.916549682617188
Finished?1?requests
Finished?2?requests
Finished?3?requests
Finished?4?requests
Finished?5?requests
Finished?6?requests
Finished?7?requests
Finished?8?requests
Finished?9?requests
Finished?10?requests
Cost?time?for?post?1.2473630905151367
這時候就看到 GET 請求由于沒有緩存,就花了 12 多秒才結(jié)束,而 POST 由于使用了緩存,一秒多就結(jié)束了。
另外我們還可以針對 Response Status Code 進(jìn)行過濾,比如只有 200 會緩存,則可以這樣寫:
import?time
import?requests
import?requests_cache
requests_cache.install_cache('demo_cache2',?allowable_codes=(200,))
當(dāng)然我們還可以匹配 URL,比如針對哪種 Pattern 的 URL 緩存多久,則可以這樣寫:
urls_expire_after?=?{'*.site_1.com':?30,?'site_2.com/static':?-1}
requests_cache.install_cache(
????'demo_cache2',?urls_expire_after=urls_expire_after)
這樣的話,site_1.com 的內(nèi)容就會緩存 30 秒,site_2.com/static 的內(nèi)容就永遠(yuǎn)不會過期。
當(dāng)然,我們也可以自定義 Filter,具體可以參見:https://requests-cache.readthedocs.io/en/stable/user_guide/filtering.html#custom-cache-filtering。
Cache Headers
除了我們自定義緩存,requests-cache 還支持解析 HTTP Request / Response Headers 并根據(jù) Headers 的內(nèi)容來緩存。
比如說,我們知道 HTTP 里面有個 Cache-Control 的 Request / Response Header,它可以指定瀏覽器要不要對本次請求進(jìn)行緩存,那 requests-cache 怎么來支持呢?
實(shí)例如下:
import?time
import?requests
import?requests_cache
requests_cache.install_cache('demo_cache3')
start?=?time.time()
session?=?requests.Session()
for?i?in?range(10):
????session.get('http://httpbin.org/delay/1',
????????????????headers={
????????????????????'Cache-Control':?'no-store'
????????????????})
????print(f'Finished?{i?+?1}?requests')
end?=?time.time()
print('Cost?time?for?get',?end?-?start)
start?=?time.time()
這里我們在 Request Headers 里面加上了 Cache-Control 為 no-store,這樣的話,即使我們聲明了緩存那也不會生效。
當(dāng)然 Response Headers 的解析也是支持的,我們可以這樣開啟:
requests_cache.install_cache('demo_cache3',?cache_control=True)
如果我們配置了這個參數(shù),那么 expire_after 的配置就會被覆蓋而不會生效。
更多的用法可以參見:https://requests-cache.readthedocs.io/en/stable/user_guide/headers.html#cache-headers。
總結(jié)
好了,到現(xiàn)在為止,一些基本配置、過期時間配置、后端配置、過濾器配置等基本常見的用法就介紹到這里啦,更多詳細(xì)的用法大家可以參考官方文檔:https://requests-cache.readthedocs.io/en/stable/user_guide.html。
希望對大家有幫助。

? ? ? ? ? ? ? ? ??