Scrapy HTTP Cache: 완전 초보자용 가이드 (웹사이트를 과도하게 요청하지 않기)

발행: (2025년 12월 27일 오전 03:51 GMT+9)
17 min read
원문: Dev.to

Source: Dev.to

죄송하지만, 현재 저는 외부 웹사이트의 내용을 직접 가져올 수 없습니다. 번역이 필요한 텍스트를 여기 채팅에 그대로 붙여 주시면, 요청하신 대로 소스 링크는 그대로 두고 한국어로 번역해 드리겠습니다.

내가 처음 스파이더를 만들기 시작했을 때

나는 스파이더를 반복해서 실행하면서 테스트하곤 했습니다.
셀렉터를 수정할 때마다 스파이더를 다시 실행하고, 웹사이트에 다시 접속해 같은 페이지를 다시 다운로드했습니다.

하루에 50번째 실행을 마친 뒤, 나는 뭔가를 깨달았습니다: 나는 끔찍한 인터넷 시민이었어요. CSS 셀렉터를 제대로 작성하지 못해서 수백 개의 요청을 보내며 가엾은 웹사이트를 마구 두드리고 있었습니다.

그때 나는 Scrapy의 HTTP 캐시를 발견했어요 – 게임 체인저였습니다. 이제 스파이더를 테스트할 때 페이지를 한 번만 가져오고 캐시된 응답을 재사용합니다. 테스트가 빨라지고, 죄책감도 없으며, 차단당할 일도 없습니다.

이제 캐시를 올바르게 사용하는 방법을 보여드리겠습니다.

HTTP 캐시란?

HTTP 캐시를 웹페이지의 복사기처럼 생각하세요.

캐시 없이

  • 스파이더 실행 → 페이지 다운로드
  • 선택자 수정 → 다시 실행 → 같은 페이지를 다시 다운로드
  • 다른 항목 수정 → 다시 실행 → 같은 페이지를 다시 다운로드

같은 페이지를 여러 번 다운로드하고 있습니다. 비효율적이고 느리며 웹사이트에 부담을 줍니다.

캐시 사용 시

  • 스파이더 실행 → 페이지 다운로드 → 복사본 저장
  • 선택자 수정 → 다시 실행 → 저장된 복사본 사용 (다운로드 없음!)
  • 다른 항목 수정 → 다시 실행 → 여전히 저장된 복사본 사용

한 번만 다운로드하고 무한히 테스트합니다. 웹사이트는 한 번의 요청만 받습니다.

캐시 활성화 (한 줄)

settings.py에 다음을 추가하세요:

HTTPCACHE_ENABLED = True

그게 전부입니다. 이제 Scrapy가 모든 것을 캐시합니다.

스파이더를 실행하세요:

scrapy crawl myspider

첫 번째 실행: 페이지를 정상적으로 다운로드합니다. 프로젝트 폴더를 확인하면 캐시된 페이지가 저장된 새로운 .scrapy/httpcache/myspider/ 디렉터리를 볼 수 있습니다.

다시 실행:

scrapy crawl myspider

이번에는 번개처럼 빠릅니다. 실제 HTTP 요청이 없으며 모든 것이 캐시에서 제공됩니다.

캐시 작동 방식 (간단 설명)

  1. 첫 번째 요청 – 스파이더가 URL을 요청합니다.
  2. 캐시 확인 – “이미 이 페이지가 있나요?”
  3. 캐시 미스 – 없어요.
  4. 다운로드 – 웹사이트에서 가져옵니다.
  5. 저장 – 응답을 캐시에 저장합니다.
  6. 반환 – 스파이더에게 응답을 전달합니다.

다음에 같은 URL을 요청하면:

  1. 요청 – 스파이더가 같은 URL을 요청합니다.
  2. 캐시 확인 – “이 페이지가 있나요?”
  3. 캐시 히트 – 네!
  4. 반환 – 캐시된 응답을 전달합니다 (다운로드 없음!).

간단함. 빠름. 효율적.

기본 캐시 설정

캐시 보관 기간

기본적으로 캐시는 만료되지 않습니다. 만료 시간을 설정할 수 있습니다:

# settings.py
HTTPCACHE_ENABLED = True
HTTPCACHE_EXPIRATION_SECS = 86400  # 24 hours

24시간이 지나면 캐시된 페이지가 다시 다운로드됩니다.

