Scraping

Die Rohantwort behalten: Gescrapte Seiten im WARC-Format für die Wiedergabe archivieren

Parser werden besser und Extraktoren brechen, aber eine Seite, die nicht gespeichert wurde, kann nicht erneut geparst werden. Wie man Rohantworten im WARC-Format speichert, mit getestetem Code und realen Größenangaben.

Matt Brown

Matt Brown

1. Oktober 2026 · 9 Min. Lesezeit

Die meisten Scraping-Pipelines werfen die Seite weg, sobald sie daraus extrahiert haben. Der Parser liest das HTML, schreibt eine Zeile, und die Antwort ist weg. Das funktioniert, bis sich eines Tages herausstellt, dass der Extraktor eine Woche lang falsch lag, oder ein neues Feld für das letzte Quartal benötigt wird, oder jemand fragt, was auf der Seite tatsächlich stand. An diesem Punkt führt der einzige Weg zurück darüber, alles erneut abzurufen, und die Seiten haben sich in der Zwischenzeit verändert.

Die Rohantwort aufzubewahren löst alle drei Probleme, und es gibt dafür ein standardisiertes Format: WARC, das Web ARChive-Format, das von Webarchiven und von Common Crawl verwendet wird. Dieser Leitfaden behandelt, was gespeichert werden sollte, den Code dafür und was es bei einem realen Test gekostet hat.

Die wichtigsten Erkenntnisse

  • Speichern Sie jede Antwort so, wie sie empfangen wurde, bevor Sie sie parsen. Die erneute Extraktion aus gespeicherten Antworten ist günstig; die nachträgliche Erfassung von Verlaufsdaten ist unmöglich.
  • WARC ist der Standardcontainer: ein offenes Format mit Request-, Response- und Metadaten-Datensätzen, lesbar mit vielen vorhandenen Tools.
  • Fordern Sie komprimierte Antworten an und speichern Sie sie weiterhin komprimiert. Bei unserem Test mit 40 Seiten hielt dies 20,6 MB HTML auf 3,4 MB bei der Übertragung und 3,5 MB auf der Festplatte.
  • Das Wiedergeben aller 40 Seiten aus dem Archiv dauerte unter 0,2 Sekunden, gegenüber 6 bis 10 Sekunden für den Abruf.
  • Zeichnen Sie auf, wie jede Datei erfasst wurde, einschließlich des Austrittslandes, damit archivierte Seiten später fair verglichen werden können.

Warum die Rohantwort aufbewahren

Drei Situationen treten in nahezu jedem langfristigen Erfassungsprojekt auf.

Extraktoren brechen lautlos. Eine Website ändert ihr Markup, der Parser läuft weiter, und ein Feld wird still leer oder falsch. Schema-Drift-Monitoring erkennt dies, aber erst, nachdem einige fehlerhafte Zeilen geschrieben wurden. Mit Rohantworten auf der Festplatte reparieren Sie den Extraktor und lassen ihn über die betroffenen Tage erneut laufen. Ohne sie sind diese Tage verloren.

Fragen ändern sich. Sechs Monate später benötigt jemand ein Feld, das niemand extrahiert hat: eine Versandnotiz, eine Verkäuferbewertung, ein Badge. Wenn die Seiten archiviert sind, ist das ein Batch-Job. Wenn nicht, beginnt es heute und hat keine Vorgeschichte.

Belege benötigen das Original. Wenn erfasste Daten eine Entscheidung, eine Beschwerde oder einen Streitfall stützen, trägt die Seite, so wie sie ausgeliefert wurde, mehr Gewicht als eine daraus abgeleitete Zeile, weshalb gerichtsfeste Beweise mit erhaltenen Erfassungen beginnen.

Es verändert auch, wie Sie mit der Extraktion arbeiten können. Einen neuen Ansatz auszuprobieren, etwa den modellbasierten Vergleich mit Selektoren, wird zu einem Offline-Experiment mit gespeicherten Seiten statt zu einem neuen Crawl.

Was WARC ist

