一個神器,大幅提升爬蟲爬取效率!

語言: CN / TW / HK

這是「進擊的Coder」的第 575  篇技術分享

作者:崔慶才

在做爬蟲的時候,我們往往可能這些情況:

  • 網站比較複雜,會碰到很多重複請求。

  • 有時候爬蟲意外中斷了,但我們沒有儲存爬取狀態,再次執行就需要重新爬取。

還有諸如此類的問題。

那怎麼解決這些重複爬取的問題呢?大家很可能都想到了“快取”,也就是說,爬取過一遍就直接跳過爬取。

那一般怎麼做呢?

比如我寫一個邏輯,把已經爬取過的 URL 儲存到檔案或者資料庫裡面,每次爬取之前檢查一下是不是在列表或資料庫裡面就好了。

是的,這個思路沒問題,但有沒有想過這些問題:

  • 寫入到檔案或者資料庫可能是永久性的,如果我想控制快取的有效時間,那就還得有個過期時間控制。

  • 這個快取根據什麼來判斷?如果僅僅是 URL 本身夠嗎?還有 Request Method、Request Headers 呢,如果它們不一樣了,那還要不要用快取?

  • 如果我們有好多專案,難道都沒有一個通用的解決方案嗎?

的確是些問題,實現起來確實要考慮很多問題。

不過不用擔心,今天給大家介紹一個神器,可以幫助我們通通解決如上的問題。

介紹

它就是 requests-cache,是 requests 庫的一個擴充套件包,利用它我們可以非常方便地實現請求的快取,直接得到對應的爬取結果。

  • 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

安裝完畢之後我們來了解下它的基本用法。

基本用法

下面我們首先來看一個基礎例項:

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)

這裡我們請求了一個網站,是 http://httpbin.org/delay/1 ,這個網站模擬了一秒延遲,也就是請求之後它會在 1 秒之後才會返回響應。

這裡請求了 10 次,那就至少得需要 10 秒才能完全執行完畢。

執行結果如下:

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 呢?結果會怎樣?

程式碼改寫如下:

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 物件進行了替換,還是請求了 10 次。

執行結果如下:

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

可以看到,一秒多就爬取完畢了!

發生了什麼?

這時候我們可以發現,在本地生成了一個 demo_cache.sqlite 的資料庫。

我們開啟之後可以發現裡面有個 responses 表,裡面多了一個 key-value 記錄,如圖所示:

我們可以可以看到,這個 key-value 記錄中的 key 是一個 hash 值,value 是一個 Blob 物件,裡面的內容就是 Response 的結果。

可以猜到,每次請求都會有一個對應的 key 生成,然後 requests-cache 把對應的結果儲存到了 SQLite 資料庫中了,後續的請求和第一次請求的 URL 是一樣的,經過一些計算它們的 key 也都是一樣的,所以後續 2-10 請求就立馬返回了。

是的,利用這個機制,我們就可以跳過很多重複請求了,大大節省爬取時間。

Patch 寫法

但是,剛才我們在寫的時候把 requests 的 session 物件直接替換了。有沒有別的寫法呢?比如我不影響當前程式碼,只在程式碼前面加幾行初始化程式碼就完成 requests-cache 的配置呢?

當然是可以的,程式碼如下:

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)

這次我們直接呼叫了 requests-cache 庫的 install_cache 方法就好了,其他的 requests 的 Session 照常使用即可。

我們再執行一遍:

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,所以很快返回了結果。

後端配置

剛才我們知道了,requests-cache 預設使用了 SQLite 作為快取物件,那這個能不能換啊?比如用檔案,或者其他的資料庫呢?

自然是可以的。

比如我們可以把後端換成本地檔案,那可以這麼做:

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 引數,然後指定為 filesystem,這樣執行之後本地就會生成一個 demo_cache 的資料夾用作快取,如果不想用快取的話把這個資料夾刪了就好了。

當然我們還可以更改快取資料夾的位置,比如:

requests_cache.install_cache('demo_cache', backend='filesystem', use_temp=True)

這裡新增一個 use_temp 引數,快取資料夾便會使用系統的臨時目錄,而不會在程式碼區建立快取資料夾。

當然也可以這樣:

requests_cache.install_cache('demo_cache', backend='filesystem', use_cache_dir=True)

這裡新增一個 use_cache_dir 引數,快取資料夾便會使用系統的專用快取資料夾,而不會在程式碼區建立快取資料夾。

另外除了檔案系統,requests-cache 也支援其他的後端,比如 Redis、MongoDB、GridFS 甚至記憶體,但也需要對應的依賴庫支援,具體可以參見下表:

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)

更多詳細配置可以參考官方文件: https://requests-cache.readthedocs.io/en/stable/user_guide/backends.html#backends

Filter

當然,我們有時候也想指定有些請求不快取,比如只快取 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 請求就不會。

看下執行結果:

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 多秒才結束,而 POST 由於使用了快取,一秒多就結束了。

另外我們還可以針對 Response Status Code 進行過濾,比如只有 200 會快取,則可以這樣寫:

import time
import requests
import requests_cache

requests_cache.install_cache('demo_cache2', allowable_codes=(200,))

當然我們還可以匹配 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 的內容就會快取 30 秒,site_2.com/static 的內容就永遠不會過期。

當然,我們也可以自定義 Filter,具體可以參見: https://requests-cache.readthedocs.io/en/stable/user_guide/filtering.html#custom-cache-filtering

Cache Headers

除了我們自定義快取,requests-cache 還支援解析 HTTP Request / Response Headers 並根據 Headers 的內容來快取。

比如說,我們知道 HTTP 裡面有個 Cache-Control 的 Request / Response Header,它可以指定瀏覽器要不要對本次請求進行快取,那 requests-cache 怎麼來支援呢?

例項如下:

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-Controlno-store ,這樣的話,即使我們聲明瞭快取那也不會生效。

當然 Response Headers 的解析也是支援的,我們可以這樣開啟:

requests_cache.install_cache('demo_cache3', cache_control=True)

如果我們配置了這個引數,那麼 expire_after 的配置就會被覆蓋而不會生效。

更多的用法可以參見: https://requests-cache.readthedocs.io/en/stable/user_guide/headers.html#cache-headers

總結

好了,到現在為止,一些基本配置、過期時間配置、後端配置、過濾器配置等基本常見的用法就介紹到這裡啦,更多詳細的用法大家可以參考官方文件: https://requests-cache.readthedocs.io/en/stable/user_guide.html

希望對大家有幫助。

End

崔慶才的新書 《Python3網路爬蟲開發實戰(第二版)》 已經正式上市了!書中詳細介紹了零基礎用 Python 開發爬蟲的各方面知識,同時相比第一版新增了 JavaScript 逆向、Android 逆向、非同步爬蟲、深度學習、Kubernetes 相關內容,‍同時本書已經獲得 Python 之父 Guido 的推薦,目前本書正在七折促銷中!

內容介紹: 《Python3網路爬蟲開發實戰(第二版)》內容介紹

掃碼購買

好文和朋友一起看~