만료를 사용할 때

  • 뉴스 사이트 스크래핑 (콘텐츠가 매일 변경)
  • 제품 가격 (자주 변동)
  • 모든 동적 콘텐츠

만료를 사용하지 않을 때

  • 개발 (페이지를 캐시된 상태로 유지하고 싶을 때)
  • 정적 콘텐츠 스크래핑
  • 변하지 않는 과거 데이터

캐시 저장 위치

기본 위치: .scrapy/httpcache/. 다음과 같이 변경할 수 있습니다:

# settings.py
HTTPCACHE_ENABLED = True
HTTPCACHE_DIR = 'my_custom_cache'

이제 캐시는 my_custom_cache/에 저장됩니다.

특정 상태 코드 무시

오류 페이지를 캐시하지 않음:

# settings.py
HTTPCACHE_ENABLED = True
HTTPCACHE_IGNORE_HTTP_CODES = [404, 500, 502, 503]

404와 500 오류는 캐시되지 않습니다 – 깨진 페이지를 캐시하고 싶지 않을 때입니다.

캐시 정책 (두 가지 종류)

Scrapy는 두 가지 캐시 정책을 제공합니다: DummyPolicyRFC2616Policy.

DummyPolicy (간단한 버전)

# settings.py
HTTPCACHE_ENABLED = True
HTTPCACHE_POLICY = 'scrapy.extensions.httpcache.DummyPolicy'
  • 모든 것을 캐시합니다.
  • 캐시가 최신인지 절대 확인하지 않습니다.
  • 재검증을 전혀 하지 않습니다.

사용 상황: 테스트, 오프라인 개발, 혹은 스크래핑을 정확히 “재생”하고 싶을 때.

RFC2616Policy (똑똑한 버전)

# settings.py
HTTPCACHE_ENABLED = True
HTTPCACHE_POLICY = 'scrapy.extensions.httpcache.RFC2616Policy'
  • HTTP 캐시 헤더(Cache‑Control, max‑age 등)를 준수합니다.
  • 필요할 때 재검증을 수행합니다.

사용 상황: 프로덕션 스크래퍼를 운영할 때, 웹사이트의 캐시 규칙을 따를 때, 최신 데이터를 필요로 할 때, 혹은 좋은 인터넷 시민이 되고 싶을 때.

실제 예시: 개발 vs. 프로덕션

개발 설정 (모두 캐시)

# settings.py

# Development: cache everything forever
HTTPCACHE_ENABLED = True
HTTPCACHE_POLICY = 'scrapy.extensions.httpcache.DummyPolicy'
HTTPCACHE_DIR = '.dev_cache'
HTTPCACHE_EXPIRATION_SECS = 0  # Never expire

테스트에 완벽합니다 – 한 번 다운로드하고 영원히 테스트합니다.

프로덕션 설정 (스마트 캐싱)

# settings.py

# Production: respect HTTP caching rules
HTTPCACHE_ENABLED = True
HTTPCACHE_POLICY = 'scrapy.extensions.httpcache.RFC2616Policy'
HTTPCACHE_DIR = '.prod_cache'
HTTPCACHE_EXPIRATION_SECS = 3600  # 1 hour
HTTPCACHE_IGNORE_HTTP_CODES = [404, 500, 502, 503]

웹사이트 규칙을 준수하고 필요할 때 업데이트합니다.

Source:

실용적인 워크플로우

단계 1: 개발용 캐시 활성화

# settings.py
HTTPCACHE_ENABLED = True

여기서 워크플로우에 맞게 다른 옵션(정책, 디렉터리, 만료 등)을 조정할 수 있습니다. 즐거운 크롤링 되세요!

단계 2: 첫 실행 (캐시 채우기)

scrapy crawl myspider

이 명령은 모든 페이지를 다운로드하고 캐시에 저장합니다.

단계 3: 캐시를 이용한 개발

이제 웹사이트에 직접 접근하지 않고 스파이더를 수백 번 실행할 수 있습니다:

# 다시 실행
scrapy crawl myspider

# 셀렉터 수정
# 다시 실행
scrapy crawl myspider

# 또 다른 부분 수정
# 다시 실행
scrapy crawl myspider

모든 실행이 즉시 이루어지며 캐시에서 제공됩니다.

단계 4: 필요할 때 캐시 삭제

