Scrapyのキャッシュを定期的に削除する

ZOZOテクノロジーズ #1 Advent Calendar 2020 23日目です。 ScrapyのCacheにおいて定期的にCacheを削除するような機能を作成したので紹介します。

ScrapyのCacheについて

ScrapyのCacheはHttpCacheMiddlewareで実装されています。
Cacheを有効にするとScrapyからリクエストを送った後に返ってきたレスポンスをCacheし、再度同じリクエストを送った際にCacheのデータを利用することができます。
Cacheを利用することによってクローリング対象のサーバへのリクエストを減らすことができます。 またCacheを保存する場所においてはデフォルトでlocalに保存されます。保存場所は変更可能で、DBや他のミドルウェアに設定可能です。
今回はデフォルトのlocalに保存される場合の話になります。

HTTPCACHE_EXPIRATION_SECSについて

Scrapy側でCacheを削除できる機能が存在するのか調査を行いました。 Scrapyが提供しているCache関連で設定できる環境変数を調べるとHTTPCACHE_EXPIRATION_SECSが設定できます。
HTTPCACHE_EXPIRATION_SECSはCacheに有効期限を設定できる設定になります。自分は最初HTTPCACHE_EXPIRATION_SECSを設定すればCacheを削除されると思ってました。しかし実装を追ってみると、特に削除されないことがわかりました。

実際にどのように実装されているのか中身を見てみようと思います。 HttpCacheMiddlewareの実装場所はhttpcache.pyになります。 storageの選択に関してはinit部分のstorageの部分で設定されます。デフォルトではscrapy.extensions.httpcache.FilesystemCacheStorageが設定されています。

self.storage = load_object(settings['HTTPCACHE_STORAGE'])(settings)

よってextensionsに配置されているFilesystemCacheStorageクラスがhttpcache.pyで利用されるようになります。

Cacheを利用する際はretrieve_responseメソッドが呼び出されます。メソッド内の_read_meta部分で 有効期限の条件分岐が実装されています。

実装箇所を確認すると下記のように実装されています。

       if 0 < self.expiration_secs < time() - mtime:
            return  # expired

実装より、特にexpireしたCacheの削除は行われないことがわかります。 周辺の処理を確認しても特に削除する実装は存在しませんでした。

Scrapyのmiddlewareは自分で実装すことが可能なので新しくCacheを削除するmiddlewareを追加しました。

実装

下記のようにmiddlewareとして実装を行いました。

from scrapy.settings import Setting
from scrapy.utils.misc import load_object
import shutil
import glob
from pathlib import Path

class CacheCleanMiddleware(object):
    def __init__(self, settings: Settings, crawler):
        self.crawler = crawler
        self.storage = load_object(settings['HTTPCACHE_STORAGE'])(settings)
        self.expiration_secs = settings.getint('HTTPCACHE_EXPIRATION_SECS')

    @classmethod
    def from_crawler(cls, crawler):
        o = cls(crawler.settings, crawler)
        return o


    def process_response(self, request, response, spider):
        rpath = self.storage._get_request_path(spider, request)
        cache_root_directory = Path(os.path.join(self.storage.cachedir, spider.name))
        cache_directory = sorted(cache_root_directory.glob('*/*/'), key=lambda f: os.stat(f).st_mtime)
        for f in cache_directory:
            metapath = os.path.join(f, 'pickled_meta')
            if os.path.exists(metapath):
                mtime = os.stat(metapath).st_mtime
                if 0 < self.expiration_secs < time() - mtime:
                    shutil.rmtree(Path(f).parent)
        cache_size = sum((os.path.getsize(f) for f in cache_root_directory.glob('**/*') if os.path.isfile(f)))
        if cache_size / 1024**2 >= 300:
            for i in range(cache_directory) / 3:
                metapath = os.path.join(f, 'pickled_meta')
                shutil.rmtree(Path(f).parent)

        return response

特に凝った実装は行っていませんがCacheの保存場所にあるCacheデータのmtmeを確認し、HTTPCACHE_EXPIRATION_SECSで指定された有効期間を過ぎていたらCacheを削除する仕組みになっています。また上限を300MBとし、300MB以上Cacheデータが増えた場合は古いCacheをある程度削除するような仕組みになっています。

まとめ

Scrapyのキャッシュを削除できるような処理を追加してみました。PC内で動かす際には特に問題ないのですが、Docker等リソースが限られている場合にScrapyを動かすとストレージが上限に達する場合があるので定期的にCacheを削除する必要が出てくるかなと思います。