Zum Inhalt springen
Zurück zum Blog

Best Practices für Web-Scraping: Ein Leitfaden für den gesamten Lebenszyklus mit Tipps und funktionierendem Code

Anda MiuțescuZuletzt aktualisiert am 30 min read
Best Practices für Web-Scraping: Ein Leitfaden für den gesamten Lebenszyklus mit Tipps und funktionierendem Code
Kurzfassung: Best Practices beim Web-Scraping sind die Gewohnheiten, die einen Scraper legal, rücksichtsvoll, genau und günstig im Betrieb halten. Schreiben Sie einen Datenvertrag, bevorzugen Sie APIs oder JSON gegenüber HTML, befolgen Sie robots.txt gemäß RFC 9309, drosseln Sie die Anfragen pro Host, beachten Sie Retry-After und validieren Sie jeden Datensatz vor dem Speichern. Dieses Playbook wendet sie auf jede Phase im Lebenszyklus eines Scrapers an, mit funktionierenden Python-Snippets.

Best Practices beim Web-Scraping sind die Vorgehensweisen, die dafür sorgen, dass ein Scraper legal, rücksichtsvoll, präzise und kostengünstig betrieben wird: Planen Sie die benötigten Daten, halten Sie sich an die „robots.txt“-Datei und die Nutzungsbedingungen der Website, begrenzen Sie die Anzahl der Anfragen pro Host, behandeln Sie 429-Fehler mit Backoff, parsen Sie defensiv, validieren Sie jeden Datensatz, überwachen Sie jeden Durchlauf, überprüfen Sie den Fortschritt und verfolgen Sie die Kosten pro gültigem Datensatz.

Der folgende Leitfaden wendet diese Best Practices für das Web-Scraping auf jede Phase des Lebenszyklus eines Scrapers an – von der ersten Planung bis zur monatlichen Rechnung –, wobei kurze Python-Codeausschnitte die Erläuterungen verdeutlichen.

Die Codeausschnitte erfordern Python 3.10 oder neuer sowie zwei Bibliotheken:

pip install requests beautifulsoup4

Speichern Sie jedes Snippet unter dem darüber angegebenen Dateinamen und führen Sie es aus diesem Ordner aus, zum Beispiel mit python polite_fetch.py.

Phase

Ziel

Wichtige Vorgehensweise

Abschnitt

Plan

Wissen, wie „fertig“ aussieht

Datenvertrag plus Reihenfolge der Quellen

Planung

Rechtlich einwandfrei und höflich

Bleiben Sie willkommen

RFC 9309 robots.txt, ehrlicher User-Agent

Rechtmäßiges und höfliches Scraping

Bewertung

Nicht überlasten

Host-spezifische Begrenzung, Retry-After, Backoff

Anfragerate

Anfragen

Weniger Fehler

Timeouts, Sitzungen, 304-Antworten

Anfrageverhalten

Blöcke

Ruhig wiederherstellen

Diagnose erstellen, dann eine Stufe höher eskalieren

Blockierungen

Rendering

Nur bei Bedarf bezahlen

JSON zuerst, Browser zuletzt

Headless-Browser

Parsen

Redesigns überstehen

Stabile Hooks, Fallbacks, Bytes rein

Parser

Datenqualität

Vertrauen Sie der Ausgabe

Schema, Ablehnungsgründe, Duplikatsbereinigung

Datenqualität

Überwachung

Erst bemerken

Metriken, Canaries, Exit-Codes ausführen

Überwachung

Skalierung

Wachstum ohne Chaos

Frontier, transaktionale Checkpoints

Skalierung

Kosten

Profitabel bleiben

Kosten pro gültigem Datensatz

Kosten

Was sind die wichtigsten Best Practices beim Web-Scraping?

Wenn Sie sich nur zehn Gewohnheiten aneignen, sollten es diese sein: Erstellen Sie einen Datenvertrag, bevorzugen Sie APIs und JSON gegenüber HTML, halten Sie sich korrekt an die robots.txt-Datei, identifizieren Sie Ihren Bot, passen Sie die Abfragehäufigkeit pro Host an, beachten Sie das Retry-After-Feld, legen Sie Timeouts fest, validieren Sie Datensätze vor dem Speichern, lassen Sie sich bei leeren oder inhaltsarmen Seiten benachrichtigen und speichern Sie Checkpoints zusammen mit den darin beschriebenen Daten.

Jede dieser zehn Best Practices für das Web-Scraping ist mit dem Abschnitt verlinkt, in dem sie erläutert wird.

  1. Erstellen Sie einen Datenvertrag, der Felder, Typen, einen eindeutigen Schlüssel und die Aktualität festlegt, bevor überhaupt Code existiert.
  2. Bevorzugen Sie eine offizielle API oder das JSON der Seite selbst gegenüber dem Parsen von HTML.
  3. Wenden Sie „robots.txt“ so an, wie es in RFC 9309 definiert ist, und nicht so, wie es Ihre Bibliothek zufällig tut.
  4. Identifizieren Sie Ihren Bot mit einem ehrlichen User-Agent und einer Kontaktadresse.
  5. Passen Sie die Zugriffsrate für jeden Host separat an, beginnen Sie langsam und passen Sie sich dem Server an.
  6. Beachten Sie „Retry-After“ und reduzieren Sie die Abfragehäufigkeit exponentiell mit Jitter.
  7. Legen Sie für jede Anfrage Zeitlimits für die Verbindung und das Lesen fest.
  8. Überprüfen Sie Datensätze vor dem Speichern und protokollieren Sie die Gründe für jede Ablehnung.
  9. Geben Sie Warnmeldungen bei leeren oder inhaltsarmen Seiten aus, nicht nur bei Abstürzen.
  10. Speichern Sie Checkpoints zusammen mit den Daten, die sie beschreiben, damit ein Absturz niemals dazu führt, dass eine Seite nur teilweise gespeichert wird.

Wie sollten Sie ein Web-Scraping-Projekt planen?

Gehen Sie bei der Planung rückwärts von den Daten aus. Notieren Sie sich die benötigten Felder, Datentypen, Aktualität und Datenvolumen und wählen Sie dann die kostengünstigste, zuverlässige Quelle aus. Ordnen Sie Web-Scraping-Techniken nach Kosten: zuerst eine offizielle API, dann das JSON, das die Seite bereits lädt, dann Sitemaps zur Erkennung, dann statisches HTML und einen Headless-Browser nur als letzten Ausweg. Legen Sie das Budget fest, bevor Sie Code schreiben.

Wenn du noch überlegst, ob du überhaupt scrapen sollst, beginne mit den Fragen, die du beantworten musst, bevor du einen Scraper schreibst.

Erstellen Sie zuerst einen Datenvertrag

Ein Datenvertrag ist eine kurze Tabelle, die genau festlegt, was der Scraper liefern muss. Hier ist ein Beispiel für einen Buchpreis-Monitor:

Feld

Typ

Erforderlich

Regel

URL

Zeichenkette (absolut)

ja

Eindeutiger Schlüssel

Titel

Zeichenkette

ja

nicht leer

Preis

Dezimal

ja

> 0

Währung

ISO 4217-Code

ja

z. B. GBP

auf Lager

Boolescher Wert

ja

aus dem Text zur Verfügbarkeit

Bewertung

Ganzzahl 1–5

nein

Null zulässig

abgeholt_am

UTC-Zeitstempel

ja

wird vom Abrufprogramm gesetzt

Der Monitor benötigt täglich aktuelle Daten, etwa 1.000 Zeilen pro Durchlauf.

Der Parser erfüllt diese Anforderung, die Validierung stellt sie sicher, und die Überwachung löst eine Warnung aus, wenn ein Durchlauf die Anforderungen nicht erfüllt.

Wählen Sie die günstigste zuverlässige Quelle

Arbeiten Sie diese Liste ab und entscheiden Sie sich für die erste Quelle, die alle Felder Ihrer Anforderungen erfüllt:

  1. Eine offizielle API. Wenn eine Website eine solche anbietet, ist das Abrufen von Daten über eine API besser als das Parsen von HTML: Sie erhalten stabile Feldnamen, dokumentierte Beschränkungen und explizite Nutzungsbedingungen.
  2. Das JSON, das die Seite bereits lädt. Öffnen Sie die Entwicklertools, filtern Sie den Reiter „Netzwerk“ nach „Fetch/XHR“ und laden Sie die Seite neu. Viele Websites rufen ihre Daten als JSON ab, bevor sie diese auf dem Bildschirm darstellen.
  3. Sitemaps zur Erkennung. Nutze die XML-Sitemaps der Website, um URLs zu erkennen, anstatt jeden Link zu crawlen. Deren lastmod Datumsangaben zeigen zudem an, welche Seiten sich seit deinem letzten Durchlauf geändert haben.
  4. Statisches HTML. Rufe die Seite ab und analysiere sie, wenn die Daten im Quellcode enthalten sind.
  5. Eine gerenderte Seite. Führe einen Headless-Browser nur dann aus, wenn die Daten nirgendwo anders vorhanden sind.

Jeder weitere Schritt bedeutet mehr Anfragen, mehr Parsing-Code und mehr Fehler, wenn sich die Website ändert.

Hier ist Schritt 2 in der Praxis: Die JavaScript-Version von quotes.toscrape.com lädt ihre Kursnotierungen von einem paginierten JSON-Endpunkt, sodass du den HTML-Code nie berührst.

json_endpoint.py

"""Prefer the JSON a page already loads over parsing its HTML."""
import requests

url = "https://quotes.toscrape.com/api/quotes"
page, quotes = 1, []
while True:
    data = requests.get(url, params={"page": page}, timeout=(5, 30),
                        headers={"User-Agent": "acme-quotes-bot/0.1 (+mailto:data@example.com)"}).json()
    quotes += [(q["text"], q["author"]["name"]) for q in data["quotes"]]
    if not data["has_next"]:
        break
    page += 1
