스크래핑

원본 응답 보관: 스크랩한 페이지를 재생을 위해 WARC로 아카이빙하기

파서는 개선되고 추출기는 고장나지만, 보관하지 않은 페이지는 다시 파싱할 수 없습니다. 테스트된 코드와 실제 크기로 원본 응답을 WARC에 저장하는 방법.

Matt Brown

Matt Brown

2026년 10월 1일 · 8 분 소요

대부분의 스크래핑 파이프라인은 페이지에서 추출을 마치는 순간 그 페이지를 버린다. 파서가 HTML을 읽고 행 하나를 기록하면, 응답은 사라진다. 이는 추출기가 일주일 동안 잘못 작동했다는 사실이 드러나거나, 지난 분기에 필요했던 새 필드가 필요해지거나, 누군가 페이지에 실제로 무엇이 쓰여 있었는지 물어보기 전까지는 문제없이 돌아간다. 그 시점이 되면 유일한 방법은 모든 것을 다시 가져오는 것뿐인데, 그 사이에 페이지는 이미 바뀌어 있다.

원본 응답을 보관하면 이 세 가지 문제가 모두 해결되며, 이를 위한 표준 형식이 이미 존재한다. 바로 웹 아카이브와 Common Crawl에서 사용하는 Web ARChive 형식, WARC다. 이 가이드는 무엇을 저장해야 하는지, 저장을 위한 코드, 그리고 실제 테스트에서 든 비용을 다룬다.

핵심 요약

  • 파싱하기 전에 모든 응답을 수신한 그대로 저장하라. 저장된 응답에서 다시 추출하는 것은 비용이 적게 들지만, 지나간 이력을 다시 수집하는 것은 불가능하다.
  • WARC는 표준 컨테이너로, request, response, metadata 레코드를 포함하는 개방형 형식이며 기존의 많은 도구로 읽을 수 있다.
  • 압축된 응답을 요청하고 압축된 상태 그대로 저장하라. 40개 페이지를 대상으로 한 테스트에서, 이 방식은 20.6 MB의 HTML을 전송 시 3.4 MB, 디스크 상에서는 3.5 MB로 유지했다.
  • 아카이브에서 40개 페이지를 모두 재생하는 데는 0.2초 미만이 걸렸으며, 이는 가져오는 데 걸린 6초에서 10초와 대조된다.
  • 각 파일이 어떻게 수집되었는지, 출구 국가를 포함하여 기록하라. 그래야 나중에 아카이브된 페이지를 공정하게 비교할 수 있다.

원본 응답을 보관해야 하는 이유

거의 모든 장기 수집 프로젝트에서 다음 세 가지 상황이 발생한다.

추출기는 조용히 고장 난다. 사이트가 마크업을 변경해도 파서는 계속 실행되고, 어떤 필드는 소리 없이 비거나 잘못된 값을 담게 된다. 스키마 변동 모니터링이 이를 잡아내지만, 이미 일부 잘못된 행이 기록된 이후에야 가능하다. 원본 응답이 디스크에 있다면 추출기를 고치고 영향받은 기간에 대해 다시 실행하면 된다. 없다면 그 기간은 영영 사라진다.

질문은 바뀐다. 6개월이 지나면 아무도 추출하지 않았던 필드가 필요해진다. 배송 메모, 판매자 평점, 배지 같은 것들이다. 페이지가 아카이브되어 있다면 이는 배치 작업에 불과하다. 그렇지 않다면 지금부터 시작해야 하고 이력은 전혀 없다.

증거는 원본을 필요로 한다. 수집된 데이터가 결정, 민원, 분쟁을 뒷받침할 때, 서빙된 그대로의 페이지는 거기서 파생된 행보다 더 무게를 가진다. 법적 증거로 쓸 수 있는 자료가 보존된 캡처에서 시작하는 이유다.

