Zum Inhalt springen
Zurück zum Blog

Web-Scraping mit Python im Jahr 2026: Das praktische Handbuch für Entwickler

Raluca PenciucZuletzt aktualisiert am 30 min read
Web-Scraping mit Python im Jahr 2026: Das praktische Handbuch für Entwickler
TL;DR: Dies ist ein praxisorientierter Leitfaden zum Web-Scraping mit Python, der mit einem Entscheidungsbaum beginnt und anschließend Requests in Verbindung mit Beautiful Soup, XPath mit lxml, Playwright, Selenium, Scrapy sowie eine Scraping-API für schwierige Ziele behandelt. Am Ende verfügen Sie über funktionierenden Code, ein Handbuch zur Umgehung von Blockierungen, LLM-fähige Speichermuster und eine Checkliste für den produktiven Einsatz.

Wenn du python web scraping in eine Suchleiste eingeben, erhalten Sie Hunderte von Tutorials, die alle dieselben drei Bibliotheken installieren und vor den interessanten Teilen aufhören. Dieser Leitfaden ist anders. Web-Scraping mit Python ist der Vorgang, bei dem ein Skript geschrieben wird, das eine Webseite abruft, deren HTML analysiert und strukturierte Felder extrahiert, die Sie in eine Tabellenkalkulation, eine Datenbank oder eine Modell-Pipeline einfügen können. Die Mechanik ist einfach. Der Grund, warum viele damit Schwierigkeiten haben, ist, dass das richtige Tool von der Zielwebsite abhängt und die Wahl des falschen Tools Stunden an Zeit kostet.

Dieses Tutorial vertritt eine bestimmte Sichtweise. Es beginnt mit einem kurzen Entscheidungsbaum, damit du nicht Playwright für eine Seite installierst, die Requests bereits in fünfzig Zeilen hätte analysieren können. Anschließend durchläuft es jede Ebene eines echten Scraping-Stacks: statisches HTML, JavaScript-Rendering, CSS-Selektoren im Vergleich zu XPath, Bereinigung von Feldern, Skalierung mit Scrapy, Proxy-Rotation, Behandlung von HTTP-Fehlern, Speicherung der Ausgabe in CSV, SQLite oder JSONL für nachgelagerte LLMs sowie die Planung wiederkehrender Läufe.

Sie sehen den Code für dasselbe Ziel aus verschiedenen Bibliotheken nebeneinander, sodass Kompromisse sichtbar werden und nicht nur vage angedeutet werden. Außerdem finden Sie ehrliche Anmerkungen zu Blockierungen, Wartungskosten und dazu, wann eine gehostete API eine bessere Nutzung Ihrer Zeit darstellt als eine andere time.sleep(random.uniform(1, 3)) . Wenn Sie bereits einen Python-Scraper geschrieben haben, der in der Produktion versagt hat, zeigt Ihnen dieser Leitfaden, warum das passiert ist und wie Sie eine Wiederholung vermeiden können.

Warum Python-Web-Scraping im Jahr 2026 die Standardwahl ist

Python-Web-Scraping dominiert den Bereich aus drei Gründen: lesbare Syntax, ein ausgereiftes Ökosystem und eine reibungslose Integration in den restlichen Daten-Stack. Ein Dutzend Zeilen Python reichen aus, um eine Seite herunterzuladen, sie zu parsen und dir eine Liste von Dictionaries zu liefern, die für pandas bereit sind. Dieser kurze Weg von der „URL“ zum „DataFrame“ ist der Grund, warum Dateningenieure, Analysten und ML-Teams immer wieder darauf zurückgreifen, anstatt auf Go oder Node.

Das Angebot an Bibliotheken ist außergewöhnlich umfangreich. „Requests“ und „httpx“ übernehmen die HTTP-Ebene. „Beautiful Soup“ und „lxml“ analysieren HTML entweder mit CSS-Selektoren oder XPath. „Playwright“ und „Selenium“ steuern echte Browser für JavaScript-gerenderte Seiten. Scrapy bietet ein vollständiges Crawling-Framework mit Parallelität, Wiederholungsversuchen, Item-Pipelines und Exportformaten, die sofort einsatzbereit sind. Jedes einzelne davon ist praxiserprobt und wird aktiv gepflegt.

Die zweite Hälfte der Geschichte betrifft die Integrationsmöglichkeiten. Die gescrapten Daten fließen direkt in pandas, DuckDB, SQLite, Parquet oder JSONL zur Einbindung in LLMs. Notebooks beschleunigen die Iteration. Typhinweise und Dataclasses sorgen für korrekte Datensätze. Zudem gibt es keine nennenswerte Lücke zwischen Prototyp und Produktion, da dasselbe Skript, das in Jupyter funktioniert, auch unter Cron oder in einem Container läuft, ohne dass eine Neuprogrammierung erforderlich ist. Diese durchgängige Kontinuität macht Python im Jahr 2026 zur pragmatischen Standardwahl für das Web-Scraping, und deshalb stützt sich der Rest dieses Leitfadens ausschließlich darauf.

Das richtige Tool auswählen: Ein Entscheidungsbaum für Ihre Zielwebsite

Bevor Sie irgendetwas installieren, beantworten Sie vier Fragen zur Zielwebsite. Jede einzelne schränkt die Auswahl an Bibliotheken um eine Größenordnung ein, und das Befolgen dieses Entscheidungsbaums spart mehr Zeit als jeder Optimierungstrick.

Schritt 1: Verfügt die Website über eine offizielle API? Wenn ja, nutzen Sie diese. Eine API ist schneller, kostengünstiger und rechtlich einwandfreier als das Scraping. Reservieren Sie das Scraping für Seiten, die öffentlich, aber nicht maschinenlesbar sind.

Schritt 2: Sind die Daten im rohen HTML-Code enthalten, den Sie von curl oder requests.get? Öffne die Seite in einem Browser, zeige den Quellcode an (nicht „Element untersuchen“) und suche nach einem der gewünschten Werte. Wenn er vorhanden ist, hast du es mit einer statischen Seite zu tun. Requests plus Beautiful Soup reichen aus. Greife nicht auf einen headless Browser zurück.

Schritt 3: Wird der Inhalt erst nach dem Laden des ursprünglichen HTML-Codes per JavaScript eingefügt? Dann benötigst du einen echten Browserkontext. Playwright ist heutzutage der Standard. Selenium ist in Ordnung, wenn dein Team es bereits in der CI einsetzt. Beide können auf Selektoren warten, klicken, scrollen und Daten aus dem gerenderten DOM extrahieren.

Schritt 4: Crawlen Sie Tausende von URLs, verketten Sie Anfragen oder führen Sie lang andauernde Jobs aus? Wechseln Sie zu Scrapy. Seine Ereignisschleife, Item-Pipelines, Parallelitätssteuerung und integrierte Wiederholungsversuche sind jeder selbst entwickelten Schleife überlegen.

Für besonders schwierige Ziele gibt es einen fünften Ansatz. Wenn die Website Browser aggressiv identifiziert, IP-Adressen von Rechenzentren blockiert oder bereits beim ersten Aufruf CAPTCHAs anzeigt, behalte deinen Parsing-Code bei, aber delegiere die Abrufebene an eine Scraping-API. Das ist ein Fallback, kein Standard.

Nutzen Sie diese Kurzanleitung:

Signal auf der Ziel-Website

Bestes Tool

Öffentliche API verfügbar

Die API

Daten im ursprünglichen HTML-Code

Anfragen + Beautiful Soup

Mit JavaScript gerenderte Inhalte

Playwright (oder Selenium)

Tausende von Seiten, Wiederholungsversuche, Pipelines

Scrapy

CAPTCHAs, IP-Sperren, TLS-Fingerprinting

Scraping-API

Die meisten Python-Web-Scraping-Projekte sollten mit dem leichtesten Tool beginnen, das die Daten abruft, und erst dann auf eine höhere Ebene wechseln, wenn die aktuelle Ebene an ihre Grenzen stößt. Dies entspricht den Empfehlungen der Community: Die beste Konfiguration ist die einfachste, die die Daten zuverlässig liefert.

Einrichtung der Umgebung: Python 3, Virtualenv und erforderliche Bibliotheken

Verwenden Sie Python 3.10 oder neuer. Erstellen Sie pro Projekt eine isolierte virtualenv-Umgebung, damit keine Abhängigkeiten in Ihr System-Python gelangen:

python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate

pip install --upgrade pip
pip install requests beautifulsoup4 lxml pandas
pip install playwright
pip install scrapy
pip install selenium webdriver-manager

# Playwright ships as a Python package plus browser binaries.
# Install the Chromium binary once:
python -m playwright install chromium