print(len(quotes), "quotes from", page, "JSON pages; first:", quotes[0][1])
  • /api/quotes?page=N ist dieselbe Anfrage, die die Seite im Browser stellt und die im Reiter „Netzwerk“ zu finden ist.
  • has_next ist die Stoppbedingung, sodass man niemals die letzte Seite erraten oder leere Seiten abrufen muss.
  • Es gibt keinen HTML-Parser, da die Felder bereits benannt und typisiert vorliegen.

Budgetanfragen vor dem Schreiben des Codes

Ein grober Kostenvoranschlag zeigt, ob der Plan überhaupt realisierbar ist: Seiten × Häufigkeit ÷ zulässige Rate = Laufzeit.

Angenommen, du benötigst 10.000 Produktseiten pro Tag und die Website lässt eine Anfrage alle zwei Sekunden zu. Das sind 10.000 × 2 = 20.000 Sekunden oder etwa 5,6 Stunden Crawling pro Tag. Wenn das nicht passt, benötigen Sie weniger Seiten, ein inkrementelles Crawling oder eine Quelle mit mehr Datensätzen pro Anfrage. Schnelleres Crawling ist keine Option, da die Website die Rate festlegt.

Speichere die Rohdaten der Antworten für einige Tage

Speichere den abgerufenen Rohdaten-HTML- oder JSON-Code einige Tage lang, komprimiert und nach URL sowie Abrufzeit sortiert. Ein fehlerhafter Selektor oder ein neues Feld bedeutet dann, dass die gespeicherten Kopien neu geparst werden müssen, anstatt die Website erneut zu crawlen.

Scrapen Sie nur öffentliche Daten, für deren Erfassung Sie einen legitimen Grund haben, lesen Sie die Nutzungsbedingungen der Website und halten Sie sich von Bereichen fern, für die eine Anmeldung erforderlich ist, es sei denn, Sie haben die Erlaubnis dazu. Rufen Sie die Datei „robots.txt“ ab und wenden Sie sie gemäß der Definition in RFC 9309 an, identifizieren Sie Ihren Bot mit einer Kontakt-URL, vermeiden Sie personenbezogene Daten, die Sie nicht benötigen, und geben Sie sich niemals als Crawler einer Suchmaschine aus.

Wie funktioniert die Überprüfung anhand der robots.txt-Datei eigentlich?