이는 추출 작업 방식 자체도 바꾼다. 셀렉터와 비교한 모델 기반 추출 같은 새로운 접근법을 시도하는 일도, 새 크롤링이 아니라 저장된 페이지에 대한 오프라인 실험이 된다.

WARC란 무엇인가

WARC는 웹 캡처를 위한 컨테이너 형식으로, International Internet Preservation Consortium이 관리하며 ISO 28500으로 표준화되어 있다. WARC 파일은 레코드의 연속이며, 각 레코드는 짧은 텍스트 헤더 블록 뒤에 콘텐츠가 이어지는 구조다. 스크래핑에서 중요한 레코드 유형은 다음과 같다.

레코드 유형담는 내용
warcinfo파일이 어떻게 만들어졌는지: 소프트웨어, 운영자, 수집 관련 메모
request전송된 그대로의 HTTP 요청, Accept-Language 같은 헤더 포함
response수신된 그대로의 HTTP 응답: 상태 줄, 헤더, 본문
metadata캡처에 관한 기타 정보로, 레코드 ID로 해당 캡처와 연결됨
revisit이전의 동일한 캡처를 가리키는 포인터로, 중복 저장을 피하는 데 쓰임

각 레코드는 콘텐츠의 다이제스트를 포함하므로 파일의 손상 여부를 확인할 수 있다. 사양에서는 각 레코드를 gzip으로 개별 압축할 것을 권장하는데, 이렇게 하면 파일 크기를 작게 유지하면서도 리더가 특정 레코드로 바로 건너뛸 수 있다. 예를 들어 Common Crawl은 크롤링 데이터를 WARC 파일 형태로 배포한다.

자체 제작 형식 대비 실질적인 이점은 도구 지원이다. 표준에 맞게 작성된 파일은 당신의 코드를 한 번도 본 적 없는 사람들도 기존의 오픈소스 도구로 색인하고, 검증하고, 재생하고, 검색할 수 있다.

코드

Webrecorder 프로젝트의 Python 라이브러리 warcio는 WARC를 읽고 쓴다. 아래 함수는 requests로 페이지를 가져와서, 요청과 응답을 열려 있는 WARC 파일에 기록하며, 본문은 서버가 보낸 그대로 유지한다.

from io import BytesIO
from urllib.parse import urlsplit

import requests
from warcio.archiveiterator import ArchiveIterator
from warcio.statusandheaders import StatusAndHeaders
from warcio.warcwriter import WARCWriter


def fetch_and_archive(session, url, writer, **kwargs):
    """Fetch url and write the request and the response, byte for byte as received, to a WARC file."""
    response = session.get(url, stream=True, **kwargs)
    # Read the body undecoded, so gzip or br responses are stored exactly as the server sent them.
    raw = response.raw.read(decode_content=False)

    sent = response.request
    path = urlsplit(sent.url)
    request_line = f"{sent.method} {path.path or '/'}{'?' + path.query if path.query else ''} HTTP/1.1"
    request_headers = [("Host", path.netloc)] + list(sent.headers.items())
    request = writer.create_warc_record(
        sent.url, "request", payload=BytesIO(b""),
        http_headers=StatusAndHeaders(request_line, request_headers, is_http_request=True))

    # urllib3 has already removed any chunked framing, so drop the header that describes it.
    headers = [(k, v) for k, v in response.raw.headers.items() if k.lower() != "transfer-encoding"]
    status = f"{response.status_code} {response.reason}"
    record = writer.create_warc_record(
        response.url, "response", payload=BytesIO(raw),
        http_headers=StatusAndHeaders(status, headers, protocol="HTTP/1.1"))

    request.rec_headers.add_header("WARC-Concurrent-To", record.rec_headers.get_header("WARC-Record-ID"))
    writer.write_record(request)
    writer.write_record(record)
    return response.status_code, raw