lxml dient gleichzeitig als schnellster Beautiful-Soup-Parser und als die XPath-Engine, die wir später verwenden. pandas ist optional, aber nützlich für die Bereinigung und den Export. webdriver-manager übernimmt den Download des ChromeDrivers von Selenium, sodass Sie die Browser-Versionen nicht auf jedem CI-Runner manuell abgleichen müssen.

Ein paar praktische Hinweise, bevor Sie loslegen:

  • Wenn Sie Apple Silicon verwenden, nutzen Sie den ARM64-Python-Build. Die Chromium-Binärdatei von Playwright läuft auf nativem ARM deutlich schneller als unter Rosetta.
  • Unter Windows aktivieren Sie die venv mit .venv\Scripts\activate und bevorzugen Sie PowerShell gegenüber cmd, um lesbare Stack-Traces zu erhalten.
  • Fixieren Sie die Versionen, sobald Ihr Scraper funktioniert: pip freeze > requirements.txt. Scraper sind von Natur aus anfällig, und das Festlegen eines bekanntermaßen funktionierenden Abhängigkeitssatzes ist der einfachste Weg, um zu verhindern, dass der morgige Cron-Lauf durch ein stillschweigendes Bibliotheks-Update unterbrochen wird.
  • Wenn Sie eine Containerisierung planen, fügen Sie den playwright install Schritt in Ihr Dockerfile ein, damit die Browser-Binärdateien im Image enthalten sind und nicht bei jedem Kaltstart neu heruntergeladen werden müssen.

Überprüfen Sie die Zielseite, bevor Sie auch nur eine einzige Zeile Code schreiben

Jede Stunde, die du in den DevTools verbringst, spart dir zehn Stunden Debugging. Öffne die Seite, die du scrapen möchtest, in Chrome oder Firefox und arbeite dich dann durch drei Panels, bevor du deinen Editor öffnest.

Das Fenster „Elemente“. Klicken Sie mit der rechten Maustaste auf einen Wert, der für Sie von Interesse ist, und wählen Sie „Untersuchen“. Notieren Sie sich das Tag, die Klasse und alle stabilen Attribute wie data-testid oder itemprop. IDs und Klassen, die automatisch generiert zu sein scheinen (css-1x9k2j) sind unbeständig und führen beim nächsten Deployment dazu, dass Ihre Selektoren nicht mehr funktionieren. Bevorzugen Sie semantische Attribute, sofern diese vorhanden sind.

Der Reiter „Netzwerk“. Lade die Seite neu, während der Reiter „Netzwerk“ geöffnet ist, und filtere nach XHR oder Fetch. Viele moderne Websites rendern Listen, indem sie einen internen JSON-Endpunkt aufrufen. Wenn du einen findest, der die benötigten Felder zurückgibt, ist es schneller, zuverlässiger und weniger fehleranfällig, diesen Endpunkt direkt mit „Requests“ anzusteuern, als HTML zu parsen. Das ist der größte Vorteil, den die meisten Scraping-Tutorials außer Acht lassen.

Als cURL kopieren. Klicke mit der rechten Maustaste auf eine beliebige Anfrage im Reiter „Netzwerk“ und wähle „Als cURL kopieren“. Füge den Code in dein Terminal ein, um zu überprüfen, ob er funktioniert, und wandle ihn anschließend in Python um. Das Tool curlconverter.com erledigt dies zwar automatisch, aber wenn du die Header manuell durchliest, lernst du, was der Server tatsächlich erwartet: ein User-Agent, ein Referer, ein Session-Cookie oder ein CSRF-Token.

Wenn das ursprüngliche HTML Ihre Daten enthält, befinden Sie sich auf dem statischen Weg. Wenn Ihre Daten erst nach dem Laden der Seite und der Ausführung von JavaScript erscheinen, befinden Sie sich auf dem Browser-Weg. Falls eine versteckte JSON-API existiert, sollten Sie diese immer bevorzugen. Der Rest dieses Leitfadens geht davon aus, dass Sie diesen Aufruf bereits vor dem Schreiben des Codes durchgeführt haben.

Erstellen eines statischen Scrapers mit Requests und Beautiful Soup

Bei einer statischen Seite reichen drei Schritte aus: Abrufen, Parsen, Extrahieren. Wir verwenden books.toscrape.com, ein öffentliches Demo-Ziel, das speziell dafür eingerichtet wurde, damit Tutorials keine echten Websites überlasten.

import requests
from bs4 import BeautifulSoup

URL = "https://books.toscrape.com/catalogue/page-1.html"
HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; MyScraper/1.0)"}

resp = requests.get(URL, headers=HEADERS, timeout=15)
resp.raise_for_status()          # raises on 4xx / 5xx

Ein resp.status_code200 bedeutet, dass das Abrufen erfolgreich war. Alles andere ist ein Signal, keine Fußnote. raise_for_status() wandelt dies in eine Ausnahme um, die Sie abfangen können. Legen Sie bei jeder Anfrage explizit timeout für jede Anfrage fest. Die Standardeinstellung ist „unbegrenzt warten“, was genau das ist, was Sie bei einem geplanten Job nicht wollen.

Analysieren Sie das HTML mit lxml, was deutlich schneller ist als Pythons integriertes html.parser:

soup = BeautifulSoup(resp.text, "lxml")

„Now select“. Beautiful Soup bietet dir drei Möglichkeiten, den Baum abzufragen. Die beiden, die du täglich verwenden wirst, sind find_all für tagbasierte Abfragen und select für CSS-Selektoren. Bevorzugen Sie select , wenn das Ziel über stabile Klassen verfügt:

books = []
for card in soup.select("article.product_pod"):
    title = card.select_one("h3 a")["title"]
    price = card.select_one("p.price_color").get_text(strip=True)
    stock = card.select_one("p.instock").get_text(strip=True)
    books.append({"title": title, "price": price, "stock": stock})

print(books[:3])

Ein paar Gewohnheiten werden dir später viel Arbeit ersparen:

  • Schützen Sie jeden Selektor. select_one gibt None , wenn keine Übereinstimmung vorliegt, sodass .get_text() es bei der nächsten Iteration zu einem Absturz kommt. Verwenden Sie if card.select_one(...) oder den Walrus-Operator, um fehlende Zeilen zu überspringen.
  • Extrahieren Sie Text mit strip=True. HTML enthält viele Leerzeichen und geschützte Leerzeichen. Eine Bereinigung beim Extrahieren ist effizienter, als später jedes nachgelagerte Feld zu korrigieren.
  • Speichern Sie Rohwerte, keine „endgültigen“ Werte. Speichern Sie die Preiszeichenfolge als "£51.77" und normalisieren Sie ihn in einem separaten Schritt. Wenn Ihre Extraktion fehlschlägt, möchten Sie sehen, was der Server tatsächlich zurückgegeben hat.

Um der Paginierung zu folgen, suche nach dem „Weiter“-Link und wiederhole den Vorgang, bis dieser nicht mehr vorhanden ist:

def scrape_all_pages(start_url):
    url = start_url
    results = []
    while url:
        r = requests.get(url, headers=HEADERS, timeout=15)
        r.raise_for_status()
        s = BeautifulSoup(r.text, "lxml")
        for card in s.select("article.product_pod"):
            results.append({
                "title": card.select_one("h3 a")["title"],
                "price": card.select_one("p.price_color").get_text(strip=True),
            })
        next_link = s.select_one("li.next a")
        url = requests.compat.urljoin(url, next_link["href"]) if next_link else None
    return results

Das ist ein vollständiger, ehrlicher statischer Python-Web-Scraper: mit einem echten Header abrufen, mit lxml, wähle mit CSS aus, achte auf fehlende Knoten, folge dem Link zur nächsten Seite und beende den Vorgang sauber, wenn die Paginierung zu Ende ist. Eine ausführlichere Anleitung zu Beautiful Soup mit Tabellen, Formularen und verschachtelten Elementen findest du in unserem begleitenden Beautiful-Soup-Tutorial.

Zwei weitere Gewohnheiten lohnt es sich, gleich vom ersten Tag an anzunehmen. Erstens: Hüllen Sie die Anfrage in ein try/except requests.RequestException und protokolliere die URL, bei der ein Fehler aufgetreten ist. Netzwerkfehler sind bei großem Umfang vorprogrammiert, und ein Scraper, der schon beim ersten ConnectionError , wird einen Crawl über Nacht niemals abschließen. Zweitens: Speichere deine Ergebnisse alle paar hundert Datensätze als Checkpoint auf der Festplatte, anstatt alles im Arbeitsspeicher zu halten:

import json

def checkpoint(records, path="checkpoint.jsonl"):
    with open(path, "a", encoding="utf-8") as f:
        for r in records:
            f.write(json.dumps(r, ensure_ascii=False) + "\n")