Die robots.txt-Datei ist seit 2022 ein offizieller Standard, als die IETF den RFC 9309, das „Robots Exclusion Protocol“, veröffentlichte. Die wichtigsten Regeln:

  • Suchen Sie zuerst Ihre Gruppe. Gruppen beginnen mit User-agent Zeilen. Ihr Crawler folgt der Gruppe, die ihr Produkt-Token angibt, und die * Gruppe gilt nur, wenn keine andere zutrifft.
  • Die längste Übereinstimmung gewinnt. Wenn mehrere Regeln auf einen Pfad zutreffen, entscheidet die spezifischste (die mit den meisten Zeichen).
  • „Allow“ entscheidet bei Gleichstand. Wenn eine Allow und eine Disallow Regel mit gleicher Länge übereinstimmen, ist der Pfad zulässig.
  • Zwei Sonderzeichen. * steht für eine beliebige Zeichenfolge und $ fixiert das Ende des Pfades, sodass Disallow: /*.pdf$ jede URL blockiert, deren Pfad mit .pdf.
  • Statuscodes ändern die Bedeutung. Ein 4xx-Fehler in der robots.txt bedeutet, dass ein Crawler auf alles zugreifen darf. Ein 5xx-Fehler oder eine fehlgeschlagene Anfrage bedeutet, dass von einer vollständigen Sperrung ausgegangen werden muss.
  • Das Caching ist begrenzt. Verwenden Sie eine zwischengespeicherte Kopie nicht länger als 24 Stunden.
  • „Crawl-delay“ ist nicht im Standard enthalten, und Google ignoriert es. Halten Sie sich dennoch daran, wenn es vorhanden ist, da es die Wünsche des Website-Betreibers widerspiegelt.

Der RFC besagt außerdem, dass diese Regeln keine Form der Zugriffsberechtigung darstellen: Die Datei ist eine veröffentlichte Anforderung des Website-Betreibers, keine Sperre.

Warum Ihr „robots.txt“-Parser möglicherweise nicht dem Standard entspricht

Pythons „urllib.robotparser“ ist die naheliegende Wahl für Python-Scraper, aber es prüft die Regeln in der Reihenfolge der Dateien und hört bei der ersten Übereinstimmung auf, anstatt die längste auszuwählen. Außerdem behandelt es Regelpfade als einfache Präfixe, sodass * und $ bedeuten nichts Besonderes.

Der folgende Codeausschnitt wendet beide Ansätze auf eine kleine robots.txt-Datei an und gibt die Ergebnisse nebeneinander aus. Der „polite fetcher“ importiert später seine parse_robots() und rfc9309_allowed() importiert.

robots_match.py

"""robots.txt per RFC 9309: pick the group for your user agent, then the
longest matching rule wins, Allow wins ties, '*' matches any characters and
'$' anchors the end of the path."""
import re
import urllib.robotparser


def parse_robots(text, user_agent):
    """Return (rules, crawl_delay) for the group that applies to user_agent."""
    groups, current, in_agent_lines = [], None, False
    for raw in text.splitlines():
        line = raw.split("#", 1)[0].strip()
        if ":" not in line:
            continue
        key, value = (part.strip() for part in line.split(":", 1))
        key = key.lower()
        if key == "user-agent":
            if not in_agent_lines:  # a new group starts
                current = {"agents": [], "rules": [], "delay": None}
                groups.append(current)
            current["agents"].append(value.lower())
            in_agent_lines = True
            continue
        in_agent_lines = False
        if current is None:
            continue
        if key in ("allow", "disallow"):
            current["rules"].append((key, value))
        elif key == "crawl-delay":  # not in RFC 9309, but a clear signal
            try:
                current["delay"] = float(value)
            except ValueError:
                pass
    token = user_agent.split("/")[0].lower()  # product token, e.g. "acme-price-monitor"
    matched = [g for g in groups if token in g["agents"]] or \
              [g for g in groups if "*" in g["agents"]]
    rules = [rule for g in matched for rule in g["rules"]]
    delay = next((g["delay"] for g in matched if g["delay"] is not None), None)
    return rules, delay


def _pattern(rule_path):
    regex = re.escape(rule_path).replace(r"\*", ".*")
    if regex.endswith(r"\$"):
        regex = regex[:-2] + "$"
    return re.compile(regex)


def rfc9309_allowed(rules, path):
    """rules: list of ("allow"|"disallow", path) for the group that applies to you."""
    best_len, verdict = -1, True  # no matching rule means allowed
    for kind, rule_path in rules:
        if not rule_path:
            continue  # an empty Disallow allows everything
        if _pattern(rule_path).match(path):
            length = len(rule_path)
            if length > best_len or (length == best_len and kind == "allow"):
                best_len, verdict = length, kind == "allow"
    return verdict


if __name__ == "__main__":
    robots_txt = """User-agent: *
Disallow: /catalogue/
Allow: /catalogue/category/
Disallow: /*.pdf$

User-agent: acme-price-monitor
Disallow: /checkout/
Crawl-delay: 2
"""
    stdlib = urllib.robotparser.RobotFileParser()
    stdlib.parse(robots_txt.splitlines())
    rules, _ = parse_robots(robots_txt, "mybot/1.0")
    for path in ["/catalogue/category/books_1/index.html", "/catalogue/item_1.html",
                 "/files/report.pdf", "/files/report.pdf?v=2", "/index.html"]:
        print(f"{path:42} RFC 9309: {str(rfc9309_allowed(rules, path)):5}  "
              f"urllib.robotparser: {stdlib.can_fetch('mybot', path)}")
    rules, delay = parse_robots(robots_txt, "acme-price-monitor/1.2 (+https://example.com/bot)")
    print("acme-price-monitor group:", rules, "crawl-delay:", delay)
    print("/catalogue/item_1.html for acme-price-monitor:", rfc9309_allowed(rules, "/catalogue/item_1.html"))

Was der Vergleich zeigt:

  • /files/report.pdf ist gemäß urllib.robotparser, obwohl Disallow: /*.pdf$ es gemäß dem Standard verbietet, da der stdlib-Parser das * wörtlich aus.
  • /catalogue/category/books_1/index.html wird durch urllib.robotparser, da Disallow: /catalogue/ in der Datei an erster Stelle steht. Gemäß dem Standard hat die längere Allow: /catalogue/category/ und die Seite ist zulässig.
  • acme-price-monitor hat eine eigene Gruppe, die die * Gruppe vollständig ersetzt. Für diesen Bot /catalogue/item_1.html ist mit einer Crawling-Verzögerung von zwei Sekunden zulässig, da keine der * Regeln übertragen werden.

Verwenden Sie einen standardkonformen Matcher oder testen Sie Ihren Parser zumindest anhand der spezifischen Regeln, auf die Sie angewiesen sind.

Nutzungsbedingungen, Anmeldungen und personenbezogene Daten

Ein paar Regeln decken die meisten Situationen ab:

  • Lesen Sie zuerst die Nutzungsbedingungen. Eine Anmeldung bedeutet in der Regel, dass man diese akzeptiert, und viele verbieten den automatisierten Zugriff; behandeln Sie authentifizierte Bereiche daher ohne Erlaubnis als Sperrgebiet.
  • Erfassen Sie nur die unbedingt notwendigen personenbezogenen Daten – oder gar keine. Lassen Sie Namen, E-Mail-Adressen oder Profilangaben weg, die in Ihrem Datenvertrag nicht aufgeführt sind; Datenschutzgesetze wie die DSGVO gelten für personenbezogene Daten, unabhängig davon, ob Sie diese gescrapt haben oder nicht.
  • Umgehen Sie keine Paywalls, Anmeldungen oder CAPTCHAs. Sie signalisieren, dass der Eigentümer den Zugriff einschränken möchte.

Dies sind Arbeitsgewohnheiten, keine Rechtsberatung. Für rechtliche Einzelheiten lesen Sie unseren Leitfaden zur Rechtmäßigkeit von Web-Scraping.

Identifizieren Sie Ihren Scraper ehrlich

Verwenden Sie einen stabilen User-Agent, der Ihr Produkt, dessen Version und eine Möglichkeit, Sie zu erreichen, angibt:

acme-price-monitor/1.2 (+https://example.com/bot; data@example.com)

Ein Website-Betreiber, der ungewöhnlichen Traffic von dieser Zeichenfolge aus bemerkt, kann Sie kontaktieren, anstatt einen IP-Bereich zu sperren, und Autoren von „robots.txt“-Dateien erhalten ein Produkt-Token, auf das sie sich beziehen können.

Senden Sie niemals einen Googlebot-User-Agent. Google dokumentiert, wie Website-Betreiber seine Crawler anhand von Reverse- und Forward-DNS-Lookups oder seinen veröffentlichten IP-Bereichen verifizieren können; ein gefälschter User-Agent scheitert daher bei der Überprüfung und wirkt betrügerisch, sobald er entdeckt wird.

Wie schnell kannst du Daten scrapen, ohne eine Website zu überlasten?

Es gibt keine allgemein gültige sichere Rate. Beginnen Sie mit ein oder zwei gleichzeitigen Anfragen pro Host und einer Pause von etwa einer Sekunde, lassen Sie dann den Server das Tempo bestimmen: Verlangsamen Sie das Tempo, wenn die Latenz steigt, halten Sie an und warten Sie, wenn Sie einen 429- oder 503-Fehler erhalten, beachten Sie „Retry-After“ und reduzieren Sie die Anfragerate exponentiell mit Jitter.

Budget pro Host, nicht pro Scraper

Zehn Worker, die jeweils höflich eine Pause einlegen, können dennoch zehn parallele Anfragen an eine Domain senden. Regeln Sie das Tempo dort, wo jede Anfrage an einen Host durchläuft: über ein Fetcher-Objekt oder einen gemeinsamen Limiter, der auf allen Rechnern gilt.

Lassen Sie den Server das Tempo bestimmen

Eine steigende Latenz bedeutet, dass der Server stärker ausgelastet ist; verlängern Sie daher die Verzögerung, bevor Fehler auftreten. Planen Sie umfangreiche Crawls für die Nebenzeiten der Website ein, in der Regel nachts in der Zeitzone des Hauptmarktes.

Behandeln Sie 429 und „Retry-After“ korrekt

429 „Too Many Requests“ (RFC 6585) bedeutet, dass Sie innerhalb eines bestimmten Zeitraums zu viele Anfragen gesendet haben, und ein 503-Fehler bedeutet, dass der Server überlastet ist oder wegen Wartungsarbeiten nicht erreichbar ist. Beide können den „Retry-After“-Header enthalten, und die HTTP-Semantik (RFC 9110) lässt zwei Formate für dessen Wert zu:

Retry-After: 120
Retry-After: Fri, 09 Oct 2026 07:30:00 GMT

Das erste ist in Sekunden angegeben, das zweite als HTTP-Datum; ein Scraper, der nur eines davon verarbeitet, stürzt ab oder ignoriert den Header, wenn er auf das andere Format trifft. Behandeln Sie den Wert als Mindestwartezeit.

Exponentiell zurückhalten, mit Jitter

Wenn kein „Retry-After“-Header vorhanden ist oder ein Wiederholungsversuch erneut fehlschlägt, warte jedes Mal länger:

wait = min(cap, base × 2^attempt) × random(0.5 to 1.5)

Verwende den größeren Wert aus dieser Wartezeit und einem etwaigen „Retry-After“-Wert. Der Zufallsfaktor ist der Jitter: Ohne ihn versuchen Worker, die gleichzeitig fehlgeschlagen sind, den Versuch gemeinsam erneut und belasten den Server in einer synchronisierten Welle.

Wiederhole nur das, was wiederholt werden kann

Wiederhole 429-, 500-, 502-, 503- und 504-Antworten sowie Netzwerk-Timeouts bei idempotenten GET-Anfragen. Versuchen Sie nicht, einen 404-Fehler erneut auszuführen, da die Seite nicht zurückkommt, und bombardieren Sie keinen 403-Fehler, da dieselbe Anfrage dieselbe Antwort erhält. Begrenzen Sie die Versuche und protokollieren Sie jeden Abbruch, damit festgefahrene URLs in Ihren Metriken erscheinen.

Ein höflicher Abrufer in einer Klasse

Die PoliteFetcher nachstehende Klasse vereint diesen und den nächsten Abschnitt und importiert parse_robots() und rfc9309_allowed() aus dem robots.txt-Schnipsel importiert; bewahre daher beide Dateien in einem Ordner auf.

polite_fetch.py

"""A polite, resilient fetcher: robots.txt, per-host pacing, timeouts,
retries with backoff and jitter, Retry-After, and conditional requests."""
import email.utils
import random
import time
from urllib.parse import urlsplit

import requests

from robots_match import parse_robots, rfc9309_allowed  # Snippet 2

USER_AGENT = "acme-price-monitor/1.2 (+https://example.com/bot; data@example.com)"
RETRYABLE = {429, 500, 502, 503, 504}
ROBOTS_TTL = 24 * 3600  # RFC 9309: don't trust a cached copy for more than 24 h


class PoliteFetcher:
    def __init__(self, min_delay=1.0, max_retries=4, timeout=(5, 30)):
        self.session = requests.Session()
        self.session.headers["User-Agent"] = USER_AGENT
        self.min_delay, self.max_retries, self.timeout = min_delay, max_retries, timeout
        self.robots, self.last_hit, self.validators = {}, {}, {}

    def _robots(self, host):
        cached = self.robots.get(host)
        if cached and time.monotonic() - cached[0] < ROBOTS_TTL:
            return cached[1], cached[2]
        try:
            resp = self.session.get(host + "/robots.txt", timeout=self.timeout)
        except requests.RequestException:
            resp = None
        if resp is None or resp.status_code >= 500:
            rules, delay = [("disallow", "/")], None  # unreachable: assume full disallow
        elif resp.status_code >= 400:
            rules, delay = [], None  # unavailable (e.g. 404): no restrictions
        else:
            rules, delay = parse_robots(resp.text, USER_AGENT)
        self.robots[host] = (time.monotonic(), rules, delay)
        return rules, delay

    def allowed(self, url):
        parts = urlsplit(url)
        rules, _ = self._robots(f"{parts.scheme}://{parts.netloc}")
        path = (parts.path or "/") + (f"?{parts.query}" if parts.query else "")
        return rfc9309_allowed(rules, path)

    def _pace(self, host):
        _, crawl_delay = self._robots(host)
        delay = max(self.min_delay, crawl_delay or 0)
        wait = self.last_hit.get(host, 0) + delay - time.monotonic()
        if wait > 0:
            time.sleep(wait)
        self.last_hit[host] = time.monotonic()

    @staticmethod
    def _retry_after(value):
        if not value:
            return None
        if value.isdigit():
            return int(value)
        when = email.utils.parsedate_to_datetime(value)  # HTTP-date form
        return max(0, when.timestamp() - time.time())

    def get(self, url):
        if not self.allowed(url):
            raise PermissionError(f"robots.txt disallows {url}")
        parts = urlsplit(url)
        host = f"{parts.scheme}://{parts.netloc}"
        headers = {}
        etag, modified = self.validators.get(url, (None, None))
        if etag:
            headers["If-None-Match"] = etag
        if modified:
            headers["If-Modified-Since"] = modified
        for attempt in range(self.max_retries + 1):
            self._pace(host)
            try:
                resp = self.session.get(url, headers=headers, timeout=self.timeout)
            except (requests.ConnectionError, requests.Timeout):
                resp = None
            if resp is not None and resp.status_code not in RETRYABLE:
                if resp.status_code == 200:
                    self.validators[url] = (resp.headers.get("ETag"),
                                            resp.headers.get("Last-Modified"))
                return resp  # 200, 304, or a non-retryable error for the caller
            if attempt == self.max_retries:
                break
            backoff = min(60, 2 ** attempt) * random.uniform(0.5, 1.5)
            server_hint = self._retry_after(resp.headers.get("Retry-After")) if resp is not None else None
            time.sleep(max(backoff, server_hint or 0))
        raise RuntimeError(f"gave up on {url} after {self.max_retries + 1} attempts")


if __name__ == "__main__":
    f = PoliteFetcher(min_delay=1.0, max_retries=2)
    for url in ["https://books.toscrape.com/", "https://httpbin.org/etag/abc123",
                "https://httpbin.org/etag/abc123", "https://httpbin.org/status/404"]:
        r = f.get(url)
        print(r.status_code, len(r.content), "bytes", url)
    for url in ["https://httpbin.org/deny", "https://httpbin.org/status/503"]:
        try:
            f.get(url)
        except (PermissionError, RuntimeError) as exc:
            print("expected:", exc)

Die wichtigsten Zeilen:

  • _robots() Ruft die robots.txt mit Ihrem eigenen User-Agent ab, behandelt einen 4xx-Fehler als „keine Einschränkungen“ und einen 5xx-Fehler oder Netzwerkfehler als „Zugang verweigert“ und führt einen erneuten Abruf durch, sobald die zwischengespeicherte Kopie 24 Stunden alt ist.
  • _pace() erzwingt die Verzögerung pro Host und verwendet den „Crawl-delay“ der Website, wenn dieser länger ist als Ihr eigenes Minimum.
  • RETRYABLE begrenzt Wiederholungsversuche auf Ratenbegrenzungen und Serverfehler. Ein 404-Fehler wird direkt an den Aufrufer zurückgegeben.
  • _retry_after() verarbeitet sowohl das Sekunden- als auch das HTTP-Datumsformat, und die Schleife wartet so lange, wie entweder die Backoff-Zeit oder die vom Server angegebene Wartezeit länger ist.

Die Demo unten erhält bei einer wiederholten Anfrage einen 304-Status, gibt ohne erneuten Versuch einen 404-Status zurück, lehnt einen durch „robots“ gesperrten Pfad vor dem Senden ab und gibt bei einem dauerhaften 503-Status nach Erreichen der maximalen Versuche auf.

Welche Anfragegewohnheiten verhindern die meisten Scraping-Fehler?

Die meisten Fehler sind auf schlampige Anfragen zurückzuführen, nicht auf clevere Abwehrmaßnahmen. Lege immer Verbindungs- und Lese-Timeouts fest, verwende pro Host nur eine Sitzung, sende einen konsistenten und korrekten Header-Satz, akzeptiere Komprimierung, überspringe Bilder und Schriftarten, die du nicht benötigst, normalisiere URLs, um Duplikate zu vermeiden, und verwende bedingte Anfragen, damit unveränderte Seiten als leere 304-Antwort zurückgegeben werden.

  • Legen Sie für jede Anfrage separate Verbindungs- und Lese-Timeouts fest, beispielsweise 5 Sekunden und 30 Sekunden. requestskann ein Aufruf ohne Zeitlimit unbegrenzt warten und einen Worker blockieren.
  • Verwenden Sie einen requests.Session pro Host. Dadurch werden Verbindungen wiederverwendet, wiederholte TLS-Handshakes übersprungen und der Server entlastet.
  • Senden Sie bei jeder Anfrage denselben realistischen, korrekten Header-Satz mit einem User-Agent, der Sie identifiziert. Unser Leitfaden zu HTTP-Headern für das Web-Scraping erläutert, welche Header wichtig sind und in welcher Reihenfolge sie stehen sollten.
  • Lassen Sie die Komprimierung aktiviert. requests fordert standardmäßig gzip an; wenn Sie Header manuell erstellen, behalten Sie Accept-Encoding, da komprimiertes HTML beim Datenübertrag nur einen Bruchteil der Größe einnimmt.
  • Bereinigen Sie URLs, bevor sie in Ihre Warteschlange gelangen: Entfernen Sie Tracking-Parameter und #fragments, folgen Sie rel="canonical" Links und entfernen Sie Duplikate.
  • Überspringe Tracking-Pixel und doppelte Paginierungsansichten; lade nur das, was du auch auswerten wirst.

Speichern Sie Seiten im Cache und nutzen Sie bedingte Anfragen

Wenn eine Antwort einen ETag oder Last-Modified Header enthält, speichern Sie diesen und senden Sie ihn beim nächsten Mal als If-None-Match oder If-Modified-Sincezurück. Bei diesen bedingten Anfragen erhält eine unveränderte Seite einen 304 Not Modified ohne Body, was Ihnen Bandbreite und dem Server Arbeit spart.

PoliteFetcher.get() Das wird bereits so gehandhabt: Das validators Wörterbuch speichert den ETag sowie Last-Modified der letzten 200 Antworten und sendet diese beim nächsten Mal zurück. Ein 304-Status wird unverändert zurückgegeben, sodass Ihr Code die gespeicherte Kopie wiederverwendet.

Was sollten Sie tun, wenn eine Website beginnt, Ihren Scraper zu blockieren?

Führe eine Diagnose durch, bevor du eskalieren. Analysiere das Signal (403, 429, 503 oder einen 200-Code, der ein CAPTCHA oder eine leere Shell liefert), überprüfe zunächst deine eigene Zugriffsrate und die Header und gehe dann Schritt für Schritt vor: Verlangsame die Abfrage, korrigiere die Header, füge getestete Proxys hinzu, rendere die Seite und übergebe die schwierigen Ziele schließlich an eine verwaltete Scraping-API.

Fehlercode

Mögliche Ursache

Erste Maßnahme

429 Zu viele Anfragen

Ratenbegrenzung

„Retry-After“ beachten; Anzahl der gleichzeitigen Verbindungen pro Host verringern

503 mit „Retry-After“

Überlastung oder Wartungsarbeiten

Die angegebene Zeit abwarten; Backoff anwenden

403 Zugriff verweigert

IP gesperrt, Header-Abweichung oder Anmeldung erforderlich

Header und Bedingungen prüfen; nicht mit Wiederholungsversuchen übertreiben

200 mit einer Challenge- oder CAPTCHA-Seite

Bot-Abwehr

Behandeln Sie dies als Fehler bei der Validierung; verlangsamen Sie den Vorgang und eskalieren Sie anschließend

200 mit leerer Datenhülle

JavaScript-gerenderte Inhalte

Nach dem JSON-Aufruf suchen; nur bei Bedarf rendern

Zunehmende Timeouts

Serverauslastung oder Drosselung

Verlängern Sie die Wartezeiten; reduzieren Sie die Parallelität

Sie sind sich nicht sicher, was ein Code bedeutet? Sehen Sie sich unsere Liste der Proxy- und HTTP-Statusfehler an.

Soft-Blocks sind besonders gefährlich: Eine 200-Antwort, die eine Challenge-Seite oder keine Daten enthält, sieht nach einem Erfolg aus, und nur die Datenprüfungen weiter unten in diesem Leitfaden erkennen dies.

Websites blockieren Scraper, da starker automatisierter Datenverkehr die Server belastet und dem Unternehmen schaden kann. Die Liste der automatisierten Bedrohungen von OWASP führt Scraping als eigene Bedrohung (OAT-011) auf, und die meisten Abwehrmaßnahmen zielen auf Datenvolumen und Missbrauchsmuster ab, sodass höflicher, gut identifizierter Datenverkehr weniger davon auslöst.

Escalieren Sie in dieser Reihenfolge:

  1. Verlangsamen Sie den Datenverkehr und verringern Sie die Parallelität auf dem betroffenen Host.
  2. Korrigieren Sie Ihre Header, damit sie konsistent und vollständig sind.
  3. Fügen Sie Proxys hinzu, beginnend mit der Auswahl des richtigen Proxy-Typs für das Ziel. Testen Sie Ihre Proxys, bevor Sie die Kapazität erhöhen, und verteilen Sie dann die Last mithilfe rotierender Proxys.
  4. Rendern Sie die Seite in einem Headless-Browser, wenn die Daten erst nach Ausführung von JavaScript angezeigt werden.
  5. Übergeben Sie die schwierigsten Ziele an eine verwaltete Scraping-API.

Für Fingerprinting, CAPTCHAs und Stealth-Browser nutzen Sie unser Playbook, um zu scrapen, ohne blockiert zu werden.

Wann benötigen Sie tatsächlich einen Headless-Browser?

Nur dann, wenn sich die Daten nicht im HTML-Code oder in einem von der Seite ausgeführten JSON-Aufruf befinden. Vergleichen Sie den Quellcode der Seite mit den Angaben in den DevTools; wenn die Daten über eine XHR-Anfrage eingehen, rufen Sie diesen Endpunkt direkt auf. Rendern Sie nur Seiten, die wirklich JavaScript benötigen, blockieren Sie Bilder und Schriftarten und verwenden Sie Browserkontexte wieder, da das Rendern wesentlich langsamer und kostspieliger ist.

Ein Headless-Browser führt das JavaScript der Seite ohne sichtbares Fenster aus: die umfassendste Methode, eine Seite abzurufen – und die ressourcenintensivste. Zwei schnelle Tests zeigen dir, ob du einen benötigst:

  • Vergleiche „Quelltext anzeigen“ mit „Elemente“. Suche im Quelltext der Seite (Strg+U oder Cmd+Option+U) nach einem gewünschten Wert, beispielsweise einem Preis. Ist dieser vorhanden, reicht eine einfache HTTP-Anfrage aus; wird er nur im „Elemente“-Panel der DevTools angezeigt, wurde er von JavaScript generiert.
  • Überprüfen Sie auf der Registerkarte „Netzwerk“, ob eine Fetch-/XHR-Anfrage vorhanden ist, die die Daten enthält – wie im vorherigen Beispiel mit dem JSON-Endpunkt –, und rufen Sie diese direkt auf.

Wenn du doch ein Rendering benötigst, sende nur die URLs, die diese Tests nicht bestehen, an den Browser und blockiere Bilder, Schriftarten und Medien. Führe mit Playwright oder einem ähnlichen Tool einen Browserprozess mit einem neuen Kontext pro Auftrag aus, anstatt für jede Seite einen neuen Browser zu starten.

Scrapy-Nutzer können JavaScript nur für die Anfragen rendern, die es benötigen, und den Rest des Crawls über den schnellen HTTP-Downloader von Scrapy abwickeln.

Wie schreibt man Parser, die Layoutänderungen überstehen?

Verankern Sie Selektoren an Elementen, die sich selten ändern (IDs, Datenattribute, semantische Struktur, eingebettetes JSON-LD), anstatt an Styling-Klassen; führen Sie eine geordnete Liste von Fallback-Selektoren pro Feld, dekodieren Sie die Rohbytes korrekt und sorgen Sie dafür, dass fehlende Pflichtfelder deutlich als Fehler gemeldet werden. Ein Parser, der stillschweigend Leerzeichen zurückgibt, ist schlimmer als einer, der abstürzt.

Wählen Sie Selektoren, die eine Neugestaltung überstehen

Styling-Klassen wie .text-lg.font-bold ändern sich, sobald ein Designer die Seite bearbeitet. IDs, data-* Attribute, itemprop Mikrodaten, semantische Tags und JSON-LD-Blöcke ändern sich weitaus seltener, da anderer Code von ihnen abhängt. Halten Sie Selektoren zudem kurz, da ein vollständiger Pfad von <body> bricht, sobald sich ein umschließendes `div`-Element verschiebt.

CSS-Selektoren sind leichter zu lesen, während XPath nach Text auswählen und die Baumstruktur nach oben durchlaufen kann. Der Artikel „XPath vs. CSS-Selektoren“ behandelt die Vor- und Nachteile.

Halten Sie für jedes Feld geordnete Fallbacks bereit

Weisen Sie jedem Feld eine Liste von Selektoren zu, wobei der spezifischste an erster Stelle steht, und nehmen Sie den ersten, der passt. Das SELECTORS Wörterbuch im folgenden Snippet funktioniert auf diese Weise.

Bytes dekodieren, statt Text zu erraten

Achten Sie auf den „£“-Kodierungsfehler. Wenn ein Server Content-Type: text/html ohne Zeichensatz sendet, requests greift er beim Erstellen auf ISO-8859-1 zurück resp.text, sodass eine UTF-8-Seite falsch dekodiert wird, „£“ zu „£“ wird und kein Preis geparst werden kann. Übergeben Sie resp.contentstattdessen die Rohbytes an Ihren Parser weiter; BeautifulSoup liest den <meta charset>.

Testen Sie mit gespeicherten Seiten

Speichere ein paar echte Seiten als Testdaten. Wenn sich ein Selektor ändert, parse die gespeicherten Rohdaten erneut und vergleiche die Ausgabe vor und nach der Änderung.

Dieser Codeausschnitt wandelt die Buchkarten auf einer Katalogseite von books.toscrape.com in typisierte Datensätze um, validiert jeden einzelnen, gibt einen Qualitätsbericht aus und übergibt dem Parser anschließend eine Karte mit einer umbenannten Preiskategorie, um zu zeigen, wie eine Layoutänderung erkannt wird.

parse_validate.py

"""Parse defensively, normalize, validate, and report data quality."""
import json
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

# Ordered fallbacks: the first selector that matches wins.
SELECTORS = {
    "title": ["h3 a[title]", "h3 a"],
    "price": ["p.price_color", "[itemprop=price]"],
    "availability": ["p.availability", ".instock"],
    "url": ["h3 a[href]"],
}
RATINGS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}


@dataclass
class Book:
    url: str
    title: str
    price: Decimal
    currency: str
    in_stock: bool
    rating: int | None
    fetched_at: str  # provenance: when this record was fetched (UTC)


def first(card, field):
    for css in SELECTORS[field]:
        node = card.select_one(css)
        if node is not None:
            return node
    return None


def parse_card(card, page_url, fetched_at=""):
    errors = []
    link, title_node = first(card, "url"), first(card, "title")
    price_node, stock_node = first(card, "price"), first(card, "availability")
    title = (title_node.get("title") or title_node.get_text(strip=True)) if title_node else ""
    if not title:
        errors.append("title missing")
    raw_price = price_node.get_text(strip=True) if price_node else ""
    currency = "GBP" if "£" in raw_price else "UNKNOWN"
    try:
        price = Decimal(raw_price.replace("£", "").replace(",", ""))
        if price <= 0:
            errors.append("price not positive")
    except InvalidOperation:
        price = Decimal("NaN")
        errors.append(f"price unparseable: {raw_price!r}")
    rating_node = card.select_one("p.star-rating")
    rating = next((RATINGS[c] for c in (rating_node.get("class", []) if rating_node else [])
                   if c in RATINGS), None)
    if link is None:
        errors.append("url missing")
    book = Book(url=urljoin(page_url, link["href"]) if link else "", title=title, price=price,
                currency=currency, in_stock="in stock" in (stock_node.get_text(" ", strip=True).lower()
                                                          if stock_node else ""),
                rating=rating, fetched_at=fetched_at)
    return book, errors


def scrape(page_url):
    resp = requests.get(page_url, timeout=(5, 30),
                        headers={"User-Agent": "acme-catalogue-bot/0.1 (+mailto:data@example.com)"})
    resp.raise_for_status()
    fetched_at = datetime.now(timezone.utc).isoformat(timespec="seconds")
    # Pass bytes, not resp.text: with no charset in Content-Type, requests guesses
    # ISO-8859-1 and "£" becomes "£". BeautifulSoup reads the page's <meta charset>.
    soup = BeautifulSoup(resp.content, "html.parser")
    cards = soup.select("article.product_pod")
    seen, good, rejected = set(), [], []
    for card in cards:
        book, errors = parse_card(card, page_url, fetched_at)
        if book.url in seen:
            errors.append("duplicate url")
        seen.add(book.url)
        (rejected if errors else good).append((book, errors))
    report = {
        "page": page_url,
        "cards_found": len(cards),
        "valid": len(good),
        "rejected": len(rejected),
        "missing_rating": sum(1 for b, _ in good if b.rating is None),
        "sample": {k: str(v) for k, v in asdict(good[0][0]).items()} if good else None,
    }
    return good, rejected, report


if __name__ == "__main__":
    _, rejected, report = scrape("https://books.toscrape.com/catalogue/page-1.html")
    print(json.dumps(report, indent=2))
    for book, errors in rejected:
        print("REJECTED", book.url, errors)
    # Simulate a layout change: the price class is renamed.
    html = '<article class="product_pod"><h3><a href="x.html" title="T">T</a></h3><p class="cost">£1.00</p></article>'
    card = BeautifulSoup(html, "html.parser").select_one("article")
    print("after layout change:", parse_card(card, "https://books.toscrape.com/")[1])
  • first() durchläuft die Fallback-Liste jedes Feldes, sodass eine umbenannte Klasse keine Spalte leert.
  • BeautifulSoup(resp.content, ...) übergibt Bytes, wodurch der oben genannte Kodierungsfehler vermieden wird.
  • parse_card() sammelt Fehler, anstatt sie auszulösen, sodass eine fehlerhafte Karte die Seite nicht zum Stillstand bringt und jede Ablehnung einen Grund hat.
  • Die simulierte Layoutänderung gibt „Preis nicht parsbar“ anstelle eines leeren Preises zurück – genau das Signal, das Sie in Ihren Protokollen haben möchten.

Weitere Informationen zur Umwandlung von Rohseiten in Felder finden Sie in unserem Leitfaden zur Datenauswertung.

Wie überprüfen Sie die Qualität der gescrapten Daten?

Behandeln Sie jeden Datensatz als nicht vertrauenswürdig, bis er ein Schema erfüllt: erforderliche Felder vorhanden, korrekte Datentypen, sinnvolle Wertebereiche und ein eindeutiger Schlüssel. Verwenden Sie den Datentyp „Decimal“ für Geldbeträge, normalisieren Sie Währungen und Datumsangaben, führen Sie eine Duplikatsbereinigung anhand des Schlüssels durch, speichern Sie die Quell-URL und den Abrufzeitpunkt und erstellen Sie einen Bericht pro Durchlauf über gültige, abgelehnte und fehlende Werte.

Bei der Datenqualität scheitern Scraper oft unbemerkt: Das Abrufen gelingt, der Parser läuft, und wochenlang fließen falsche Werte in die Berichte ein. Diese Best Practices für das Data Scraping verhindern das:

  • Definieren Sie das Schema im Code. Die Book „dataclass“ im Parsing-Snippet ist der Datenvertrag in Python; „pydantic“ fügt weitere Prüfungen hinzu, aber eine „dataclass“ benötigt keine zusätzlichen Abhängigkeiten.
  • Legen Sie Werte lieber zurück, anstatt sie zu konvertieren. Wenn ein Preis nicht geparst werden kann, lehnen Sie den Datensatz mit einer Begründung ab, anstatt eine Null zu speichern; Ablehnungsgründe sind Ihre früheste Warnung vor einer Layoutänderung.
  • Speichern Sie Geldbeträge als Decimal, niemals als Float, da Floats die meisten Preise nicht exakt darstellen können. Normalisieren Sie Währungen auf ISO-4217-Codes und Zeitstempel auf UTC.
  • Achten Sie auf Nullwerte auf Feldebene, die im Vergleich zum letzten Durchlauf sprunghaft ansteigen.
  • Entdoppeln Sie anhand der kanonischen URL oder eines Geschäftsschlüssels wie einer SKU. Duplikate deuten in der Regel auf eine Paginierungsschleife oder denselben Artikel in zwei Kategorien hin.
  • Weisen Sie jeder Zeile ein source_url und fetched_at, damit jeder Wert auf seine Seite und seinen Zeitpunkt zurückverfolgt werden kann.
  • Erstellen Sie einen Qualitätsbericht pro Durchlauf: gefundene Karten, gültige, abgelehnte und fehlende optionale Felder. Das Parsing-Snippet gibt genau diese Informationen aus, und der nächste Abschnitt überwacht sie.

Prüfen Sie

Beispielregel

Erfasst

Erforderliche Felder

Titel und Preis vorhanden

Layoutänderungen

Typen

Preis wird als Dezimalzahl interpretiert

Kodierungsfehler, falscher Selektor

Bereiche

0 < Preis < 10.000

Verwechslungen bei Einheiten oder Währungen

Eindeutigkeit

eine Zeile pro URL

Paginierungsschleifen, Duplikate

Null-Rate

Bewertungs-Nullwerte dürfen nicht mehr als 5 Punkte über dem letzten Durchlauf liegen

Teilweise Fehler

Herkunft

source_url und fetched_at festgelegt

Nicht nachverfolgbare Datensätze

Wie überwacht man einen Web-Scraper in der Produktion?

Verfolgen Sie einige Metriken auf Ausführungsebene und lösen Sie bei Änderungen – nicht nur bei Abstürzen – Warnmeldungen aus: Erfolgsquote nach Statuscode, Datensätze pro Seite, leere Seiten, Null-Raten bei Feldern, Wiederholungsversuche, Latenz, Aktualität und Kosten pro gültigem Datensatz. Stellen Sie sicher, dass der Job bei Überschreitung der Schwellenwerte mit einem Ergebnis ungleich Null beendet wird, damit Ihr Scheduler – und nicht ein Kunde – dies als Erster bemerkt.

Ein abstürzender Scraper fällt leicht auf; einer, der still und leise die falschen Daten sammelt, hingegen nicht. Betrachten Sie die folgenden Schwellenwerte als Ausgangspunkte für die Feinabstimmung, nicht als Benchmarks.

Metrik

Warum sie wichtig ist

Beispiel für eine Warnmeldung

Erfolgsquote nach Statuscode

Erkennt Blockierungen und Ausfälle

Anteil der 2xx-Codes fällt unter 95 %

Einträge pro Seite

Erkennt Layoutänderungen

Unter 50 % des 7-Tage-Medianwerts

Leere Seiten

Unbemerkte Fehlfunktionen

Jede Seite mit 0 Datensätzen

Anteil der Nullwerte in Feldern

Teilweiser Ausfall

Anstieg um 10 Punkte gegenüber dem letzten Durchlauf

Wiederholungsversuche pro Anfrage

Drosselung

Durchschnitt über 0,3

p95-Latenz

Serverauslastung

Verdoppelt sich im Vergleich zur Basislinie, daher Verlangsamung

Aktualität

Verpasste Durchläufe

Daten älter als Ihr Ziel

Kosten pro gültigem Datensatz

Budgetabweichung

Anstieg um 25 % im Wochenvergleich

Zwei Vorgehensweisen machen diese Kennzahlen nützlicher:

  • Rufen Sie bei jedem Durchlauf einige „Canary“-URLs mit bekannten Werten ab, beispielsweise ein Produkt mit einem festen Titel. Ein falscher „Canary“ bedeutet, dass der Parser versagt hat, nicht die Daten.
  • Warnungen sollten eher bei Abweichungen als bei festen Zahlen ausgelöst werden. Ein Rückgang der Datensätze pro Seite im Vergleich zum 7-Tage-Median ist ein besserer Alarm für Layoutänderungen als jede absolute Zahl.

Das Crawl-Snippet im nächsten Abschnitt endet mit einer health() Funktion, die Abruffehler, leere Seiten und „Thin Pages“ kennzeichnet und anschließend mit Status 1 beendet wird. Scheduler wie cron, systemd, Airflow und CI-Runner behandeln dies als Fehler, sodass die Warnung von Tools ausgeht, die Sie ohnehin bereits einsetzen.

Informationen zur Planung, Zustellung und Wiederholung finden Sie im Abschnitt „So erstellen Sie Scraping-Jobs, die Sie wiederherstellen können“.

Wie lässt sich Web-Scraping skalieren, ohne dass es dabei zu Ausfällen kommt?

Skalieren Sie die Verwaltungslogik vor den Arbeitern, da fortgeschrittene Web-Scraping-Techniken wie verteiltes Crawling davon abhängen. Verwende eine deduplizierte URL-Frontier, speichere die Datensätze jeder Seite und deren „Done“-Flag in einer Transaktion, schreibe idempotent, damit bei Wiederholungsläufen keine Zeilen doppelt erfasst werden, setze Ratenbegrenzungen pro Domain für alle Worker durch und crawle inkrementell mithilfe von Sitemaps und bedingten Anfragen, anstatt alles erneut abzurufen.

Best Practices für skalierbares Web-Scraping drehen sich hauptsächlich um den Status, denn mehr Worker verstärken jegliche Fehler bei der Fortschrittsverfolgung.

  • Verwenden Sie eine „Frontier“ statt einer Liste: eine deduplizierte Warteschlange von URLs, die es einem Crawler ermöglicht, im Laufe des Vorgangs neue Seiten zu entdecken. Sind Sie neu im Bereich „Frontiers“? Beginnen Sie mit Web-Crawling in Python.
  • Gestalten Sie Checkpoints transaktionsbasiert. Speichern Sie die Datensätze einer Seite, die entdeckten Links und das „Done“-Flag in einer einzigen Transaktion, sodass bei einem Absturz entweder die gesamte Arbeit an dieser Seite oder gar nichts davon verloren geht.
  • Schreiben Sie idempotent. Primärschlüssel oder „Upserts“ sorgen dafür, dass ein doppelter Schreibvorgang keine Auswirkungen hat, sodass Wiederholungen und erneute Versuche sicher sind.
  • Setzen Sie den Vorgang fort, anstatt neu zu starten. Der nächste Durchlauf setzt an der ersten unvollendeten URL an.

Um zu sehen, wie das funktioniert, crawlen Sie quotes.toscrape.com mit dem folgenden Codeausschnitt, der eine SQLite-Frontier verwaltet und die Arbeit jeder Seite in einer Transaktion festschreibt. Führen Sie python resumable_crawl.py 3 aus, um nach drei Seiten anzuhalten, und führen Sie dann python resumable_crawl.py erneut aus: Der zweite Durchlauf setzt dort fort, wo der erste aufgehört hat, ohne doppelte Zeilen.

resumable_crawl.py

"""Resumable pagination crawl with a SQLite checkpoint and a run health check."""
import json
import sqlite3
import sys
import time
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

START = "https://quotes.toscrape.com/page/1/"
HEADERS = {"User-Agent": "acme-quotes-bot/0.1 (+mailto:data@example.com)"}


def open_db(path="crawl.db"):
    db = sqlite3.connect(path)
    db.executescript("""
        CREATE TABLE IF NOT EXISTS frontier (url TEXT PRIMARY KEY, done INTEGER DEFAULT 0);
        CREATE TABLE IF NOT EXISTS quotes (text TEXT, author TEXT, tags TEXT,
                                           page TEXT, PRIMARY KEY (text, author));
    """)
    db.execute("INSERT OR IGNORE INTO frontier(url) VALUES (?)", (START,))
    db.commit()
    return db


def crawl(db, max_pages):
    stats = {"pages": 0, "errors": 0, "rows": 0, "empty_pages": 0, "rejected": 0}
    while stats["pages"] < max_pages:
        row = db.execute("SELECT url FROM frontier WHERE done = 0 LIMIT 1").fetchone()
        if row is None:
            break  # frontier exhausted
        url = row[0]
        try:
            resp = requests.get(url, headers=HEADERS, timeout=(5, 30))
            resp.raise_for_status()
        except requests.RequestException:
            stats["errors"] += 1
            break  # leave it undone; the next run resumes here
        soup = BeautifulSoup(resp.content, "html.parser")
        quotes = soup.select("div.quote")
        stats["empty_pages"] += not quotes
        with db:  # rows, link discovery and the checkpoint commit together
            for q in quotes:
                text, author = q.select_one("span.text"), q.select_one("small.author")
                if text is None or author is None:
                    stats["rejected"] += 1  # malformed card: reject it, don't coerce
                    continue
                cur = db.execute(
                    "INSERT OR IGNORE INTO quotes VALUES (?, ?, ?, ?)",
                    (text.get_text(strip=True), author.get_text(strip=True),
                     ",".join(t.get_text(strip=True) for t in q.select("a.tag")), url))
                stats["rows"] += cur.rowcount
            nxt = soup.select_one("li.next a[href]")
            if nxt:
                db.execute("INSERT OR IGNORE INTO frontier(url) VALUES (?)",
                           (urljoin(url, nxt["href"]),))
            db.execute("UPDATE frontier SET done = 1 WHERE url = ?", (url,))
        stats["pages"] += 1
        time.sleep(1.0)  # pace requests to one host
    return stats


def health(stats, db, min_rows_per_page=5):
    problems = []
    if stats["errors"]:
        problems.append(f"{stats['errors']} fetch errors")
    if stats["empty_pages"]:
        problems.append(f"{stats['empty_pages']} pages had no records (layout change?)")
    if stats["rejected"]:
        problems.append(f"{stats['rejected']} cards missing text or author")
    if stats["pages"] and stats["rows"] / stats["pages"] < min_rows_per_page:
        problems.append("rows per page below threshold")
    total = db.execute("SELECT COUNT(*) FROM quotes").fetchone()[0]
    pending = db.execute("SELECT COUNT(*) FROM frontier WHERE done = 0").fetchone()[0]
    return {**stats, "total_rows": total, "pending_urls": pending, "problems": problems}


if __name__ == "__main__":
    max_pages = int(sys.argv[1]) if len(sys.argv) > 1 else 100
    db = open_db()
    report = health(crawl(db, max_pages), db)
    print(json.dumps(report))
    sys.exit(1 if report["problems"] else 0)
  • with db: umschließt die eingefügten Zitate, den neu gefundenen Link zur nächsten Seite und die done = 1 Update in einer Transaktion zusammen.
  • INSERT OR IGNORE Die Abfrage anhand von Primärschlüsseln macht jeden Schreibvorgang idempotent, sowohl für Frontier-URLs als auch für Zitate.
  • Ein fehlgeschlagener Abruf lässt seine URL unberücksichtigt und beendet den Durchlauf, sodass der nächste Durchlauf diesen Versuch als Erstes wiederholt.
  • Karten, bei denen ein Zitat oder ein Autor fehlt, werden übersprungen und gezählt, anstatt halb leer gespeichert zu werden.
  • health() wandelt die Zähler des Durchlaufs in eine Liste von Problemen um und sys.exit(1) macht diese Probleme für denjenigen sichtbar, der den Job plant.

Sobald die Buchhaltung solide ist, sorgen drei weitere Vorgehensweisen für eine sichere Kapazitätserweiterung:

  • Teilen Sie die Ratenbegrenzungen pro Domain. Über mehrere Maschinen hinweg schützt die lokale Drosselung die Website nicht mehr; verwalten Sie daher ein Budget pro Domain an einem gemeinsamen Ort, beispielsweise in einem Token-Bucket in Redis.
  • Führen Sie das Crawling schrittweise durch. Verwenden Sie Sitemap- lastmod , um nur neue oder geänderte URLs in die Warteschlange zu stellen, und bedingte Anfragen, um unveränderte Seiten in leere 304-Antworten umzuwandeln.
  • Skalieren Sie horizontal erst als Letztes. Fügen Sie Worker erst hinzu, wenn die Host-Limits zentral durchgesetzt werden – so bedeuten mehr Worker mehr parallel laufende Hosts und nicht mehr Druck auf einen einzelnen.

Wie halten Sie die Kosten für das Web-Scraping unter Kontrolle?

Messen Sie die Kosten pro gültigem Datensatz, nicht die Kosten pro Anfrage. Die meisten Einsparungen ergeben sich aus einem geringeren Abrufaufwand: Zwischenspeichern und bedingte Anfragen nutzen, JSON-Endpunkte bevorzugen, nur bei Bedarf rendern, Wiederholungsversuche begrenzen, Proxys nur für Hosts verwenden, die diese benötigen, und inkrementell crawlen. Vergleichen Sie diese Zahl dann ehrlich mit einer verwalteten Scraping-API, bevor Sie weitere Lösungen entwickeln.

Die Formel ist einfach:

cost per valid record = (infrastructure + proxies + engineering hours) ÷ valid records delivered

Entwicklerstunden gehören in den Zähler, da ein Scraper, der jede Woche einen Entwickler benötigt, nicht billig ist. Der Nenner zählt nur Datensätze, die die Validierung bestanden haben.

Hebel

Effekt

Siehe

Bedingte Anfragen und Caching

Unveränderte Seiten geben einen leeren 304-Status zurück

Anfragegewohnheiten

JSON-Endpunkte statt HTML

Weniger Anfragen, einfacheres Parsen

Planung

Nur bei Bedarf rendern

Vermeidet Browser-Kosten bei statischen Seiten

Headless-Browser

Wiederholungsgrenzen und Backoff

Verhindert Wiederholungsstürme

Anfragerate

Proxys nur dort, wo sie benötigt werden

Bezahlung von IPs nur für Hosts, die blockieren

Blockierungen

Inkrementelle Crawls

Nur neue oder geänderte URLs abrufen

Skalierung

Validierung vor der Speicherung

Keine Kosten für die Speicherung oder den Versand fehlerhafter Datensätze

Datenqualität

Selbst entwickeln oder kaufen

Eine Eigenentwicklung ist bei wenigen stabilen, schwach geschützten Zielen sinnvoll. Wenn Proxys, Wiederholungsversuche, Rendering und Blockierungen mehr Entwicklungszeit kosten, als die Daten wert sind, vergleichen Sie die Kosten pro gültigem Datensatz bei den verschiedenen Optionen in unserer Übersicht über Web-Scraper und Scraping-APIs. Wenn Sie diese Infrastruktur lieber gar nicht betreiben möchten, ist die Scraper-API von WebScrapingAPI eine verwaltete Scraping-API, die sich um Proxys, Wiederholungsversuche und optionales JavaScript-Rendering kümmert. JavaScript-Rendering und Geotargeting kosten dabei keine zusätzlichen Credits, und fehlgeschlagene Anfragen werden nicht in Rechnung gestellt.

Welche gängigen Web-Scraping-Tipps sollten Sie ignorieren?

Ignorieren Sie Ratschläge, die kurzfristigen Zugriff gegen Risiken eintauschen: das Fälschen eines Googlebot-User-Agents, das Rotieren von User-Agents bei jeder Anfrage mit nicht übereinstimmenden Headern, standardmäßig per Skript gesteuerte „zufällige“ Mausbewegungen, feste Verzögerungen, die Serversignale ignorieren, das endlose Wiederholen jedes Fehlers und das Scrapen von allem, nur für den Fall. Jeder dieser Punkte verursacht zusätzliche Kosten, Risiken oder fehlerhafte Daten.

„Verwende einen Googlebot-User-Agent, damit Websites dich hereinlassen.“ Website-Betreiber können einen gefälschten Googlebot durch eine DNS-Abfrage entlarven. Verwende einen ehrlichen User-Agent mit Kontaktdaten, wie im Abschnitt zur Identifizierung beschrieben.

„Wechseln Sie den User-Agent bei jeder Anfrage.“ Ein Browser-User-Agent mit Headern, die kein echter Browser senden würde, fällt stärker auf als eine konsistente Identität; behalten Sie daher pro Sitzung einen einheitlichen Header-Satz bei.

„Füge zufällige Mausbewegungen und Klicks hinzu, um menschlich zu wirken.“ Bei den meisten Zielen erhöht dies die Komplexität und verlangsamt jede Seite, ohne das eigentliche Problem – das Anfragevolumen – zu beheben; passe daher zuerst deine Anfragefrequenz und Header an.

„Warte 5 bis 10 Sekunden zwischen den Anfragen, dann bist du auf der sicheren Seite.“ Eine feste Verzögerung ist zu langsam für eine Website, die gut zurechtkommt, und zu schnell für eine, die 429-Fehler zurückgibt. Verwende eine hostbezogene Ratenregelung, die sich an die Latenz und den Retry-After-Wert anpasst.

„Versuche es so lange, bis es funktioniert.“ Unbegrenzte Wiederholungsversuche verwandeln einen Ausfall in eine Flut von Wiederholungsanfragen. Versuche nur Fehler erneut, die wiederholt werden können, wende Jitter an und begrenze die Anzahl der Versuche.

„Scrape alles, nur für den Fall.“ Zusätzliche Seiten kosten Bandbreite, Speicherplatz und Serverzeit, und zusätzliche Felder erhöhen dein Risiko, wenn sie personenbezogene Daten enthalten. Sammle nur das, was in deinem Datenvertrag aufgeführt ist.

„Crawl-Delay ist ein Standard, an den sich alle halten.“ Er ist nicht Teil von RFC 9309, und Google ignoriert ihn. Halten Sie sich daran, wenn er vorhanden ist, aber erwarten Sie nicht, dass jeder Crawler oder Parser ihn auf die gleiche Weise interpretiert.

„Meine Bibliothek kümmert sich für mich um robots.txt.“ requests und BeautifulSoup rufen robots.txt überhaupt nicht ab und wenden es auch nicht an. Scrapy kann dies über seine ROBOTSTXT_OBEY Einstellung, die bei mit scrapy startproject aktivieren. Überprüfen Sie Ihre Einstellungen und testen Sie den Matcher anhand der Regeln, auf die Sie sich verlassen.

Checkliste für Best Practices beim Web-Scraping

Bevor ein Scraper in die Produktion geht, stellen Sie sicher, dass er über einen schriftlichen Datenvertrag, eine standardkonforme robots.txt-Prüfung, einen ehrlichen User-Agent, eine hostbezogene Auslastungssteuerung mit Retry-After-Unterstützung, Timeouts, bedingte Anfragen, Fallback-Selektoren, Schema-Validierung, Warnmeldungen zum Betriebszustand, transaktionale Checkpoints und eine nachverfolgte Kosten pro gültigem Datensatz verfügt.

Jede Zeile enthält einen „Fertig, wenn“-Test, sodass Sie feststellen können, ob ein Scraper für den Produktivbetrieb bereit ist.

Vorbereitungsphase

Prüfung

Erledigt, wenn

Plan

Datenvertrag erstellt

Felder, Typen, Schlüssel und Aktualität vereinbart

Rechtliches

Bedingungen und robots.txt geprüft

Standardkonformer Matcher im Abrufpfad

Identität

Ehrlicher User-Agent

Name, Version und Kontakt-URL enthalten

Rate

Pacing pro Host

„Retry-After“ wird beachtet; Backoff mit Jitter; Obergrenze für Wiederholungsversuche festgelegt

Anfragen

Timeouts und Sitzungen

Zeitüberschreitungen für Verbindung und Lesen überall festgelegt

Caching

Bedingte Anfragen

ETag und Last-Modified werden gespeichert und gesendet

Parsen

Fallback-Selektoren

An den Parser übergebene Bytes; Fixtures in Tests

Qualität

Schema-Validierung

Protokollierte Ablehnungsgründe; Duplikatsbereinigung anhand des Schlüssels

Überwachung

Laufzustand

Beendigung mit Wert ungleich Null bei Schwellenwertüberschreitung; Canaries eingerichtet

Skalierung

Checkpoints

Daten- und Fortschritts-Commit in einer Transaktion

Kosten

Einheitskosten

Kosten pro gültigem Datensatz, wöchentlich erfasst

Möchten Sie diese Fähigkeiten von Grund auf erlernen? Wählen Sie aus unseren besten Web-Scraping-Kursen.

FAQ

Was sind Best Practices beim Web-Scraping?

Best Practices beim Web-Scraping sind die Vorgehensweisen, die dafür sorgen, dass Scraper legal, rücksichtsvoll, präzise und kostengünstig arbeiten. Dazu gehören das Sammeln nur der benötigten Daten, die Einhaltung von robots.txt und den Nutzungsbedingungen der Website, die Identifizierung Ihres Bots, die Begrenzung der Zugriffe pro Host, die Beachtung von „Retry-After“, die Validierung von Datensätzen, die Überwachung von Läufen, die Erfassung von Fortschrittspunkten und die Erfassung der Kosten pro gültigem Datensatz.

Wie viele Anfragen pro Sekunde sind beim Scraping einer Website sicher?

Es gibt keine Zahl, die überall sicher ist. Beginnen Sie mit einer oder zwei gleichzeitigen Anfragen pro Host im Abstand von etwa einer Sekunde und passen Sie sich dann an: Verlangsamen Sie das Tempo, wenn die Antwortzeiten steigen, halten Sie bei 429- oder 503-Fehlern an, beachten Sie „Retry-After“ und führen Sie umfangreiche Crawls außerhalb der Spitzenzeiten durch.

Muss ich mich beim Scraping an die robots.txt halten?

Technisch gesehen hindert Sie nichts daran, aber „robots.txt“ ist die vom Betreiber veröffentlichte Regel für automatisierte Clients, und deren Missachtung führt zu Sperrungen. Wenden Sie sie gemäß der Definition in RFC 9309 an, verwenden Sie dabei die eigene Gruppe Ihres Bots, falls vorhanden, und lesen Sie auch die Nutzungsbedingungen der Website.

Hält sich der Python-Robotparser an den robots.txt-Standard?

Nicht vollständig. Pythons `urllib.robotparser` wendet die Regeln in der Reihenfolge der Dateien an, anstatt die längste Übereinstimmung auszuwählen. Daher kann es URLs zulassen, die eine Website verbietet, oder URLs blockieren, die sie zulässt. Außerdem ignoriert es die in RFC 9309 definierten Platzhalter * und $.

Welchen User-Agent sollte ich für das Web-Scraping verwenden?

Verwende einen ehrlichen, stabilen User-Agent, der deinen Bot benennt und eine Kontakt-URL oder E-Mail-Adresse angibt, damit Website-Betreiber dich kontaktieren können, anstatt dich zu blockieren. Gib dich niemals als Googlebot aus, da Google veröffentlicht, wie sich seine Crawler verifizieren lassen.

Wie sollte ein Scraper mit einer 429-„Too Many Requests“-Antwort umgehen?

Stellen Sie das Senden von Anfragen an diesen Host ein, lesen Sie den „Retry-After“-Header und warten Sie mindestens so lange; der Wert ist entweder in Sekunden oder als HTTP-Datum angegeben. Fehlt der Header, reduzieren Sie die Anfragedichte exponentiell mit Jitter, begrenzen Sie die Anzahl der Wiederholungsversuche und senken Sie die Parallelität pro Host, bevor Sie fortfahren.

Wie verhindere ich, dass mein Scraper ausfällt, wenn eine Website ihr Layout ändert?

Du kannst Neugestaltungen nicht verhindern, aber du kannst sie überstehen: Verknüpfe Selektoren mit stabilen Ankerpunkten wie IDs, Datenattributen oder JSON-LD, halte Feld-spezifische Fallbacks bereit, melde fehlende Pflichtfelder deutlich und gib eine Warnung aus, wenn die Anzahl der Datensätze pro Seite sinkt.

Wie überprüfe ich die Qualität der gescrapten Daten?

Validieren Sie jeden Datensatz vor dem Speichern anhand eines Schemas: Pflichtfelder, korrekte Datentypen, sinnvolle Wertebereiche und einen eindeutigen Schlüssel. Verwenden Sie den Datentyp „Decimal“ für Preise, führen Sie eine Duplikatsbereinigung anhand des Schlüssels durch, protokollieren Sie Ablehnungsgründe und vergleichen Sie Nullraten und Zeilenanzahlen über verschiedene Durchläufe hinweg.

Benötige ich einen Headless-Browser für das Web-Scraping?

In der Regel nicht. Wenn sich die Daten im Quellcode der Seite oder in einer JSON-Anfrage auf der Registerkarte „Netzwerk“ befinden, rufe diese direkt ab. Verwende einen Headless-Browser nur für Seiten, deren Inhalt mit JavaScript generiert wird und die Bilder und Schriftarten blockieren.

Wie kann ich das Web-Scraping beschleunigen, ohne blockiert zu werden?

Rufen Sie weniger Daten ab, anstatt intensiver zu scrapen: JSON-Endpunkte statt gerenderter Seiten, bedingte Anfragen, damit unveränderte Seiten einen 304-Status zurückgeben, sitemap-gesteuerte inkrementelle Crawls und die Wiederverwendung von Verbindungen. Parallelisieren Sie dann über Hosts hinweg, nicht innerhalb eines Hosts, und legen Sie dabei Host-spezifische Limits fest.

Ist es in Ordnung, Daten hinter einer Anmeldung zu scrapen?

Seien Sie vorsichtig. Sich anzumelden bedeutet in der Regel, die Nutzungsbedingungen der Website zu akzeptieren, von denen viele den automatisierten Zugriff verbieten. Behandeln Sie authentifizierte Bereiche daher ohne Erlaubnis oder Vertrag als Sperrgebiet. Wenn Sie eine Erlaubnis haben, bewahren Sie Anmeldedaten in einem Secrets-Manager auf, beschränken Sie die Erfassung personenbezogener Daten und halten Sie sich an die üblichen Ratenbeschränkungen.

Wann sollte ich eine Web-Scraping-API verwenden, anstatt meinen eigenen Scraper zu entwickeln?

Entwickeln Sie selbst, wenn die Ziele wenige, stabil und nur schwach geschützt sind. Nutzen Sie eine verwaltete Scraping-API, wenn Proxys, Wiederholungsversuche, Rendering und Blockierungen mehr Entwicklungszeit in Anspruch nehmen, als die Daten wert sind, oder wenn Sie viele Länder abdecken müssen, und vergleichen Sie die Kosten pro gültigem Datensatz, einschließlich der Entwicklungsstunden.

Verbringen Sie Ihre Zeit mit den Daten, nicht mit der Infrastruktur

Alle oben genannten Vorgehensweisen können Sie intern abwickeln, und für viele Teams ist das die richtige Entscheidung. Ist dies nicht der Fall, können Sie die Scraper-API mit 5.000 kostenlosen Anfragen ausprobieren und sie Proxys, Wiederholungsversuche und optionales JavaScript-Rendering übernehmen lassen, dynamische Seiten über die Browser-API mit einer einzigen REST-Anfrage ausführen sowie Pay-as-you-go-Proxys für Privatanwender ab 8,50 $ pro GB hinzufügen oder strukturierte Datensätze aus 116 unterstützten Quellen über die Data-API abrufen.

Über den Autor

Anda Miuțescu, Autor für technische Inhalte @ WebScrapingAPI

Anda Miuțescu

Autor für technische Inhalte

Anda Miuțescu ist Technical Content Writer bei WebScrapingAPI und verfasst verständliche, nützliche Inhalte, die Entwicklern helfen, das Produkt und seine Funktionen besser zu verstehen.

Nodriver-Python-Tutorial: Unauffälliges Web-Scraping – vom ersten Skript bis zur Produktion
Die Wissenschaft des Web-Scrapings

Nodriver-Python-Tutorial: Unauffälliges Web-Scraping – vom ersten Skript bis zur Produktion

TL;DR: Kurzfassung: Nodriver ist eine asynchrone Python-Bibliothek, die Chromium über das Chrome DevTools Protocol steuert, ohne Selenium oder chromedriver-Binary. Nutzen Sie sie, wenn ein selbst gehosteter Stealth-Browser, der einer echten Chrome-Sitzung näherkommt, besser passt als ein WebDriver-Stack – aber sie beseitigt IP-, TLS- oder Verhaltensrisiken nicht von allein. Dieses Tutorial führt vom ersten Skript bis zur containerisierten Produktion und einer gehosteten Browser API.

Suciu Dan12 min read
Artikel lesen
Umgang mit Google-/goto-Weiterleitungs-URLs und CAES-Token in einem SERP-Scraper
Die Wissenschaft des Web-Scrapings

Umgang mit Google-/goto-Weiterleitungs-URLs und CAES-Token in einem SERP-Scraper

TL;DR: Kurzfassung: Google-/goto-URLs mit CAES-Token und ältere /url-Wrapper sind Weiterleitungsschichten, keine Ziele; ein SERP-Scraper, der jede href als endgültig behandelt, verfälscht Rank-Tracking und Link-Datenbanken. Bevorzugen Sie eine strukturierte SERP-API, wenn sie Ziel-Links liefert. Wenn Sie HTML parsen müssen, klassifizieren Sie jede href, deduplizieren Sie Weiterleitungen und lösen Sie sie mit einem einfachen GET auf. Entschlüsseln Sie CAES nicht.

Suciu Dan11 min read
Artikel lesen
HTTP-Antwort-Header in cURL: Jedes Flag, jede Technik und jedes Skripting-Rezept
Die Wissenschaft des Web-Scrapings

HTTP-Antwort-Header in cURL: Jedes Flag, jede Technik und jedes Skripting-Rezept

TL;DR: cURL verbirgt standardmäßig Antwort-Header. Verwenden Sie -i, um Header neben dem Body zu sehen, -I für eine HEAD-Anfrage, die nur Header zurückgibt, -v für vollständiges Debugging von Anfrage und Antwort und -D, um Header in einer Datei zu speichern. Für modernes Scripting können Sie mit cURL 7.83+ einzelne Header extrahieren oder mit der Option -w write-out alle Header als JSON ausgeben.

Suciu Dan11 min read
Artikel lesen

Los geht’s

Sind Sie bereit, Ihre Datenerfassung zu erweitern?

Schließen Sie sich den über 2.000 Unternehmen an, die WebScrapingAPI nutzen, um Webdaten im Unternehmensmaßstab ohne zusätzlichen Infrastrukturaufwand zu extrahieren.