def replay(path):
    """Yield (url, status, decoded body) for every response in a WARC file, with no network access."""
    with open(path, "rb") as stream:
        for record in ArchiveIterator(stream):
            if record.rec_type == "response":
                status = int(record.http_headers.get_statuscode())
                yield record.rec_headers.get_header("WARC-Target-URI"), status, record.content_stream().read()

그리고 이를 프록시를 통해 사용하는 예시다. 페이지가 어디에서 수집되었는지를 나타내는 warcinfo 레코드를 포함한다.

import os

import requests
from warcio.warcwriter import WARCWriter

from archive import fetch_and_archive, replay

proxy = f"http://{os.environ['SHIFTER_PROXY_USER']}-country-de:{os.environ['SHIFTER_PROXY_PASS']}@p.shifter.io:443"
session = requests.Session()
session.proxies = {"http": proxy, "https": proxy}
session.headers["Accept-Encoding"] = "gzip"
# Identify your collector; some sites refuse the library's default User-Agent.
session.headers["User-Agent"] = "ExampleArchiver/1.0 (+https://example.com/bot)"

urls = ["https://en.wikipedia.org/wiki/Web_archiving", "https://en.wikipedia.org/wiki/Web_crawler"]

with open("crawl-2026-10-01.warc.gz", "wb") as output:
    writer = WARCWriter(output, gzip=True)
    # One warcinfo record per file says how and from where the pages were collected.
    writer.write_record(writer.create_warcinfo_record(
        "crawl-2026-10-01.warc.gz", {"software": "warcio", "description": "exit country: de"}))
    for url in urls:
        fetch_and_archive(session, url, writer, timeout=30)

# Months later, with no network access: re-run a new extractor over the same bytes.
for url, status, body in replay("crawl-2026-10-01.warc.gz"):
    print(status, url, len(body))

이 코드에는 테스트 과정에서 드러난 세 가지 세부 사항이 있다.

  • 본문을 디코딩하지 않은 채로 읽어라. requests는 보통 응답을 자동으로 압축 해제한다. 원시 스트림을 읽으면 전송된 그대로의 바이트가 유지되며, 이는 더 충실할 뿐 아니라 크기도 훨씬 작다. warcio는 재생 시 다시 압축을 해제한다.
  • 청크 헤더를 제거하라. 우리의 응답 중 4분의 1은 청크 전송 인코딩으로 도착했는데, HTTP 라이브러리가 이미 이를 풀어놓은 상태였다. 풀린 본문에 그 헤더를 그대로 저장하면 스스로를 잘못 설명하는 레코드가 남는다. warcio는 그럭저럭 대응했지만, 다른 리더는 그렇지 않을 수 있다.
  • 실제 User-Agent를 보내라. 예제를 처음 실행했을 때 HTTP 403으로 거부당했는데, 해당 사이트가 HTTP 라이브러리의 기본 User-Agent를 거부했기 때문이다. 수집기를 정직하게 식별하라.

테스트가 아니라 라이브러리 소스 코드에서 발견한 또 하나의 함정이 있다. Brotli로 압축된 응답(br)을 받아들인다면 brotli 패키지를 설치해야 한다. 설치하지 않으면 warcio는 재생 시 해당 본문을 디코딩하지 못하며, 오류를 발생시키지 않고 여전히 압축된 바이트를 그대로 반환한다.

비용은 얼마였나

우리는 독일에 있는 레지덴셜 출구를 통해 2026년 10월 1일에 영어 Wikipedia 기사 40개를 두 차례 아카이브했다. 한 번은 gzip으로 압축된 응답을 받아들이는 방식으로, 한 번은 압축되지 않은 응답을 요청하는 방식으로 진행했다.

압축된 응답압축되지 않은 응답
페이지 수4040
전송량3.43 MB20.6 MB
디코딩된 HTML20.6 MB20.6 MB
디스크 상 WARC 파일3.54 MB3.51 MB
가져오는 데 걸린 시간6~10초9~12초
40개 전체 재생 시간0.19초0.18초