Wenn der Prozess nun auf Seite 47 von 100 abstürzt, hast du die ersten 46 Seiten auf der Festplatte und kannst den Vorgang fortsetzen, ohne die Daten erneut abrufen zu müssen. Diese beiden Vorgehensweisen – Fehlerbehandlung und Zwischenspeicherung – unterscheiden Demo-Scraper von solchen, die man tatsächlich laufen lassen kann.

CSS-Selektoren vs. XPath: Entscheiden Sie sich für eine Abfragesprache und bleiben Sie dabei

Beautiful Soup bietet dir CSS-Selektoren. lxml bietet Ihnen beides, aber sein Hauptvorteil ist XPath. Beide lösen dasselbe Problem mit unterschiedlichen Fehlermodi, und die bewusste Wahl einer der beiden ist wichtiger als die Wahl der „besten“ Variante.

CSS-Selektoren sind kürzer, vertrauter und lesen sich wie Frontend-Code. Sie eignen sich hervorragend für die klassenbasierte Auswahl: article.product_pod > h3 a. Sie haben jedoch Schwierigkeiten mit allem, was das Durchlaufen des DOMs, das Abgleichen von Textinhalten oder die Navigation relativ zu einem Geschwisterelement erfordert.

XPath ist ausdrucksstärker. Es unterstützt die Achsen-Navigation (ancestor::, following-sibling::), Textabgleich (//a[text()="Next"]) und die positionsbezogene Auswahl ((//tr)[3]). Die gleiche Extraktion mit lxml und XPath sieht so aus:

from lxml import html

tree = html.fromstring(resp.text)
titles = tree.xpath("//article[contains(@class,'product_pod')]//h3/a/@title")
prices = tree.xpath("//article[contains(@class,'product_pod')]//p[@class='price_color']/text()")

Die Frage der Wartbarkeit ist wichtiger als die der Ästhetik. Selektoren, die sich auf Klassennamen stützen, versagen an dem Tag, an dem der Designer der Website eine Klasse umbenennt. Selektoren, die sich auf die Struktur stützen (div > div > span:nth-child(2)) funktionieren nicht mehr, sobald jemand einen Wrapper hinzufügt. XPath-Ausdrücke, die auf contains(@class, ...) oder auf stabilen Attributen basieren, wie [@itemprop="price"] , überstehen beides. In der Praxis schreiben Teams, die sich für XPath entscheiden, tendenziell robustere Scraper – allerdings auf Kosten einer etwas steileren Lernkurve.

Wählen Sie pro Projekt eine Abfragesprache und bleiben Sie dabei. Ein Code, der soup.select(...) und tree.xpath(...) mischt, zwingt jeden neuen Entwickler dazu, beide Sprachen zu erlernen. Unser XPath-Leitfaden und unser Vergleich zwischen XPath und CSS-Selektoren behandeln die Navigationsmuster, die den Wechsel zu XPath lohnenswert machen, wenn sich das Markup häufig ändert.

Ein pragmatischer Standard für Python-Web-Scraping-Teams: Beginnen Sie jeden neuen Spider mit Beautiful Soup und CSS-Selektoren, da diese sich so lesen wie das Frontend, das Sie scrapen. Greifen Sie auf XPath zurück, sobald Sie auf einen Fall stoßen, in dem Sie den Baum hinaufsteigen, nach Textinhalten filtern oder anhand eines Index navigieren müssen. Damit lassen sich etwa 90 % der Aufgaben mit dem einfacheren Werkzeug bewältigen, und das ausdrucksstärkere Werkzeug bleibt für die Fälle reserviert, in denen es sich tatsächlich auszahlt.

Bereinigen und normalisieren Sie die extrahierten Felder

Rohe, gescrapte Zeichenfolgen sind fast nie verwendbar. Preise enthalten Währungssymbole, Datumsangaben in einem halben Dutzend Formaten, überall Leerzeichen und gelegentlich \u00a0 Schreibschutz-Leerzeichen, das sich in ansonsten sauberem Text versteckt. Trenne die Extraktion von der Normalisierung, damit beide Schritte weiterhin debuggbar bleiben.

import re
from datetime import datetime

def to_float_price(raw: str) -> float | None:
    if not raw:
        return None
    cleaned = re.sub(r"[^\d.,]", "", raw).replace(",", "")
    try:
        return float(cleaned)
    except ValueError:
        return None

def to_iso_date(raw: str, fmt: str) -> str | None:
    try:
        return datetime.strptime(raw.strip(), fmt).date().isoformat()
    except (ValueError, AttributeError):
        return None

def clean_text(raw: str) -> str:
    return " ".join(raw.replace("\xa0", " ").split()) if raw else ""

Wenden Sie diese Hilfsfunktionen in einem einzigen Durchlauf auf Ihre Rohdatensätze an:

def normalize(record):
    return {
        "title": clean_text(record["title"]),
        "price": to_float_price(record["price"]),
        "in_stock": "in stock" in clean_text(record["stock"]).lower(),
    }

normalized = [normalize(r) for r in raw_records]

Entdoppeln Sie die Daten vor dem Speichern. Ein stabiler Primärschlüssel auf Basis der URL oder der Produkt-ID verhindert, dass Ihr täglicher Cron-Job stillschweigend die Anzahl der Zeilen vervielfacht. Wenn Sie in pandas exportieren, df.drop_duplicates(subset=["url"]) ist das in einer Zeile erledigt.

Zwei Fallstricke bei der Kodierung sollten Sie kennen. Erstens: requests erraten die Kodierung der Antwort anhand der Header und können sich irren; legen Sie resp.encoding = "utf-8" es explizit fest, wenn du Zeichenchaos siehst. Zweitens geben manche Websites Zeichen zurück, die mit HTML-Entitäten maskiert sind, wie ’ selbst in modernen Browsern. Beautiful Soup dekodiert diese beim Parsen, aber wenn du JSON direkt abrufst, verwende html.unescape() vor dem Speichern verwenden. Eine konsistente Normalisierung ist das, was einen Demo-Scraper von einem Datensatz unterscheidet, den man tatsächlich abfragen kann.

JavaScript-gerenderte Seiten mit Playwright scrapen

Statische Tools versagen in dem Moment, in dem eine Website ihre Inhalte im Browser rendert. Moderne React-, Vue- und Svelte-Anwendungen geben oft eine fast leere HTML-Hülle zurück und füllen das DOM anschließend mit abgerufenem JSON. Beautiful Soup sieht nur die Hülle, nicht die Daten. Sie benötigen einen echten Browserkontext, und Playwright ist der sauberste Weg, diesen in Python zu erhalten.

Einmal installieren (damit wird auch die Chromium-Binärdatei mitgeliefert):

pip install playwright
python -m playwright install chromium

Ein vollständiger, lauffähiger Scraper für eine JavaScript-gerenderte Seite:

from playwright.sync_api import sync_playwright

def scrape_js_page(url: str):
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(
            user_agent="Mozilla/5.0 (compatible; MyScraper/1.0)",
            viewport={"width": 1366, "height": 900},
        )
        page = context.new_page()
        page.goto(url, wait_until="domcontentloaded", timeout=30_000)

        # Wait for the element that only appears after JS runs.
        page.wait_for_selector("article.product_pod", timeout=15_000)

        cards = page.query_selector_all("article.product_pod")
        results = []
        for card in cards:
            title = card.query_selector("h3 a").get_attribute("title")
            price = card.query_selector("p.price_color").inner_text().strip()
            results.append({"title": title, "price": price})

        browser.close()
        return results

Zwei Details entscheiden darüber, ob dieser Code zuverlässig oder unbeständig ist.

Wartestrategie. wait_until="domcontentloaded" wird ausgelöst, sobald das ursprüngliche HTML geparst ist. "networkidle" wartet, bis die Netzwerkaktivität für 500 ms unterbrochen ist – das ist strenger, aber langsamer. Bei Single-Page-Anwendungen solltest du wait_for_selector auf ein konkretes Element, das Sie tatsächlich benötigen. Dies ist schneller als networkidle und weniger anfällig als eine feste time.sleep.

Umgang mit dynamischem Laden. Verwenden Sie für „Infinite Scroll“ page.mouse.wheel(0, 2000) in einer Schleife, bis sich die Datensatzanzahl nicht mehr ändert. Bei Inhalten, die erst nach einem Klick angezeigt werden, page.click("button.load-more") und wählen Sie es erneut aus. Bei Seiten mit Login-Beschränkung führen Sie die Anmeldung einmal mit context.storage_state(path="auth.json") und verwende die Statusdatei für alle Durchläufe wieder, damit du dich nicht bei jedem Auftrag erneut authentifizieren musst.

Playwright unterstützt außerdem Asynchronität, das Abfangen von Anfragen (Blockieren von Bildern und Schriftarten zur Beschleunigung) sowie Multi-Browser-Ziele. Bei Crawls mit hohem Datenaufkommen wird es häufig mit Scrapy kombiniert, und zwar über scrapy-playwright. Unser Playwright-Leitfaden behandelt diese Muster ausführlich.

Bei Seiten mit unendlichem Bildlauf ist es üblich, so lange zu scrollen, bis die Datensatzanzahl nicht mehr zunimmt:

def scroll_until_stable(page, selector, max_rounds=20):
    prev = 0
    for _ in range(max_rounds):
        page.mouse.wheel(0, 4000)
        page.wait_for_timeout(1000)
        count = len(page.query_selector_all(selector))
        if count == prev:
            break
        prev = count
    return prev

Um den Browser-Overhead weiter zu reduzieren, blockieren Sie ressourcenintensive Elemente, die Sie nicht benötigen:

context.route("**/*.{png,jpg,jpeg,gif,svg,woff2,mp4}", lambda route: route.abort())

Diese eine Zeile verkürzt die Ladezeit auf medienintensiven Websites oft um 40 % oder mehr, da du Downloads überspringst, die du ohnehin verwerfen würdest. In Kombination mit wait_for_selectorist Playwright bei Seiten, die eine JavaScript-Rendering erfordern, in puncto Geschwindigkeit wirklich konkurrenzfähig zu statischen Scrapern. Wenn Sie Playwright in großem Maßstab einsetzen, starten Sie pro Worker einen persistenten Browserkontext anstelle eines neuen Browsers pro URL und verwenden Sie dieselbe Seite für ähnliche Anfragen wieder. Der Kaltstart von Chromium dauert etwa eine Sekunde pro Seite, und dieser Overhead dominiert jeden realen Crawl.

Selenium als Alternative für JavaScript-Seiten

Selenium ist etwa ein Jahrzehnt älter als Playwright, und viele Teams setzen es nach wie vor in der CI ein, da ihre bestehende Testinfrastruktur bereits mit WebDriver kompatibel ist. Für das Scraping ist es eine durchaus sinnvolle Wahl, wenn Sie zu diesen Teams gehören.

Ein minimaler Selenium-Scraper mit webdriver-manager , damit Sie den ChromeDriver nicht manuell herunterladen müssen:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager

opts = Options()
opts.add_argument("--headless=new")
opts.add_argument("--window-size=1366,900")

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=opts)
driver.get("https://books.toscrape.com/")

