一個(gè)神器,大幅提升爬蟲(chóng)爬取效率!

在做爬蟲(chóng)的時(shí)候,我們往往可能這些情況:
- 網(wǎng)站比較復(fù)雜,會(huì)碰到很多重復(fù)請(qǐng)求。
- 有時(shí)候爬蟲(chóng)意外中斷了,但我們沒(méi)有保存爬取狀態(tài),再次運(yùn)行就需要重新爬取。
還有諸如此類(lèi)的問(wèn)題。
那怎么解決這些重復(fù)爬取的問(wèn)題呢?大家很可能都想到了“緩存”,也就是說(shuō),爬取過(guò)一遍就直接跳過(guò)爬取。
那一般怎么做呢?
比如我寫(xiě)一個(gè)邏輯,把已經(jīng)爬取過(guò)的 URL 保存到文件或者數(shù)據(jù)庫(kù)里面,每次爬取之前檢查一下是不是在列表或數(shù)據(jù)庫(kù)里面就好了。
是的,這個(gè)思路沒(méi)問(wèn)題,但有沒(méi)有想過(guò)這些問(wèn)題:
- 寫(xiě)入到文件或者數(shù)據(jù)庫(kù)可能是永久性的,如果我想控制緩存的有效時(shí)間,那就還得有個(gè)過(guò)期時(shí)間控制。
- 這個(gè)緩存根據(jù)什么來(lái)判斷?如果僅僅是 URL 本身夠嗎?還有 Request Method、Request Headers 呢,如果它們不一樣了,那還要不要用緩存?
- 如果我們有好多項(xiàng)目,難道都沒(méi)有一個(gè)通用的解決方案嗎?
的確是些問(wèn)題,實(shí)現(xiàn)起來(lái)確實(shí)要考慮很多問(wèn)題。
不過(guò)不用擔(dān)心,今天給大家介紹一個(gè)神器,可以幫助我們通通解決如上的問(wèn)題。
介紹
它就是 requests-cache,是 requests 庫(kù)的一個(gè)擴(kuò)展包,利用它我們可以非常方便地實(shí)現(xiàn)請(qǐng)求的緩存,直接得到對(duì)應(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
下面我們來(lái)介紹下它的使用。
安裝
安裝非常簡(jiǎn)單,使用 pip3 即可:
pip3?install?requests-cache
安裝完畢之后我們來(lái)了解下它的基本用法。
基本用法
下面我們首先來(lái)看一個(gè)基礎(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)
這里我們請(qǐng)求了一個(gè)網(wǎng)站,是 http://httpbin.org/delay/1,這個(gè)網(wǎng)站模擬了一秒延遲,也就是請(qǐng)求之后它會(huì)在 1 秒之后才會(huì)返回響應(yīng)。
這里請(qǐ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é)果會(huì)怎樣?
代碼改寫(xiě)如下:
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)
這里我們聲明了一個(gè) CachedSession,將原本的 Session 對(duì)象進(jìn)行了替換,還是請(qǐng)求了 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ā)生了什么?
這時(shí)候我們可以發(fā)現(xiàn),在本地生成了一個(gè) demo_cache.sqlite 的數(shù)據(jù)庫(kù)。
我們打開(kāi)之后可以發(fā)現(xiàn)里面有個(gè) responses 表,里面多了一個(gè) key-value 記錄,如圖所示:

我們可以可以看到,這個(gè) key-value 記錄中的 key 是一個(gè) hash 值,value 是一個(gè) Blob 對(duì)象,里面的內(nèi)容就是 Response 的結(jié)果。
可以猜到,每次請(qǐng)求都會(huì)有一個(gè)對(duì)應(yīng)的 key 生成,然后 requests-cache 把對(duì)應(yīng)的結(jié)果存儲(chǔ)到了 SQLite 數(shù)據(jù)庫(kù)中了,后續(xù)的請(qǐng)求和第一次請(qǐng)求的 URL 是一樣的,經(jīng)過(guò)一些計(jì)算它們的 key 也都是一樣的,所以后續(xù) 2-10 請(qǐng)求就立馬返回了。
是的,利用這個(gè)機(jī)制,我們就可以跳過(guò)很多重復(fù)請(qǐng)求了,大大節(jié)省爬取時(shí)間。
Patch 寫(xiě)法
但是,剛才我們?cè)趯?xiě)的時(shí)候把 requests 的 session 對(duì)象直接替換了。有沒(méi)有別的寫(xiě)法呢?比如我不影響當(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 庫(kù)的 install_cache 方法就好了,其他的 requests 的 Session 照常使用即可。
我們?cè)龠\(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
這次比上次更快了,為什么呢?因?yàn)檫@次所有的請(qǐng)求都命中了 Cache,所以很快返回了結(jié)果。
后端配置
剛才我們知道了,requests-cache 默認(rèn)使用了 SQLite 作為緩存對(duì)象,那這個(gè)能不能換啊?比如用文件,或者其他的數(shù)據(jù)庫(kù)呢?
自然是可以的。
比如我們可以把后端換成本地文件,那可以這么做:
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)
這里我們添加了一個(gè) backend 參數(shù),然后指定為 filesystem,這樣運(yùn)行之后本地就會(huì)生成一個(gè) demo_cache 的文件夾用作緩存,如果不想用緩存的話(huà)把這個(gè)文件夾刪了就好了。
當(dāng)然我們還可以更改緩存文件夾的位置,比如:
requests_cache.install_cache('demo_cache',?backend='filesystem',?use_temp=True)
這里添加一個(gè) use_temp 參數(shù),緩存文件夾便會(huì)使用系統(tǒng)的臨時(shí)目錄,而不會(huì)在代碼區(qū)創(chuàng)建緩存文件夾。
當(dāng)然也可以這樣:
requests_cache.install_cache('demo_cache',?backend='filesystem',?use_cache_dir=True)
這里添加一個(gè) use_cache_dir 參數(shù),緩存文件夾便會(huì)使用系統(tǒng)的專(zhuān)用緩存文件夾,而不會(huì)在代碼區(qū)創(chuàng)建緩存文件夾。
另外除了文件系統(tǒng),requests-cache 也支持其他的后端,比如 Redis、MongoDB、GridFS 甚至內(nèi)存,但也需要對(duì)應(yīng)的依賴(lài)庫(kù)支持,具體可以參見(jiàn)下表:
| 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 就可以改寫(xiě)如下:
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)然,我們有時(shí)候也想指定有些請(qǐng)求不緩存,比如只緩存 POST 請(qǐng)求,不緩存 GET 請(qǐng)求,那可以這樣來(lái)配置:
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)
這里我們添加了一個(gè) allowable_methods 指定了一個(gè)過(guò)濾器,只有 POST 請(qǐng)求會(huì)被緩存,GET 請(qǐng)求就不會(huì)。
看下運(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
這時(shí)候就看到 GET 請(qǐng)求由于沒(méi)有緩存,就花了 12 多秒才結(jié)束,而 POST 由于使用了緩存,一秒多就結(jié)束了。
另外我們還可以針對(duì) Response Status Code 進(jìn)行過(guò)濾,比如只有 200 會(huì)緩存,則可以這樣寫(xiě):
import?time
import?requests
import?requests_cache
requests_cache.install_cache('demo_cache2',?allowable_codes=(200,))
當(dāng)然我們還可以匹配 URL,比如針對(duì)哪種 Pattern 的 URL 緩存多久,則可以這樣寫(xiě):
urls_expire_after?=?{'*.site_1.com':?30,?'site_2.com/static':?-1}
requests_cache.install_cache(
????'demo_cache2',?urls_expire_after=urls_expire_after)
這樣的話(huà),site_1.com 的內(nèi)容就會(huì)緩存 30 秒,site_2.com/static 的內(nèi)容就永遠(yuǎn)不會(huì)過(guò)期。
當(dāng)然,我們也可以自定義 Filter,具體可以參見(jiàn):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)容來(lái)緩存。
比如說(shuō),我們知道 HTTP 里面有個(gè) Cache-Control 的 Request / Response Header,它可以指定瀏覽器要不要對(duì)本次請(qǐng)求進(jìn)行緩存,那 requests-cache 怎么來(lái)支持呢?
實(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()
這里我們?cè)?Request Headers 里面加上了 Cache-Control 為 no-store,這樣的話(huà),即使我們聲明了緩存那也不會(huì)生效。
當(dāng)然 Response Headers 的解析也是支持的,我們可以這樣開(kāi)啟:
requests_cache.install_cache('demo_cache3',?cache_control=True)
如果我們配置了這個(gè)參數(shù),那么 expire_after 的配置就會(huì)被覆蓋而不會(huì)生效。
更多的用法可以參見(jiàn):
https://requests-cache.readthedocs.io/en/stable/user_guide/headers.html#cache-headers。
總結(jié)
好了,到現(xiàn)在為止,一些基本配置、過(guò)期時(shí)間配置、后端配置、過(guò)濾器配置等基本常見(jiàn)的用法就介紹到這里啦
更多詳細(xì)的用法大家可以參考官方文檔:
https://requests-cache.readthedocs.io/en/stable/user_guide.html
希望對(duì)大家有幫助

End