웹사이트 구조가 변경되었거나 최신 데이터가 필요할 때:

rm -rf .scrapy/httpcache/

그런 다음 스파이더를 다시 실행하여 최신 페이지로 캐시를 재채우세요.

요청별 캐시 제어

특정 요청에 대해 캐시를 비활성화할 수 있습니다:

def parse(self, response):
    # This request won't be cached
    yield scrapy.Request(
        'https://example.com/dynamic',
        callback=self.parse_dynamic,
        meta={'dont_cache': True}
    )

일부 페이지는 최신 상태를 유지해야 하고 다른 페이지는 캐시해도 될 때 유용합니다.

고급: 스토리지 백엔드

Scrapy는 HTTP 캐시를 위한 두 가지 스토리지 백엔드를 제공합니다: Filesystem(기본) 및 DBM.

Filesystem (Default)

# settings.py
HTTPCACHE_ENABLED = True
HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

장점

  • 쉽게 확인 가능 (파일을 열기만 하면 됨)
  • 어디서든 작동
  • 간단함

단점

  • 작은 파일이 많이 생성
  • 수천 페이지에서는 느림
  • 디스크 공간을 더 많이 차지

DBM (Database)

# settings.py
HTTPCACHE_ENABLED = True
HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.DbmCacheStorage'

장점

  • 많은 페이지에서 더 빠름
  • 파일 수가 적음
  • 더 효율적

단점

  • 확인하기 어려움
  • 데이터베이스별 이슈
  • 더 복잡함

팁: 대부분의 프로젝트에서는 파일시스템 백엔드를 사용하세요; 더 간단합니다.

캐시 디버깅

캐시된 내용 확인

ls -R .scrapy/httpcache/

각 요청마다 폴더가 표시됩니다. 각 폴더 안에는 다음과 같은 파일이 있습니다:

  • request_body – 수행된 요청
  • request_headers – 전송된 헤더
  • response_body – 받은 HTML
  • response_headers – 응답 헤더
  • meta – 메타데이터

요청이 캐시되었는지 확인

Scrapy는 캐시 히트를 로그에 기록합니다:

[scrapy.core.engine] DEBUG: Crawled (200)  (referer: None) ['cached']

['cached'] 접미사는 캐시 히트를 의미합니다.

캐시 히트가 없을 경우 로그는 다음과 같습니다:

[scrapy.core.engine] DEBUG: Crawled (200)  (referer: None)

일반적인 함정

함정 #1: 캐시가 스파이더 실행 사이에 유지됨

캐시는 실행 간에 유지됩니다. 최신 데이터를 강제로 얻으려면 수동으로 삭제하세요:

rm -rf .scrapy/httpcache/

또는 만료 시간을 설정하세요(위 참고).

함정 #2: 서로 다른 스파이더가 동일한 캐시 디렉터리를 공유함

하나의 프로젝트에 여러 스파이더가 있으면 .scrapy/httpcache/를 공유하지만, 각 스파이더마다 자체 하위 폴더가 생성됩니다:

.scrapy/httpcache/
    spider1/
    spider2/
    spider3/

함정 #3: POST 요청은 기본적으로 캐시되지 않음

GET 요청만 캐시됩니다. POST 요청(예: 폼 제출)은 캐시를 우회합니다:

# This won't be cached
yield scrapy.FormRequest(
    'https://example.com/search',
    formdata={'query': 'test'}
)

이는 POST 요청이 보통 멱등성이 없기 때문에 의도된 동작입니다.

함정 #4: 리다이렉트도 캐시됨

URL이 리다이렉트되면 그 리다이렉트도 캐시됩니다. 이후 실행에서는 다시 리다이렉트를 따라가지 않고 캐시된 최종 페이지를 반환합니다:

https://example.com → https://www.example.com

실제 시나리오

시나리오 1: 선택자 테스트

# settings.py
HTTPCACHE_ENABLED = True
HTTPCACHE_EXPIRATION_SECS = 0   # 절대 만료되지 않음

한 번 실행해 캐시를 채운 뒤, 사이트에 다시 접속하지 않고 선택자를 하루 종일 조정할 수 있습니다.

시나리오 2: 과거 데이터 스크래핑