WebDriverWait(driver, 15).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "article.product_pod"))
)

results = []
for card in driver.find_elements(By.CSS_SELECTOR, "article.product_pod"):
    title = card.find_element(By.CSS_SELECTOR, "h3 a").get_attribute("title")
    price = card.find_element(By.CSS_SELECTOR, "p.price_color").text.strip()
    results.append({"title": title, "price": price})

driver.quit()

Selenium ist auf einen WebDriver angewiesen, der die Brücke zwischen Ihrem Python-Skript und dem eigentlichen Browserprozess bildet. Chrome verwendet ChromeDriver, Firefox verwendet GeckoDriver, Edge verwendet EdgeDriver, und jeder muss mit der Hauptversion des Browsers übereinstimmen.

Im Vergleich zu Playwright hat Selenium einen langsameren Start, eine etwas ausführlichere API und keine integrierte Abfangfunktion für Anfragen. Seine Stärken liegen in der ausgereiften Ökosystem-Struktur, dem erstklassigen Support bei den meisten CI-Anbietern und der Tatsache, dass Ihre Testingenieure bereits damit vertraut sind. Wenn Sie bei Null anfangen, ist Playwright einfacher. Wenn Sie eine bestehende Testsuite erweitern, ist Selenium in Ordnung. Unser Selenium-Leitfaden behandelt Skalierungsmuster und Techniken zur Umgehung von Cloudflare, falls der Standardablauf blockiert wird.

Ein Selenium-spezifisches Detail, das Sie sich verinnerlichen sollten: Bevorzugen Sie immer WebDriverWait mit expliziten Bedingungen (presence_of_element_located, element_to_be_clickable) gegenüber time.sleep. Explizite Wartezeiten enden, sobald die Bedingung erfüllt ist; „sleep“-Anweisungen verschwenden Zeit auf schnellen Seiten und schlagen auf langsamen Seiten dennoch fehl.

Skalierung mit Scrapy: Spider, Pipelines und Exporte

Scrapy ist nicht nur eine weitere Parsing-Bibliothek. Es ist ein vollständiges Crawling-Framework: eine asynchrone Engine, ein Middleware-Stack für Header und Proxys, Item-Pipelines zur Bereinigung und Persistenz sowie integrierte Exportfunktionen für JSON, JSONL und CSV. Greifen Sie darauf zurück, wenn Ihr Auftrag etwa tausend URLs umfasst, Wiederholungsversuche erfordert oder mehrere Ausgabestufen aufweist.

Ein Projekt aufsetzen:

scrapy startproject bookstore
cd bookstore
scrapy genspider books books.toscrape.com

Bearbeiten bookstore/spiders/books.py:

import scrapy

class BooksSpider(scrapy.Spider):
    name = "books"
    start_urls = ["https://books.toscrape.com/catalogue/page-1.html"]
    custom_settings = {
        "DOWNLOAD_DELAY": 0.5,
        "CONCURRENT_REQUESTS": 8,
        "USER_AGENT": "Mozilla/5.0 (compatible; MyScraper/1.0)",
        "RETRY_TIMES": 3,
    }

    def parse(self, response):
        for card in response.css("article.product_pod"):
            yield {
                "title": card.css("h3 a::attr(title)").get(),
                "price": card.css("p.price_color::text").get(),
                "stock": card.css("p.instock::text").re_first(r"\S.*"),
            }
        next_page = response.css("li.next a::attr(href)").get()
        if next_page:
            yield response.follow(next_page, self.parse)

Ausführen und exportieren:

scrapy crawl books -O books.jsonl

Dieser einzelne Befehl crawlt jede Seite, folgt dem Link zur nächsten Seite, wendet Ihre Verzögerungs- und Wiederholungs-Einstellungen an und streamt die Ergebnisse in JSONL. Das -O Flag überschreibt die Datei; -o hängt den Inhalt an.

Zwei Scrapy-Funktionen sollten Sie sich frühzeitig aneignen:

Item-Pipelines leiten jedes gecrawlte Element durch eine Kette von Python-Klassen. Nutze sie zur Normalisierung, Deduplizierung und Persistenz. Ein PriceCleanerPipeline könnte Währungssymbole entfernen; eine SQLitePipeline könnte Daten in eine Datenbank einfügen. Verknüpfen Sie Pipelines in settings.py unter ITEM_PIPELINES.

Middlewares greifen in jede Anfrage oder Antwort ein. Hier gehören Proxy-Rotation, Header-Rotation und die Cloudflare-Verarbeitung hin. Community-Middlewares wie scrapy-rotating-proxies und scrapy-user-agents lassen sich mit nur zwei Zeilen Konfiguration integrieren.

Wenn Sie auf JavaScript-lastige Seiten stoßen, geben Sie Scrapy nicht auf. Fügen Sie scrapy-playwright und kennzeichnen Sie bestimmte Anfragen mit meta={"playwright": True}. So behalten Sie die Parallelität, Pipelines und Exporte von Scrapy bei und zahlen die Browserkosten nur für die Seiten, die diese tatsächlich benötigen. Unser Scrapy-Playbook und der Leitfaden zur Integration von scrapy-playwright behandeln dieses Hybridmuster.

Eine minimale Pipeline, die Preise normalisiert und Duplikate entfernt, sieht so aus:

# bookstore/pipelines.py
import re

class BookstorePipeline:
    def __init__(self):
        self.seen = set()

    def process_item(self, item, spider):
        title = item.get("title")
        if title in self.seen:
            raise DropItem(f"duplicate: {title}")
        self.seen.add(title)
        raw_price = item.get("price") or ""
        item["price"] = float(re.sub(r"[^\d.]", "", raw_price) or 0)
        return item

Binde sie ein settings.py mit ITEM_PIPELINES = {"bookstore.pipelines.BookstorePipeline": 300}. Die Ganzzahl gibt die Priorität an; niedrigere Werte werden zuerst ausgeführt. Fügen Sie weitere Klassen für Validierung, Speicherung und Slack-Benachrichtigungen hinzu, und die gesamte Nachbearbeitungskette wird deklarativ.