재생된 모든 페이지는 SHA-256 해시로 확인한 결과 가져온 것과 바이트 단위로 동일했으며, warcio check는 파일 내 81개 레코드 전체의 다이제스트를 검증했다.

두 가지가 눈에 띈다. 첫째, 저장 비용은 저렴하다. 아카이브는 전송된 압축 바이트보다 약 3% 더 컸는데, 그 차이는 request 레코드와 헤더 때문이다. 둘째, 저장 용량은 어느 쪽이든 동일했는데, WARC 파일은 어차피 압축되기 때문이다. 바뀐 것은 대역폭이었다. 압축되지 않은 응답을 요청하면 동일한 아카이브에 대해 전송량이 여섯 배 더 들었다. 대역폭 기준으로 과금되는 수집 작업에서는 이것이 청구서에 그대로 나타나는 차이이며, 프록시 대역폭 비용 절감에서 더 자세히 다룬다.

실무 규칙

  • 파싱하기 전에 아카이브하라. WARC 레코드를 먼저 기록하고 나서 추출하라. 추출기가 중단되더라도 페이지는 보존된다.
  • 크기나 시간 기준으로 파일을 롤오버하라. WARC 사양은 파일당 실질적인 목표 크기로 1 GB를 권장한다. 파일 이름에 날짜와 수집 정보를 포함시키고, 다른 프로세스가 쓰고 있는 파일에는 절대 이어 붙이지 말라.
  • 관측 지점을 기록하라. 독일에서 가져온 페이지와 미국에서 가져온 페이지는 언어, 가격, 내용이 다를 수 있다. 출구 국가와 수집 설정을 warcinfo 레코드에 담아라. 데이터가 관측된 위치를 기록해야 한다는 주장에서 설명한 바와 같다.
  • 보관하는 것을 색인화하라. URL, 날짜, 파일, 오프셋에 대한 작은 색인이 있으면 테라바이트 분량 전체를 읽지 않고도 하나의 캡처를 꺼낼 수 있다. warcio의 커맨드라인 index 도구가 이를 생성한다.
  • 보존 정책을 정하라. 원본 페이지에는 개인정보가 포함될 수 있다. 얼마나 오래 보관할지 정하고, 읽을 수 있는 사람을 제한하며, 일정에 따라 삭제하라.
  • 중복을 의도적으로 건너뛰어라. 마지막 캡처 이후 페이지가 변하지 않았다면, revisit 레코드가 다시 저장하는 대신 이전 사본을 가리키게 할 수 있으며, 이는 변경 감지와 잘 맞물린다.

결론

파싱은 스크래핑 파이프라인에서 잘못될 가능성이 가장 크고 가장 자주 바뀌는 부분이므로, 수집된 내용에 대한 유일한 기록이 되어서는 안 된다. 파싱하기 전에 원본 응답을 WARC로 저장하고, 압축된 응답을 요청하여 압축된 상태로 보관하며, 각 파일이 어디에서 수집되었는지 기록하라.

우리의 테스트에서는 40개 페이지에 디스크 약 3.5 MB가 들었고, 몇 초가 걸리던 크롤링을 그 일부에 불과한 시간의 재생으로 바꿔놓았다. 다음번에 추출기가 고장 나거나, 지난달에 관한 새 질문이 생겼을 때, 답은 잃어버린 한 주가 아니라 배치 작업 하나가 된다.

출처 및 참고자료

  • International Internet Preservation Consortium, The WARC Format 1.1.
  • Webrecorder, warcio, 버전 1.8.1, 위 코드와 테스트에 사용됨.
  • Common Crawl, 시작하기, WARC 형식 사용에 관하여.
  • 2026년 10월 1일 Shifter를 통해 위 코드로 수집한 40개 페이지의 테스트 아카이브.

시작할 준비가 되셨나요?

205M개 이상의 IP, 195개 이상의 국가를 지원하는 Shifter의 레지덴셜 프록시를 $0.10/GB부터 이용해보세요.

시작하기