# settings.py
HTTPCACHE_ENABLED = True
HTTPCACHE_POLICY = 'scrapy.extensions.httpcache.DummyPolicy'
HTTPCACHE_EXPIRATION_SECS = 0   # 영구 보관

절대 변하지 않는 데이터(예: 오래된 기사)에 적합합니다.

시나리오 3: 프로덕션 스크래퍼

# settings.py
HTTPCACHE_ENABLED = True
HTTPCACHE_POLICY = 'scrapy.extensions.httpcache.RFC2616Policy'
HTTPCACHE_EXPIRATION_SECS = 1800   # 30분

HTTP 캐싱 규칙을 따르며 30 분 후에 새로 고침합니다—균형 잡힌 접근 방식입니다.

시나리오 4: 오프라인 개발

# settings.py
HTTPCACHE_ENABLED = True
HTTPCACHE_IGNORE_MISSING = False   # 페이지가 캐시되지 않으면 실패

스파이더가 캐시된 페이지만 사용합니다. 페이지가 없으면 다운로드를 시도하지 않고 실패하므로 비행기 안에서 작업하기에 이상적입니다.

아무도 알려주지 않는 팁

  • 버전 관리로 캐시(또는 일부)를 협업 시에 사용하여 모두가 동일한 데이터를 사용하도록 합니다.
  • 정책 결합: 정적 페이지에는 DummyPolicy를, 동적 페이지에는 RFC2616Policy를 사용하고, 스파이더별로 HTTPCACHE_POLICY를 재정의합니다.
  • 캐시 크기 모니터링: 주기적으로 du -sh .scrapy/httpcache/ 명령을 실행하고 오래된 항목을 정리하여 디스크 용량이 늘어나는 것을 방지합니다.
  • HTTPCACHE_IGNORE_HTTP_CODES 사용: 오류 페이지(예: 404, 500)의 캐시를 방지합니다.

팁 #1: CI/CD에 캐시 사용

연속 통합에서는 실제 웹사이트에 접근하고 싶지 않습니다. Scrapy HTTP 캐시를 사용하세요:

# settings.py for CI/CD
HTTPCACHE_ENABLED = True
HTTPCACHE_IGNORE_MISSING = False  # Tests fail if page not cached

리포지토리에서 캐시를 미리 채워두세요. 테스트는 캐시된 페이지를 대상으로 실행되므로 빠르고 신뢰할 수 있습니다.

Tip #2: 개발자 간 캐시 공유

캐시 폴더를 버전 관리에 커밋합니다:

git add .scrapy/httpcache/
git commit -m "Add test cache"

이제 팀의 모든 사람이 테스트를 위해 동일한 캐시된 페이지를 사용하므로 일관된 결과를 얻을 수 있습니다.

팁 #3: 환경별 다른 캐시

# settings.py
import os

HTTPCACHE_ENABLED = True

if os.getenv('ENV') == 'production':
    HTTPCACHE_DIR = '.prod_cache'
    HTTPCACHE_POLICY = 'scrapy.extensions.httpcache.RFC2616Policy'
else:
    HTTPCACHE_DIR = '.dev_cache'
    HTTPCACHE_POLICY = 'scrapy.extensions.httpcache.DummyPolicy'

개발 환경과 프로덕션 환경을 위한 별도 캐시 – 양쪽의 장점을 모두 누릴 수 있습니다.

팁 #4: 캐시 압축으로 공간 절약

# settings.py
HTTPCACHE_ENABLED = True
HTTPCACHE_GZIP = True  # Compress cached responses

특히 큰 페이지의 경우 디스크 공간을 크게 절약합니다.

전체 예제 스파이더

스마트 캐싱이 적용된 프로덕션 레디 스파이더:

# spider.py
import scrapy

class SmartCacheSpider(scrapy.Spider):
    name = 'smartcache'
    start_urls = ['https://example.com/products']

    custom_settings = {
        'HTTPCACHE_ENABLED': True,
        'HTTPCACHE_POLICY': 'scrapy.extensions.httpcache.RFC2616Policy',
        'HTTPCACHE_EXPIRATION_SECS': 3600,
        'HTTPCACHE_IGNORE_HTTP_CODES': [404, 500, 502, 503],
        'HTTPCACHE_GZIP': True,
        'HTTPCACHE_DIR': '.product'
    }

    def parse(self, response):
        # parsing logic here
        pass
Back to Blog

관련 글

더 보기 »