Wann man Scrapy überspringen sollte. Für ein einmaliges Crawlen einer einzelnen Seite ist Scrapys Projektgerüst überdimensioniert. Bei wiederkehrenden Aufgaben oder bei mehr als ein paar hundert URLs machen sich die Standardwerte für Parallelität und Wiederholungsversuche sofort bezahlt. Eine nützliche Faustregel: Wenn du feststellst, dass du deine eigene asynchrone Schleife, deinen eigenen Thread-Pool oder einen eigenen Wiederholungs-Dekorator rund um Requests schreibst, hast du genug von Scrapy neu erfunden, sodass du es einfach nutzen solltest.

Verwende eine Web-Scraping-API für Anti-Bot-Ziele

Manche Websites werden sich allem widersetzen, was du bisher gelesen hast. Sie erkennen deinen TLS-Handshake, blockieren IPs von Rechenzentren, zeigen beim ersten Kontakt CAPTCHAs an oder senden einen 200-OK-Status mit einem Body, der besagt: „Bitte aktiviere JavaScript.“ Playwright kann einige dieser Hindernisse überwinden; Residential-Proxys können noch mehr bewältigen. Wenn die Kombination dennoch fehlschlägt, ist eine gehostete Scraping-API der pragmatische Weg.

Eine Scraping-API nimmt eine URL entgegen und gibt HTML zurück. Sie übernimmt die Bearbeitung der Anforderungsschicht: Browser-Rendering, Proxy-Rotation, Wiederholungsversuche und Umgehung von Anti-Bot-Maßnahmen. Sie behalten Ihren bestehenden Beautiful-Soup- oder lxml-Parsing-Code bei und tauschen lediglich den Fetch-Aufruf aus. Das bedeutet, Sie müssen keine Proxy-Pools verwalten, Browser-Fingerabdrücke auf dem neuesten Stand halten oder debuggen, warum der CAPTCHA-Löser einer Region heute langsamer ist.

Ein typischer Aufruf sieht so aus:

import requests

API_ENDPOINT = "https://api.webscrapingapi.com/v2"
params = {
    "api_key": "YOUR_KEY",
    "url": "https://example.com/hard-target",
    "render_js": "true",
    "proxy_type": "residential",
    "country": "us",
}
resp = requests.get(API_ENDPOINT, params=params, timeout=60)
resp.raise_for_status()
html = resp.text

# Parse with the same code you would use on a static page.
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "lxml")

Die Parameter sind entscheidend. render_js=true führt die Ziel-URL serverseitig über einen Headless-Browser aus. proxy_type=residential Leitet den Datenverkehr über IP-Adressen weiter, die wie privater Internetverkehr aussehen. country=us ermittelt den Standort der Anfrage, wenn das Ziel je nach Region unterschiedliche Inhalte bereitstellt.

Zwei wichtige Einschränkungen sollten erwähnt werden. Erstens zahlen Sie pro Anfrage, sodass die Kosten für Scraping-APIs linear mit dem Volumen steigen; wenn Ihre Seite statisch ist und nichts blockiert, sind Requests plus Beautiful Soup um ein Vielfaches günstiger. Zweitens veröffentlichen einige Anbieter sehr hohe Zahlen zum Proxy-Pool als Marketingangaben; betrachten Sie diese konkreten Zahlen als Richtwerte, bis Sie sie in der aktuellen Dokumentation des Anbieters bestätigen können. Die richtige Entscheidung lautet nicht „API als Standard“, sondern „API, wenn die Abrufebene das Problem ist“.

Eine nützliche Faustregel: Wenn Sie mehr als einen Tag pro Woche damit verbringen, Blockierungen statt Parsing-Fehler zu beheben, ist die Abrufebene nicht mehr Ihr Problem, sondern das eines anderen. Lagern Sie sie an eine Scraping-API aus und gewinnen Sie die Entwicklungszeit für die Extraktions- und Modellierungsarbeit zurück, die nur Ihr Team leisten kann.

Behandlung von HTTP-Fehlern, Wiederholungsversuchen und Timeouts

Ein Scraper ohne Wiederholungsrichtlinie ist ein Scraper, der bereits in seiner ersten Produktionsnacht versagen wird. HTTP-Antworten sagen dir genau, was zu tun ist – wenn du darauf hörst.

Status

Bedeutung

Richtige Reaktion

200

Erfolg

Analysieren und weitermachen

301 / 302

Weiterleiten

Folgen (Anfragen tun dies standardmäßig)

403

Zugriff verweigert

IP oder User-Agent wechseln, auf Anti-Bot prüfen

404

Nicht gefunden

Überspringen und protokollieren; die Ressource ist nicht mehr vorhanden oder die URL ist falsch

429

Zu viele Anfragen

Ehrerbietung Retry-After, zurückziehen, langsamer machen

5xx

Serverfehler

Mit exponentiellem Backoff erneut versuchen, dann aufgeben

Anfragen plus urllib3 erhalten Sie in wenigen Zeilen eine geeignete Wiederholungsstrategie:

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

session = requests.Session()
retry = Retry(
    total=5,
    backoff_factor=1.5,           # 1.5s, 3s, 6s, 12s, 24s
    status_forcelist=[429, 500, 502, 503, 504],
    allowed_methods=["GET", "HEAD"],
    respect_retry_after_header=True,
)
session.mount("https://", HTTPAdapter(max_retries=retry))
session.mount("http://", HTTPAdapter(max_retries=retry))

resp = session.get(url, timeout=(5, 30))    # (connect, read)

Lege bei jeder Anfrage explizite timeout Werte bei jeder Anfrage fest. Ein Tupel aus (connect, read) Sekunden ist sicherer als eine einzelne Zahl und verhindert den klassischen Fehler, bei dem „ein langsamer Ursprungsserver den gesamten Crawl blockiert“.

In Playwright gelten Timeouts pro Aktion: page.goto(url, timeout=30_000) und page.wait_for_selector(sel, timeout=15_000). Wiederholungsversuche müssen explizit erfolgen, da Playwright nicht automatisch erneut versucht. Schließen Sie Navigationen in eine eigene Schleife ein oder verwenden Sie tenacity für einen deklarativen Dekorator:

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(4), wait=wait_exponential(multiplier=1.5, min=2, max=30))
def goto_with_retry(page, url):
    page.goto(url, wait_until="domcontentloaded", timeout=30_000)

Protokollieren Sie bei jedem Wiederholungsversuch den Statuscode, die URL und die Versuchsnummer. Stille Wiederholungsversuche sind der Grund, warum Scraper wochenlang „funktionieren“, während sie veraltete Daten zurückgeben. Noch eine Empfehlung: Begrenze das gesamte Wiederholungsbudget pro URL, nicht nur die Anzahl der Versuche. Eine Anfrage, die über vier Wiederholungsversuche hinweg 90 Sekunden in Anspruch nimmt, ist schlimmer als eine, die nach 15 Sekunden schnell fehlschlägt und weitergeht, da der langsame Fehler alle anderen URLs blockiert, die sich diesen Worker teilen.

Blockierungen vermeiden: Proxys, Header und Ratenbegrenzungen

Die meisten Sperrungen lassen sich auf drei Anzeichen zurückführen: Ihre Header sehen nicht wie die eines Browsers aus, Ihre Anfragen erfolgen zu schnell und Ihre IP-Adresse steht auf einer Sperrliste des Rechenzentrums. Beheben Sie alle drei Probleme.

Wechseln Sie regelmäßig zwischen einem Pool echter User-Agents und senden Sie die Header, die ein echter Browser sendet. Websites erkennen Accept, Accept-Languageund Accept-Encoding ebenfalls, nicht nur User-Agent.

import random, time

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ...",
    # ...more real UAs
]

def browser_headers():
    return {
        "User-Agent": random.choice(USER_AGENTS),
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
        "Accept-Encoding": "gzip, deflate, br",
    }

Fügen Sie Ihrer Anfragesequenz Jitter hinzu. Ein festes time.sleep(1) Rhythmus ist leichter zu erkennen als zufällige, menschliche Zeitabstände.

time.sleep(random.uniform(1.2, 3.5))

Wechseln Sie die Proxys, idealerweise private. IPs aus Rechenzentren werden massenhaft blockiert, da sie leicht zu erkennen sind. Private Proxys leiten den Datenverkehr über echte Benutzergeräte weiter und fügen sich unauffällig in den Datenverkehr ein.

PROXIES = [
    "http://user:pass@proxy1.example.com:8000",
    "http://user:pass@proxy2.example.com:8000",
    # ...
]

def get(url):
    proxy = random.choice(PROXIES)
    return requests.get(
        url,
        headers=browser_headers(),
        proxies={"http": proxy, "https": proxy},
        timeout=(5, 30),
    )

Erkennen Sie eine Blockierung schnell. Eine Antwort mit dem Status 200 und einem Textkörper, der „Just a moment...“, „Attention Required“ oder „cf-chl-bypass“ enthält, ist eine Cloudflare-Herausforderung, kein echter Inhalt.