WARC ist ein Containerformat für Webaufnahmen, das vom International Internet Preservation Consortium gepflegt und als ISO 28500 standardisiert wird. Eine WARC-Datei ist eine Abfolge von Datensätzen, jeder mit einem kurzen Textkopf gefolgt vom Inhalt. Die Datensatztypen, die für Scraping relevant sind, sind:

DatensatztypWas er enthält
warcinfoWie die Datei erstellt wurde: Software, Betreiber, Erfassungshinweise
requestDie HTTP-Anfrage wie gesendet, einschließlich Headern wie Accept-Language
responseDie HTTP-Antwort wie empfangen: Statuszeile, Header und Body
metadataAlles andere zu einer Erfassung, verknüpft über die Datensatz-ID
revisitEin Verweis auf eine frühere identische Erfassung, verwendet, um Duplikate zu vermeiden

Jeder Datensatz trägt einen Digest seines Inhalts, sodass eine Datei auf Beschädigung geprüft werden kann. Die Spezifikation empfiehlt, jeden Datensatz separat mit gzip zu komprimieren, was Dateien klein hält und gleichzeitig erlaubt, dass ein Reader direkt zu einem Datensatz springt. Common Crawl zum Beispiel verteilt seine Crawl-Daten als WARC-Dateien.

Der praktische Vorteil gegenüber einem selbstgebauten Format ist das Tooling. Dateien, die nach dem Standard geschrieben wurden, können indexiert, validiert, wiedergegeben und durchsucht werden, mit vorhandenen Open-Source-Tools, von Leuten, die Ihren Code nie gesehen haben.

Der Code

Die Python-Bibliothek warcio, vom Webrecorder-Projekt, liest und schreibt WARC. Die folgende Funktion ruft eine Seite mit requests ab, schreibt die Anfrage und die Antwort in eine offene WARC-Datei und behält den Body genau so, wie der Server ihn gesendet hat:

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()

Und die Verwendung über einen Proxy, mit einem warcinfo-Datensatz, der angibt, woher die Seiten erfasst wurden:

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))

Drei Details in diesem Code ergaben sich aus dem Testen.

  • Den Body undekodiert lesen. requests dekomprimiert Antworten normalerweise automatisch für Sie. Das Lesen des rohen Streams behält die Bytes so, wie sie gesendet wurden, was sowohl originalgetreuer als auch deutlich kleiner ist. warcio dekomprimiert sie bei der Wiedergabe erneut.
  • Den Chunked-Header entfernen. Ein Viertel unserer Antworten kam mit Chunked-Transfer-Encoding an, das die HTTP-Bibliothek bereits aufgelöst hatte. Den Header mit dem aufgelösten Body zu speichern, hinterlässt einen Datensatz, der sich selbst falsch beschreibt. warcio kam damit zufällig zurecht; andere Reader möglicherweise nicht.
  • Einen echten User-Agent senden. Unser erster Durchlauf des Beispiels wurde mit HTTP 403 abgelehnt, weil die Website den Standard-User-Agent der HTTP-Bibliothek ablehnt. Identifizieren Sie Ihren Collector ehrlich.

Noch eine Falle, gefunden im Quellcode der Bibliothek statt beim Testen: Wenn Sie Brotli-komprimierte Antworten (br) akzeptieren, installieren Sie das brotli-Paket. Ohne es kann warcio diese Bodies bei der Wiedergabe nicht dekodieren und gibt die weiterhin komprimierten Bytes zurück, ohne einen Fehler auszulösen.

Was es gekostet hat

Wir haben 40 englische Wikipedia-Artikel am 1. Oktober 2026 über einen residentiellen Exit in Deutschland archiviert, zweimal: einmal mit Annahme von gzip-komprimierten Antworten, und einmal mit Anforderung unkomprimierter Antworten.

Komprimierte AntwortenUnkomprimierte Antworten
Seiten4040
Übertragen3,43 MB20,6 MB
HTML nach Dekodierung20,6 MB20,6 MB
WARC-Datei auf Festplatte3,54 MB3,51 MB
Zeit zum Abrufen6 bis 10 s9 bis 12 s
Zeit zur Wiedergabe aller 400,19 s0,18 s