def looks_blocked(resp):
    body = resp.text[:5000].lower()
    return any(k in body for k in [
        "just a moment", "attention required", "cf-chl-bypass",
        "captcha", "access denied",
    ])

Bei looks_blocked ein Problem auftritt, wechseln Sie den Proxy, reduzieren Sie die Anfragefrequenz drastisch und erwägen Sie, die Anfrage auf eine Scraping-API umzustellen. Unsere Leitfäden zur Proxy-Rotation und zum „No-Block“-Ansatz gehen näher auf Muster zur Wiederherstellung nach IP-Sperren ein.

Stellen Sie den Betrieb nach IP-Sperren wieder her, ohne den Prozess neu zu starten. Halten Sie die funktionierenden Proxys in einem collections.deque. Wenn einer einen 403-Fehler oder ein CAPTCHA erhält, rotieren Sie ihn nach hinten und lassen Sie ihn eine Abkühlphase durchlaufen, bevor er wieder nach vorne kommt. Wenn der gesamte Pool „kalt“ ist, warten Sie die Sperre ab, anstatt den Prozess abzubrechen:

from collections import deque
pool = deque(PROXIES)

def next_proxy():
    proxy = pool.popleft()
    pool.append(proxy)
    return proxy

Das reicht für die schwierigsten Ziele zwar nicht aus, sorgt aber dafür, dass ein gut programmierter Scraper monatelang ohne manuelle Eingriffe auf den meisten Websites läuft. Wenn dies nicht mehr ausreicht, ist das das Signal, die Abrufebene gegen eine gehostete API auszutauschen, anstatt Ihren Proxy-Pool weiter zu vergrößern.

Gescrapte Daten speichern: CSV, JSON, SQLite und LLM-fähige Formate

Die Wahl des Speicherformats hängt davon ab, wie die Daten genutzt werden sollen. Wählen Sie bewusst aus; eine spätere Konvertierung ist mühsam.

CSV für Tabellenkalkulationen und schnelle Weitergaben:

import csv
with open("books.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["title", "price", "in_stock"])
    writer.writeheader()
    writer.writerows(records)

JSON für verschachtelte Datensätze und API-konforme Ausgabe:

import json
with open("books.json", "w", encoding="utf-8") as f:
    json.dump(records, f, ensure_ascii=False, indent=2)

SQLite für wiederholte Durchläufe mit Deduplizierung und Abfragen. Es besteht aus einer einzigen Datei, benötigt keine Server und ist im Lieferumfang von Python enthalten:

import sqlite3
conn = sqlite3.connect("books.db")
conn.execute(
    "CREATE TABLE IF NOT EXISTS books ("
    " url TEXT PRIMARY KEY,"
    " title TEXT, price REAL, scraped_at TEXT)"
)
conn.executemany(
    "INSERT OR REPLACE INTO books VALUES (?, ?, ?, ?)",
    [(r["url"], r["title"], r["price"], r["scraped_at"]) for r in records],
)
conn.commit()

Parquet für Analyse-Workloads, die in DuckDB, Spark oder ein Data Warehouse gelangen: pandas.DataFrame(records).to_parquet("books.parquet"). Spaltenorientiert und typisiert, sodass nachgelagerte Abfragen schnell sind.

LLM-fähige JSONL-Ausgabe

Wenn Ihre gescrapten Daten in ein LLM oder eine Embedding-Pipeline eingespeist werden sollen, liefern Sie sie als JSONL: ein JSON-Objekt pro Zeile, ein Datensatz pro Dokument. Jeder Datensatz sollte eine stabile ID, eine Quell-URL, einen Zeitstempel und ein text Feld enthalten, das das Modell in Chunks aufteilen kann.

def to_llm_record(item):
    return {
        "id": item["url"],
        "source_url": item["url"],
        "scraped_at": item["scraped_at"],
        "title": item["title"],
        "text": f"{item['title']}\n\nPrice: {item['price']}\n\n{item.get('description', '')}",
        "metadata": {"category": item.get("category"), "in_stock": item["in_stock"]},
    }

with open("books.jsonl", "w", encoding="utf-8") as f:
    for r in records:
        f.write(json.dumps(to_llm_record(r), ensure_ascii=False) + "\n")

Halten Sie die Chunks unter etwa 2000 Tokens, bewahren Sie die Quell-URLs für Zitate auf und führen Sie Datensätze aus verschiedenen Quellen niemals zusammen. Diese Metadaten pro Datensatz sorgen dafür, dass eine nachgelagerte RAG-Pipeline korrekt bleibt.

Einlesen der Ergebnisse in pandas oder ein Data Warehouse. Für Analyse-Workflows laden Sie Ihre JSONL- oder SQLite-Daten direkt in einen DataFrame und verarbeiten Sie sie im Arbeitsspeicher weiter:

import pandas as pd
df = pd.read_json("books.jsonl", lines=True)
df["price"] = df["price"].astype(float)
df.to_parquet("books.parquet")

Bei größeren Datenmengen duckdb.read_json("books.jsonl") bietet Ihnen SQL-Zugriff auf dieselbe Datei, ohne diese in den Arbeitsspeicher zu laden. Beide Ansätze halten Ihre Extraktionsphase vollständig von Ihrer Analysephase entkoppelt – genau das, was Sie wollen, wenn sich der Scraper ändert, die Analyse aber gleich bleibt.

Wiederkehrende Scraper planen und überwachen

Ein manuell ausgeführter Scraper ist ein Hobby. Ein nach Zeitplan ausgeführter Scraper ist Dateninfrastruktur und erfordert dieselbe Disziplin wie jeder andere Job.

Cron ist unter Linux die schnellste Methode zur Planung. Bearbeite die Datei crontab -e:

undefined0 6 * * * /path/to/.venv/bin/python /path/to/scraper.py >> /var/log/scraper.log 2>&1

Das läuft täglich um 06:00 Uhr und erfasst stdout und stderr. Wenn Ihr Job länger als ein paar Minuten dauert oder Abhängigkeiten hat, sollten Sie einen systemd-Timer bevorzugen, der Ihnen eine korrekte Start-/Stopp-Semantik, Neustart-Richtlinien und die Integration mit journalctl. Für die prozessinterne Planung während der Entwicklung ist die schedule Bibliothek ist schlank und übersichtlich:

import schedule, time
schedule.every().day.at("06:00").do(run_job)
while True:
    schedule.run_pending()
    time.sleep(30)

Protokolliere strukturiert. Protokolle im Klartext sind nicht durchsuchbar; JSON-Protokolle lassen sich ohne Nachbearbeitung an Loki, Datadog oder eine SQLite-Tabelle weiterleiten:

import logging, json
logging.basicConfig(level=logging.INFO, format="%(message)s")

def log_event(event, **fields):
    logging.info(json.dumps({"event": event, **fields}))

log_event("page_scraped", url=url, status=resp.status_code, items=len(items))

Erstellen Sie Warnmeldungen für die wichtigen Metriken: wiederholte 403-Fehler, ein plötzlicher Rückgang der Elementanzahl (ein häufiges Anzeichen dafür, dass die Website ihr Markup geändert hat) und eine Gesamtlaufzeit, die einen Schwellenwert überschreitet. Ein stiller Scraper, der null Zeilen zurückgibt, ist schlimmer als einer, der lautstark abstürzt, da Sie dies möglicherweise wochenlang nicht bemerken. Richte eine Überprüfung der „erwarteten Mindestanzahl an Zeilen“ ein und benachrichtige dich selbst, wenn die Anzahl darunter fällt:

MIN_EXPECTED = 400
if len(items) < MIN_EXPECTED:
    log_event("row_count_alert", got=len(items), expected=MIN_EXPECTED)
    raise SystemExit(2)     # non-zero exit code trips your cron alerter

Bei lang andauernden Python-Web-Scraping-Jobs sollten Sie außerdem alle N Seiten einen Heartbeat senden, damit ein externer Watchdog (Healthchecks.io, Uptime Kuma oder ein einfacher Cron-Sentinel) einen hängenden Prozess von einem lediglich langsamen unterscheiden kann. Stillstand ist der Ausfallmodus, gegen den Sie bei der Entwicklung vorgehen müssen.

Fehlerbehebung bei defekten Scrapern: Eine wiederholbare Checkliste

Scraper fallen aus einer kleinen Anzahl von Gründen aus. Gehen Sie diese Checkliste der Reihe nach durch; die Lösung liegt fast immer in den ersten drei Schritten.

  1. Überprüfen Sie die Länge des rohen HTML-Codes. print(len(resp.text)). Ein Body-Bereich von weniger als einigen Kilobyte deutet in der Regel auf eine Challenge-Seite, eine Ratenbegrenzung oder eine leere Hülle vor der Ausführung von JavaScript hin. Wenn die Länge im Vergleich zu einer funktionierenden Basislinie gesunken ist, liegt das Problem bei der Abrufebene, nicht beim Parser.
  2. Speichere die gerenderte Seite auf der Festplatte und öffne sie in einem Browser. Path("debug.html").write_bytes(resp.content). In neun von zehn Fällen zeigt sich sofort, ob du echten Inhalt, eine Anmeldeseite oder die Meldung „Bitte aktiviere JavaScript“ erhalten hast.
  3. Vergleichen Sie die Selektoren mit einem bekanntermaßen fehlerfreien Snapshot. Führen Sie ein kleines tests/fixtures/ Verzeichnis mit echten Seitenbeispielen aus der Zeit, als der Scraper noch funktionierte. Wenn etwas nicht mehr funktioniert, diff vergleiche das aktuelle HTML mit dem Referenz-Snapshot und suche nach umbenannten Klassen, neuen Wrappern oder entfernten Elementen.
  4. Prüfe auf Lazy Loading. Wenn die Anzahl der Elemente gesunken ist, die Seite im Browser aber normal aussieht, ist die Website wahrscheinlich auf virtualisierte Listen oder unendliches Scrollen umgestiegen. Öffne erneut die Registerkarte „Netzwerk“ und suche nach einem JSON-Endpunkt, den das Frontend nun nach dem Mount aufruft.
  5. Suchen Sie nach einer versteckten JSON-API. Frontends werden im Laufe der Zeit auf APIs umgestaltet. Was früher Playwright erforderte, wird oft zu einem einzigen fetch() Aufruf an /api/products?page=2. Dieser Endpunkt ist stabil, schnell und fast immer die richtige Antwort, sobald er existiert.
  6. Überprüfen Sie die HTTP-Semantik. Erhalten Sie 200 , aber leeres JSON? Eine 403 nur zeitweise? Falsche Cookies? Fügen Sie Protokollierungen zu Statuscodes, Antwort-Headern und Cookies hinzu, bevor Sie davon ausgehen, dass der Parser schuld ist.

Rechtliche und ethische Leitlinien für das Scraping mit Python

Web-Scraping ist in vielen Rechtsordnungen legal, wenn es auf öffentliche Daten angewendet wird, aber „legal“ ist nicht dasselbe wie „verantwortungsbewusst“. Achten Sie auf beides.

Beachten Sie robots.txt. Das Robots Exclusion Protocol ist mittlerweile ein offizieller Internetstandard, der als IETF RFC 9309 veröffentlicht wurde, und obwohl es selbst kein Gesetz ist, wird es in vielen Nutzungsbedingungen durch Verweis einbezogen. Analysieren Sie es mit Pythons integrierter urllib.robotparser und überspringen Sie unzulässige Pfade:

from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url("https://example.com/robots.txt")
rp.read()
if not rp.can_fetch("MyScraper/1.0", target_url):
    return

Lies die Nutzungsbedingungen der Zielseite. Einige Websites verbieten die automatisierte Datenerfassung ausdrücklich; das Ignorieren dieser Bestimmungen kann einen Anspruch wegen Vertragsbruchs begründen, selbst wenn kein Gesetz verletzt wurde. Achte besonders auf Websites, die eine Anmeldung erfordern, da das Überschreiten einer Authentifizierungsgrenze die rechtliche Lage erheblich verändert.

Sammeln Sie keine personenbezogenen Daten leichtfertig. Wenn die Daten Angaben enthalten, die eine Person identifizieren (Name, E-Mail-Adresse, Anschrift, IP-Adresse, an einen Nutzer gebundene Cookies), fallen Sie wahrscheinlich in den Geltungsbereich von Gesetzen wie der DSGVO, dem britischen Datenschutzgesetz oder dem CCPA. Konsultieren Sie einen Rechtsbeistand, bevor Sie eine Pipeline erstellen, die diese Daten speichert.

Bevorzugen Sie offizielle APIs, sofern vorhanden, umgehen Sie keine Paywalls oder Authentifizierungsmechanismen und legen Sie selbst eine Zugriffsbegrenzung fest, auch wenn das Ziel Sie nicht dazu zwingt. Ein langsamer, „höflicher“ Scraper, der nachhaltig skaliert, ist mehr wert als ein schneller, der dazu führt, dass Ihr gesamtes Unternehmen mit einer IP-Sperre belegt wird. Unser Rahmenwerk zur Einhaltung gesetzlicher Vorschriften geht detailliert auf die Situation in den einzelnen Ländern ein.

Legen Sie schließlich eine klare, ehrliche User-Agent , die Ihr Projekt identifiziert und eine Kontaktadresse enthält (MyProject/1.0 (+https://example.com/contact)). Das ist eine kleine Höflichkeit, die Website-Betreibern einen Ansprechpartner per E-Mail bietet, bevor sie auf ihre WAF zurückgreifen, und in der Praxis verringert dies die Wahrscheinlichkeit einer IP-Sperre erheblich.

Drei Einstiegsprojekte zum Üben

Der beste Weg, sich mit einem Python-Web-Scraping-Stack vertraut zu machen, besteht darin, denselben Kern-Scraper für drei verschiedene Arten von Zielseiten zu entwickeln. Jede dieser Aufgaben ist klein genug, um sie an einem Nachmittag zu bewältigen, und realistisch genug, um dir die Muster zu vermitteln, die du im großen Maßstab anwenden wirst.

Preis-Tracker. Wählen Sie eine Demo-E-Commerce-Seite aus (books.toscrape.com ist sicher und stabil). Scrapen Sie Produktname, Preis und Lagerbestand einmal täglich in SQLite, speichern Sie jeden Snapshot und stellen Sie mit pandas eine Preisserie pro Produkt grafisch dar. Dadurch lernst du idempotente Läufe, Deduplizierung nach URL und die Speicherung von Zeitreihen kennen. Sobald deine Test-Website ein Feld „Rabatt“ hinzufügt, kannst du auch Schema-Drift üben. Unser Leitfaden zu Produktdaten enthält ähnliche Muster für echte E-Commerce-Ziele.

Aggregator für Stellenanzeigen. Rufe Stellenanzeigen von einer öffentlichen Jobbörse (oder einer Demo wie realpython.github.io/fake-jobs/). Normalisieren Sie Titel, Standorte und Veröffentlichungsdaten und führen Sie anschließend eine deduplizierende Abgleichung über alle Seiten hinweg durch. Dieses Projekt belohnt eine saubere Normalisierung und vermittelt Ihnen, warum Extraktion und Bereinigung getrennte Schritte sein müssen. Unser Leitfaden zu Jobdaten behandelt Skalierungsmuster für den Fall, dass Sie von einer Jobbörse auf fünf umsteigen.

Dashboard für Schlagzeilen. Extrahieren Sie Schlagzeilen und Veröffentlichungszeitstempel aus einem öffentlichen RSS-Feed oder einem Nachrichtenaggregator. Speichern Sie die Daten als JSONL mit title, url, published_at, source. Dieses Projekt bereitet Sie auf nachfolgende LLM-Arbeiten vor: Das JSONL-Format lässt sich direkt in eine Embedding-Pipeline einbinden, und die Metadaten pro Datensatz (Quelle, Zeitstempel) unterstützen RAG mit umfangreichen Zitaten. Unser Leitfaden zu Nachrichtendaten behandelt die Änderungsüberwachung auf Basis desselben Basis-Scrapers.

Dieselbe dreistufige Pipeline (Abrufen, Parsen, Extrahieren), drei verschiedene Datenformate. So verinnerlicht man das Muster.

Checkliste für die Produktion, bevor Sie einen Scraper in Betrieb nehmen

Bevor du deinen Scraper vom lokalen Skript zum Produktionsjob hochstufst, gehe diese Liste durch. Fehlende Punkte sind der Grund, warum aus „Gestern hat es noch funktioniert“ ein Notruf um 3 Uhr morgens wird.

Wichtige Erkenntnisse

  • Beginnen Sie mit dem einfachsten Tool. „Requests“ plus „Beautiful Soup“ bewältigen die meisten statischen Seiten; greifen Sie erst auf „Playwright“ zurück, wenn JavaScript die Daten rendert, und auf „Scrapy“ nur, wenn Sie Skalierbarkeit, Pipelines und Wiederholungsversuche benötigen.
  • Überprüfen Sie das Ziel in den DevTools, bevor Sie Code schreiben. Versteckte JSON-Endpunkte sind fast immer zuverlässiger als das Scraping von HTML.
  • Trennen Sie die Extraktion von der Normalisierung. Speichern Sie Rohwerte, bereinigen Sie diese in einem zweiten Durchgang und entfernen Sie Duplikate anhand eines stabilen Schlüssels.
  • Blockierungen sind ein Problem der Abrufebene. Wechsle User-Agents, füge Jitter hinzu, verwende Residential-Proxys und erkenne Challenge-Seiten explizit.
  • Stellen Sie Scraper wie echte Jobs bereit: strukturierte Protokolle, Wiederholungsversuche, Timeouts, Warnungen bei Schema-Drift und eine Produktions-Checkliste. Ein stiller Fehler ist der Fehlermodus, gegen den Sie bei der Entwicklung vorgehen müssen.

FAQ

Inwiefern unterscheidet sich Web-Scraping von der Nutzung einer offiziellen API?

Eine offizielle API ist ein unterstützter, versionierter Vertrag mit Ratenbeschränkungen, Authentifizierung und stabilen Feldnamen. Beim Scraping wird eine für Menschen konzipierte Seite gelesen und die Struktur aus HTML abgeleitet, das sich ohne Vorankündigung ändern kann. APIs sind schneller, kostengünstiger und rechtlich sicherer, sofern sie vorhanden sind. Führen Sie Scraping nur durch, wenn keine API Ihre Daten abdeckt oder wenn die Nutzungsbedingungen der API Ihren Anwendungsfall ausschließen.

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

Es gibt keine allgemeingültige Zahl, aber ein vertretbarer Ausgangspunkt ist 1 Anfrage alle 2 bis 5 Sekunden pro Domain mit variierendem Timing. Wenn die Website eine Ratenbegrenzung in robots.txt oder in der Dokumentation eine Ratenbegrenzung veröffentlicht, halten Sie sich daran. Achten Sie auf HTTP-429-Antworten und reduzieren Sie die Abfragefrequenz exponentiell. Ein schnelleres Crawling ist nur mit ausdrücklicher Genehmigung der Website oder über einen gehosteten Dienst möglich, der die Auslastung bereits mit der Ziel-Website abstimmt.

Wie stelle ich sicher, dass mein Python-Scraper weiterhin funktioniert, wenn die Zielwebsite ihren HTML-Code ändert?

Testen Sie Selektoren in der CI anhand einer festgelegten HTML-Vorlage, überwachen Sie bei jedem Durchlauf die Füllraten pro Feld und lösen Sie einen Alarm aus, wenn diese sinken. Bevorzugen Sie Selektoren, die auf semantischen Attributen basieren (data-testid, itemprop) gegenüber automatisch generierten Klassennamen. Halten Sie Extraktion und Normalisierung voneinander getrennt, damit ein fehlerhafter Selektor nur ein Feld und nicht den gesamten Job zum Scheitern bringt. Versionsverwalten Sie Ihre Parsing-Logik, damit ein Rollback mit einem einzigen Commit möglich ist.

Wann sollte ich von einem selbst gehosteten Python-Scraper zu einer verwalteten Scraping-API wechseln?

Wechseln Sie, wenn die Abrufebene – und nicht der Parser – die Ursache für Fehler ist. Anzeichen hierfür sind wiederholte 403-Fehler trotz Proxy-Rotation, CAPTCHAs beim ersten Zugriff, Blockierungen durch TLS-Fingerabdrücke sowie Wartungsaufwand für die Infrastruktur, der den Zeitaufwand für die Datenlogik übersteigt. Behalten Sie Ihren bestehenden Parsing-Code bei; nur die Request-Ebene muss ersetzt werden. Wenn das Ziel statisch und ungeschützt ist, bleibt das Selbsthosting deutlich kostengünstiger.

Wie sollte ich gescrapte Daten speichern, wenn ich sie später in ein LLM einspeisen möchte?

Verwenden Sie JSONL mit einem Datensatz pro Zeile. Jeder Datensatz benötigt einen stabilen id, ein source_url, einen scraped_at Zeitstempel, ein Klartext- text Feld für die Einbettung sowie ein metadata Objekt für Filter. Halten Sie einzelne text Chunks auf etwa 2000 Token, damit nachgeschaltete Chunker sie nicht willkürlich aufteilen müssen. Führen Sie Datensätze aus verschiedenen Quellen niemals zusammen und bewahren Sie URLs, damit eine RAG-Pipeline sie zitieren kann.

Fazit

Beim Python-Webscraping im Jahr 2026 geht es weniger darum, eine weitere Bibliothek zu erlernen, sondern vielmehr darum, für jedes Ziel die richtige auszuwählen und sie dann in die Infrastruktur einzubinden, die ihren Betrieb sicherstellt. Ein erster Durchlauf mit „Requests“ und „Beautiful Soup“ deckt die meisten statischen Seiten ab. Playwright bewältigt JavaScript. Scrapy ermöglicht die Skalierung auf Tausende von URLs. XPath, Proxy-Rotation, strukturierte Protokollierung und Warnmeldungen bei Schema-Drift machen aus einem Skript einen Job, den man tatsächlich laufen lassen kann. Und wenn die Abrufebene eines Ziels wirklich feindselig ist, ist eine Scraping-API eher das pragmatische Notausgangstor als die Standardlösung.

Die wichtigste Gewohnheit ist, klein anzufangen. Starten Sie keinen Headless-Browser, um eine Seite zu scrapen, die Sie mit lxml. Richten Sie keinen Proxy-Pool ein, bevor Sie einen funktionierenden Scraper geschrieben haben. Stellen Sie zuerst die einfache Version bereit, fügen Sie weitere Ebenen erst hinzu, wenn die aktuelle an ihre Grenzen stößt, und instrumentieren Sie alles so, dass Sie Änderungen an der Website bemerken, noch bevor Ihr Dashboard darauf hinweist.

Wenn du auf ein Ziel stößt, das Browser identifiziert, IPs von Rechenzentren blockiert oder beim ersten Kontakt CAPTCHAs anzeigt, übernimmt unser Team bei WebScrapingAPI die Anforderungsschicht (Proxy-Rotation, JS-Rendering, Wiederholungsversuche), sodass du deinen Python-Parsing-Code beibehalten und das Wettrüsten gegen Bots überspringen kannst. Probieren Sie es mit Ihrer schwierigsten URL aus und sehen Sie, wie viel von der Pipeline in Ihrer Hand bleibt.

Über den Autor

Raluca Penciuc, Full-Stack-Entwickler @ WebScrapingAPI

Raluca Penciuc

Full-Stack-Entwickler

Raluca Penciuc ist Full-Stack-Entwicklerin bei WebScrapingAPI. Sie entwickelt Scraper, verbessert Umgehungsstrategien und findet zuverlässige Wege, um die Erkennung auf Zielwebsites zu verringern.

Web-Scraping mit AWS Lambda: Leitfaden für Python und Java 2026
Anleitungen

Web-Scraping mit AWS Lambda: Leitfaden für Python und Java 2026

Kurz gesagt: Web-Scraping mit AWS Lambda funktioniert am besten, wenn jeder Aufruf kurz, begrenzt und unabhängig wiederholbar ist. Beginnen Sie mit direktem HTTP, AWS SAM und S3 und fügen Sie erst dann SQS, Container, Browser-Rendering, Proxys oder eine verwaltete Abrufebene hinzu, wenn sich im Rahmen der Arbeitslast herausstellt, dass diese erforderlich sind.

Suciu Dan26 min read
Artikel lesen
So verwenden Sie GoSpider: Crawlen, URLs bereinigen und Daten extrahieren
Anleitungen

So verwenden Sie GoSpider: Crawlen, URLs bereinigen und Daten extrahieren

TL;DR: GoSpider ist ein Befehlszeilen-Crawler zum Auffinden von URLs, kein vollständiger Scraper für strukturierte Daten. Diese Anleitung zur Verwendung von GoSpider zeigt einen begrenzten Crawl, die saubere Verarbeitung der Ausgabe, die Übergabe von Colly an CSV sowie einen Diagnosepfad für 403-Antworten oder Seiten, die eine JavaScript-Darstellung erfordern.

Suciu Dan20 min read
Artikel lesen
Wie man Redfin scrappt: Python-Leitfaden für Immobiliendaten
Anleitungen

Wie man Redfin scrappt: Python-Leitfaden für Immobiliendaten

TL;DR: Redfin stellt versteckte API-Endpunkte zur Verfügung, die strukturiertes JSON für Immobilienangebote zurückgeben, wodurch das fragile HTML-Parsing vollständig übersprungen werden kann. Diese Anleitung führt Sie durch den Aufbau eines Python-Scrapers, der Miet- und Verkaufsdaten extrahiert, nach Standort sucht, neue Angebote über XML-Sitemaps überwacht und saubere Ergebnisse in CSV oder JSON exportiert.

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.