Jede wiedergegebene Seite war byteidentisch mit der abgerufenen, geprüft per SHA-256-Hash, und warcio check validierte die Digests aller 81 Datensätze in der Datei.

Zwei Dinge stechen hervor. Erstens: Speicherung ist günstig: Das Archiv war etwa 3% größer als die übertragenen komprimierten Bytes, wobei der Unterschied aus den Request-Datensätzen und Headern stammt. Zweitens: Die Speicherung war in beiden Fällen gleich, da die WARC-Datei ohnehin komprimiert wird; was sich änderte, war die Bandbreite. Die Anforderung unkomprimierter Antworten kostete sechsmal so viel Übertragung für ein identisches Archiv. Bei nach Bandbreite abgerechneter Erfassung ist das der Unterschied, der auf der Rechnung erscheint, wie die Senkung von Proxy-Bandbreitenkosten ausführlicher erklärt.

Praktische Regeln

  • Archivieren Sie vor dem Parsen. Schreiben Sie zuerst den WARC-Datensatz, dann extrahieren Sie. Wenn der Extraktor abstürzt, bleibt die Seite erhalten.
  • Dateien nach Größe oder Zeit rotieren. Die WARC-Spezifikation empfiehlt 1 GB als praktische Zielgröße pro Datei. Benennen Sie Dateien mit Datum und Sammlung, und hängen Sie nie an eine Datei an, die ein anderer Prozess gerade schreibt.
  • Den Beobachtungsstandpunkt aufzeichnen. Eine Seite, die aus Deutschland abgerufen wurde, und eine, die aus den Vereinigten Staaten abgerufen wurde, können sich in Sprache, Preis und Inhalt unterscheiden. Vermerken Sie das Austrittsland und die Erfassungseinstellungen im warcinfo-Datensatz, wie es in dem Argument für die Aufzeichnung, wo Daten beobachtet wurden dargelegt wird.
  • Indexieren Sie, was Sie behalten. Ein kleiner Index aus URL, Datum, Datei und Offset erlaubt es Ihnen, eine einzelne Erfassung aus Terabytes herauszuziehen, ohne alles zu lesen. Das Kommandozeilen-Tool index von warcio erzeugt einen solchen.
  • Legen Sie eine Aufbewahrungsrichtlinie fest. Rohseiten können personenbezogene Daten enthalten. Entscheiden Sie, wie lange Sie sie behalten, beschränken Sie, wer sie lesen darf, und löschen Sie nach Plan.
  • Duplikate bewusst überspringen. Wenn sich eine Seite seit der letzten Erfassung nicht geändert hat, kann ein revisit-Datensatz auf die frühere Kopie verweisen, statt sie erneut zu speichern, was gut mit Änderungserkennung zusammenpasst.

Fazit

Das Parsen ist der Teil einer Scraping-Pipeline, der am wahrscheinlichsten falsch ist und sich am wahrscheinlichsten ändert, daher sollte es nicht die einzige Aufzeichnung dessen sein, was erfasst wurde. Speichern Sie die Rohantwort in WARC, bevor Sie sie parsen, fordern Sie komprimierte Antworten an und behalten Sie sie komprimiert, und notieren Sie, woher jede Datei erfasst wurde.

Bei unserem Test kostete das etwa 3,5 MB Festplattenspeicher für 40 Seiten und verwandelte einen Crawl, der Sekunden dauerte, in eine Wiedergabe, die einen Bruchteil davon dauerte. Wenn beim nächsten Mal ein Extraktor ausfällt oder eine neue Frage zum letzten Monat auftaucht, lautet die Antwort Batch-Job statt verlorener Woche.

Quellen und Referenzen

  • International Internet Preservation Consortium, The WARC Format 1.1.
  • Webrecorder, warcio, Version 1.8.1, verwendet für den obigen Code und Test.
  • Common Crawl, Get started, zu seiner Verwendung des WARC-Formats.
  • Testarchiv von 40 Seiten, erfasst von Shifter am 1. Oktober 2026, mit dem obigen Code.

Bereit, loszulegen?

Testen Sie Shifters Residential-Proxys, 205M+ IPs, 195+ Länder, ab 0,75 $/GB.

Jetzt starten