Zurück zum Blog
Anleitungen
Sorin-Gabriel MaricaLast updated on May 12, 202617 min read

BeautifulSoup Anleitung: Bauen Sie einen echten Python Scraper von Grund auf neu

BeautifulSoup Anleitung: Bauen Sie einen echten Python Scraper von Grund auf neu
Kurz gesagt: Dieses BeautifulSoup-Tutorial führt dich durch die Erstellung eines kompletten Python-Scrapers – pip install einem robusten Skript, das Hacker News paginiert, in CSV und JSON exportiert und dabei so höflich bleibt, dass es nicht blockiert wird. Jeder Codeausschnitt ist lauffähig, und wir weisen genau darauf hin, wann BeautifulSoup das falsche Werkzeug ist.

Wenn du eine for Schleife in Python schreiben und schon einmal auf eine Webseite gestarrt und gedacht haben: „Ich möchte diese Daten in einer Tabelle haben“, dann ist dieses BeautifulSoup-Tutorial genau das Richtige für dich. Beautiful Soup ist eine Python-Bibliothek zum Parsen von HTML und XML in einen Baum, den du mit vertrauten Methoden im jQuery-Stil abfragen kannst. Sie ruft keine Seiten ab, führt kein JavaScript aus und gibt nicht vor, ein Browser zu sein. Sie nimmt einfach rohen Markup-Code und bietet dir eine saubere API, um die Teile herauszuziehen, die dich interessieren.

Der Plan ist konkret. Wir richten eine neue Umgebung ein, rufen eine echte Listenseite mit der requests Bibliothek abrufen, diese mit BeautifulSoup parsen, Elemente sowohl mit find_all und CSS-Selektoren ansprechen, die Paginierung über mehrere Seiten hinweg verfolgen und die Ergebnisse in CSV und JSON schreiben. Dabei werden wir User-Agent-Rotation, Wiederholungsversuche und Ratenbegrenzung einbauen, denn ein Tutorial, das Anti-Bot-Abwehrmechanismen ignoriert, scheitert in dem Moment, in dem man es auf eine echte Website anwendet. Am Ende verfügen Sie über einen kopier- und einfügbaren, lauffähigen Scraper und ein klares Gespür dafür, wann Sie BeautifulSoup weiterhin nutzen sollten und wann es Zeit ist, zu einem leistungsstärkeren Tool überzugehen.

Was BeautifulSoup ist und wann man darauf zurückgreifen sollte

BeautifulSoup (das bs4 Paket auf PyPI, derzeit in der 4.x-Reihe) ist eine Parsing-Bibliothek, kein Crawler und kein Browser. Man übergibt ihm eine HTML-Zeichenkette und es gibt einen Parse-Baum zurück, den man nach Tags, Attributen, CSS-Selektoren oder Beziehungen durchsuchen kann. Das ist die gesamte Aufgabe. Alles, was mit HTTP-Anfragen, Cookies, Sitzungen, der Ausführung von JavaScript oder Warteschlangen zu tun hat, ist das Problem eines anderen, und genau diese Trennung ist der Grund, warum BeautifulSoup auch mehr als ein Jahrzehnt nach seiner ersten Veröffentlichung immer noch die erste Wahl für statische Seiten ist.

Es hilft, es in einen Zusammenhang zu stellen. requests Außerdem ist BeautifulSoup die leichteste mögliche Lösung: Es eignet sich hervorragend, wenn die gewünschten Daten bereits im vom Server zurückgegebenen HTML enthalten sind und du nur eine Handvoll Seiten statt einer Million crawlen. Scrapy ist das richtige Tool, wenn du ein vollständiges Crawling-Framework mit Pipelines, Deduplizierung und Parallelität benötigst. Selenium und Playwright sind die richtigen Tools, wenn es sich bei der Seite um eine Single-Page-App handelt, die ihren Inhalt erst nach Ausführung von JavaScript zusammenstellt. Wenn du die URL mit curl abrufen und deine Daten im Antworttext sehen kannst, ist BeautifulSoup fast immer die einfachste Lösung.

Einrichtung der Umgebung: Python, Requests und BeautifulSoup4

Verwenden Sie eine virtuelle Umgebung, damit dieses Projekt Ihre globalen Site-Pakete nicht beeinträchtigt. Für dieses BeautifulSoup-Tutorial funktioniert alles ab Python 3.9 einwandfrei, und durch das Fixieren der Versionen bleiben die hier gezeigten Code-Schnipsel reproduzierbar.

python -m venv .venv
source .venv/bin/activate   # on Windows: .venv\Scripts\activate
pip install requests==2.32.3 beautifulsoup4==4.12.3 lxml==5.2.2

requests übernimmt die HTTP-Ebene, beautifulsoup4 ist die Parser-API selbst, und lxml ist ein optionaler, aber dringend empfohlener C-basierter Parser. BeautifulSoup greift auf die Standardbibliothek html.parser , wenn du lxml, aber der C-Parser ist bei großen Dokumenten deutlich schneller und toleranter gegenüber unordentlichem Markup. Wenn du Python-Umgebungen unterstützen musst, in denen das Kompilieren von C-Erweiterungen umständlich ist, lass lxml und Sie verlieren etwas an Geschwindigkeit, aber keine Funktionalität.

Schneller Test in einer Python-REPL:

import requests, bs4
print(requests.__version__, bs4.__version__)

Wenn beide Versionen fehlerfrei ausgegeben werden, sind Sie startklar. Speichern Sie den restlichen Code in einer Datei namens hn_scraper.py und führen Sie ihn mit python hn_scraper.py.

HTML mit Requests abrufen

BeautifulSoup benötigt Bytes zum Parsen. Die requests Bibliothek ist der ergonomischste Weg, diese zu erhalten. Wähle ein echtes Ziel, das du höflich ansprechen kannst: Hacker News ist die klassische Wahl, da die Startseite aus einfachem, vom Server gerendertem HTML mit vorhersehbarer Struktur und sehr leichtem Anti-Bot-Schutz besteht, was ideal zum Lernen ist.

import requests

URL = "https://news.ycombinator.com/news"
HEADERS = {
    "User-Agent": "Mozilla/5.0 (compatible; LearningScraper/1.0)",
    "Accept-Language": "en-US,en;q=0.9",
}

response = requests.get(URL, headers=HEADERS, timeout=15)
response.raise_for_status()        # blows up on 4xx/5xx
html_bytes = response.content      # bytes, not str

Zwei Dinge, bei denen es sich lohnt, inne zu halten. Erstens: Überprüfe immer den Statuscode. Ein stiller 403-Fehler, der eine „Zugriff verweigert“-Seite zurückgibt, wird sauber in ein BeautifulSoup-Objekt geparst, das keine der Daten enthält, die du eigentlich willst, und du wirst einen Nachmittag damit verschwenden, Selektoren an der falschen Seite zu debuggen. raise_for_status() macht diesen Fehler deutlich sichtbar.

Zweitens: Bevorzuge response.content statt response.text bevorzugen, wenn du BeautifulSoup fütterst. .text erzwingt eine Dekodierung unter Verwendung der Kodierung requests , die aus den Headern erraten wird, was manchmal falsch ist. .content gibt die rohen Bytes zurück, und BeautifulSoup ist viel besser darin, die tatsächliche Kodierung aus einem <meta charset> Tag oder dem Dokument selbst zu ermitteln. Der Unterschied spielt auf rein englischsprachigen Websites kaum eine Rolle, ist jedoch von großer Bedeutung, sobald Sie Inhalte mit Akzentzeichen scrapen.

Erstellen eines BeautifulSoup-Objekts und Auswählen eines Parsers

Erstellen Sie mit den Bytes den Parse-Baum, indem Sie diese an den BeautifulSoup Konstruktor zusammen mit einem Parser-Namen übergeben. Die offizielle Beautiful Soup-Dokumentation listet drei Parser auf, die man kennen sollte.

Parser

Geschwindigkeit

Toleranz gegenüber fehlerhaftem HTML

Anmerkungen

html.parser

Annehmbar

Gut

Standardbibliothek, keine Installation erforderlich.

lxml

Am schnellsten

Gut

C-Erweiterung; pip install lxml.

html5lib

Langsamste

Am besten

Reines Python; ahmt nach, wie Browser sich von fehlerhaftem Markup erholen.

Für dieses BeautifulSoup-Tutorial verwenden wir lxml , da es schnell ist und heutzutage überall verfügbar ist. Greifen Sie html5lib nur dann darauf zurück, wenn eine Website wirklich fehlerhaftes HTML enthält, das lxml verzerrt, und greifen Sie auf html.parser , wenn du nichts über die Standardbibliothek hinaus installieren kannst.

from bs4 import BeautifulSoup

soup = BeautifulSoup(html_bytes, "lxml")
print(soup.title.string)            # "Hacker News"
print(soup.prettify()[:300])        # peek at the formatted DOM

soup.title.string funktioniert, weil BeautifulSoup Tags der obersten Ebene als Attribute bereitstellt. get_text(strip=True) ist die sicherere Allzweckalternative, wenn du nicht weißt, ob ein Tag reinen Text oder verschachtelte untergeordnete Elemente enthält, und prettify() ist bei der Erkundung von unschätzbarem Wert, da es Ihnen den eingerückten Baum anzeigt, den Sie tatsächlich abfragen.

Elemente ansprechen: find, find_all und select

BeautifulSoup bietet Ihnen drei Möglichkeiten, Knoten zu finden: find, find_all, und select. find gibt die erste Übereinstimmung zurück (oder None). find_all gibt eine Liste aller Treffer zurück. select und select_one verwenden CSS-Selektor-Strings, die wir im nächsten Unterabschnitt behandeln werden.

Nach Tag suchen. Die einfachste Form. soup.find_all("a") gibt jeden Anker auf der Seite zurück.

links = soup.find_all("a")
print(len(links), "anchors found")

Suche nach Klasse. Verwenden Sie das Schlüsselwort class_ mit einem abschließenden Unterstrich, da class in Python ein reserviertes Wort ist. Das bringt fast jeden Anfänger ins Straucheln.

rows = soup.find_all("tr", class_="athing")          # Hacker News story rows
titles = soup.find_all("span", class_="titleline")

Suche nach ID. Übergebe id= direkt. IDs sollen eindeutig sein, daher find ist das in der Regel das, was du willst.

main = soup.find(id="hnmain")

Suche nach Attribut. Jedes beliebige Attribut kann in einem attrs dict übergeben werden. So kannst du data-* Attribute, aria-* oder alles andere, was kein Tag, keine ID oder keine Klasse ist.

rows = soup.find_all("tr", attrs={"data-row-type": "story"})

Filtern nach einer aufrufbaren Funktion. Wenn du Logik benötigst, die kein Schlüsselwort abdeckt, übergebe ein Lambda. Die Funktion erhält jedes Tag und gibt True , um es beizubehalten.

def is_external_link(tag):
    return tag.name == "a" and tag.get("href", "").startswith("http")

external = soup.find_all(is_external_link)

Sie können auch ein Lambda an das string Argument übergeben, um nach Textinhalt zu filtern. Die groß-/kleinschreibungsunabhängige Übereinstimmung von Teilzeichenfolgen ist ein häufiger Anwendungsfall:

python_links = soup.find_all("a", string=lambda s: s and "python" in s.lower())

Eine pragmatische Faustregel: Verwenden Sie find und find_all , wenn die Suche nur ein oder zwei Attribute tief ist. Sobald du eine Klasse, ein übergeordnetes Element und eine Position kombinieren musst, wechsle zu CSS-Selektoren. Diese sind leichter zu lesen und lassen sich einfacher aus den DevTools des Browsers kopieren.

CSS-Selektoren im Detail mit select() und select_one()

select() akzeptiert dieselben CSS-Selektor-Strings, die du in document.querySelectorAll. Das bedeutet, dass Nachkommen-Kombinatoren, Kind-Kombinatoren, Attributselektoren, Pseudoklassen und verkettete Klassennamen alle funktionieren.

# Descendant: any .titleline inside a tr.athing, at any depth
titles = soup.select("tr.athing .titleline")

# Direct child: only immediate children
direct = soup.select("tr.athing > td.title > span.titleline")

# Attribute selector: links to PDFs
pdfs = soup.select("a[href$='.pdf']")

# Positional: every fifth story row
every_fifth = soup.select("tr.athing:nth-of-type(5n)")

# Multiple classes at once
emphasized = soup.select("span.titleline.featured")

Hier ist die praktische Zuordnung zwischen den beiden APIs.

find_all form

select Formular

find_all("a", class_="storylink")

select("a.storylink")

find_all("div", id="main")

select("div#main")

find_all("input", attrs={"type": "hidden"})

select("input[type='hidden']")

Selektoren sind in diesem BeautifulSoup-Tutorial kein Nebenschauplatz, sondern die wichtigste Wartungsstrategie. Der Trick, der Scraper am Laufen hält, wenn sich das Markup ändert, besteht darin, Ihre Selektoren als benannte Konstanten am Anfang des Moduls zu definieren. Wenn die Website eine Klasse umbenennt, korrigieren Sie eine Zeile, anstatt den gesamten Code zu durchsuchen.

STORY_ROW = "tr.athing"
TITLE_LINK = "span.titleline > a"
RANK = "span.rank"

Machen Sie es sich zur Gewohnheit, einen funktionierenden Selektor aus den Chrome DevTools zu kopieren (Rechtsklick auf ein Element, Kopieren > Selektor kopieren) und dann die automatisch generierte Kette auf die kürzeste Version zu kürzen, die das Gewünschte noch eindeutig identifiziert. Lange Selektoren brechen als Erstes, wenn sich das Markup ändert; kurze, benannte Selektoren überstehen kleine Neugestaltungen.

Das DOM durchlaufen: Eltern, Geschwister und Kinder

Manchmal ist das Element, das du eindeutig identifizieren kannst, nicht das Element, das du tatsächlich willst. Ein häufiges Muster: Du kannst ein bestimmtes Element <span class="rank"> leicht ansprechen, aber der Titel und der Link befinden sich in einem Geschwisterknoten. Anstatt einen instabilen zusammengesetzten Selektor zu schreiben, durchlaufen Sie den Baum.

Jedes BeautifulSoup-Tag stellt Navigationsattribute bereit:

  • .parent: das unmittelbar umschließende Tag.
  • .parents: ein Generator, der alle Vorfahren bis zur Dokumentwurzel liefert.
  • .next_sibling und .previous_sibling: benachbarte Knoten auf derselben Ebene (kann Leerzeichen sein).
  • .find_next("tag") und .find_previous("tag"): überspringe Leerzeichen-Knoten und finde das nächste echte Tag.
  • .children und .descendants: direkte Kinder oder jeden verschachtelten Knoten.

Ein Beispiel zur Veranschaulichung. Angenommen, du hast alle .titleline Spans auf Hacker News erfasst haben und für jeden einzelnen die umgebende Zeile sowie die nächste Zeile (die die Punktzahl und den Autor enthält) abrufen möchten.

for title_span in soup.select("span.titleline"):
    row = title_span.find_parent("tr")               # the .athing row
    meta_row = row.find_next_sibling("tr")           # the subtext row
    score = meta_row.find("span", class_="score")
    print(title_span.get_text(strip=True), score.get_text() if score else "-")

Der eigentliche Kompromiss besteht zwischen Lesbarkeit und Robustheit. Ein verketteter CSS-Selektor ist kürzer, aber das Durchlaufen des Baums ist oft robuster, wenn die Seite dieselben Daten je nach Kontext in unterschiedliche Container verpackt. Greifen Sie auf die Traversierung zurück, wenn eine einzelne Abfrage die benötigte Beziehung nicht ausdrücken kann.

End-to-End-Projekt: Scraping von Rang, Titel und URL bei Hacker News

Es ist an der Zeit, keine isolierten Schnipsel mehr zu zeigen und den Kern des Scrapers zu erstellen. Die Hacker-News-Startseite rendert jeden Beitrag als tr.athing Zeile, wobei der Rang in span.rank, der Titel und der externe Link in span.titleline > aund eine gleichrangige Zeile die Punktzahl und den Autor enthält. Unsere Aufgabe ist es, jeden Artikel in ein Dictionary umzuwandeln.

Hier ist die erste Version des Parsers. Beachte, dass er keine Daten abruft; er akzeptiert eine HTML-Zeichenkette und gibt strukturierte Datensätze zurück. Die Trennung von Abruf und Analyse ermöglicht es dir, den Parser anhand von Fixture-HTML-Dateien zu testen, ohne das Netzwerk zu belasten.

from bs4 import BeautifulSoup

def parse_stories(html: bytes) -> list[dict]:
    soup = BeautifulSoup(html, "lxml")
    stories = []
    for row in soup.select("tr.athing"):
        rank_tag = row.select_one("span.rank")
        link_tag = row.select_one("span.titleline > a")
        if not (rank_tag and link_tag):
            continue                                # skip malformed rows
        stories.append({
            "rank": rank_tag.get_text(strip=True).rstrip("."),
            "title": link_tag.get_text(strip=True),
            "url": link_tag.get("href", ""),
            "id": row.get("id"),
        })
    return stories

Ein paar Details, die wichtiger sind, als es den Anschein hat. rank_tag.get_text(strip=True).rstrip(".") behandelt den abschließenden Punkt, den Hacker News nach jedem Rang anzeigt ("1." wird zu "1"). link_tag.get("href", "") gibt die leere Zeichenkette zurück, anstatt KeyError , wenn das Attribut fehlt – genau diese Art von Ein-Zeichen-Änderung macht aus einem anfälligen Scraper einen robusten. Und das frühe continue hält die Schleife am Laufen, wenn die Website gelegentlich eine Anzeigenzeile oder einen gesponserten Platzhalter einfügt, der nicht dem Schema entspricht.

Verbinden Sie den Parser mit dem Fetcher:

import requests

def fetch(url: str) -> bytes:
    headers = {"User-Agent": "LearningScraper/1.0"}
    response = requests.get(url, headers=headers, timeout=15)
    response.raise_for_status()
    return response.content

if __name__ == "__main__":
    stories = parse_stories(fetch("https://news.ycombinator.com/news"))
    for story in stories[:5]:
        print(story["rank"], story["title"])

Wenn Sie dies ausführen, sollten die ersten fünf rangierten Schlagzeilen so ausgegeben werden, wie sie derzeit auf der Seite erscheinen. Sie haben einen funktionierenden Ein-Seiten-Scraper in weniger als dreißig Zeilen. Die verbleibenden Abschnitte dieses BeautifulSoup-Tutorials fügen Paginierung, Exporte, Wiederholungsversuche und den Feinschliff hinzu, der das Skript dazu befähigt, eine Stunde lang statt nur eine Minute auf einer echten Website zu laufen.

Umgang mit Paginierung und mehrseitigen Crawls

Hacker News paginiert mit einem Abfrageparameter: ?p=2, ?p=3, und so weiter. Am Ende jeder Seite befindet sich ein <a class="morelink"> Anker, der auf die nächste Seite verweist. Das Erkennen dieses Ankers ist die sauberste Stoppbedingung, da sie funktioniert, unabhängig davon, ob die Website sequenzielle Seiten, Cursor-Token oder Offset-Parameter verwendet.

import time
from urllib.parse import urljoin

BASE = "https://news.ycombinator.com/"

def scrape_all(start_url: str, max_pages: int = 5, delay: float = 1.5) -> list[dict]:
    url = start_url
    pages_done = 0
    all_stories: list[dict] = []

    while url and pages_done < max_pages:
        html = fetch(url)
        all_stories.extend(parse_stories(html))

        soup = BeautifulSoup(html, "lxml")
        more = soup.select_one("a.morelink")
        url = urljoin(BASE, more["href"]) if more else None

        pages_done += 1
        time.sleep(delay)
    return all_stories

Drei Details, die es wert sind, erwähnt zu werden. urljoin(BASE, more["href"]) ist, wie man relative hrefs wie news?p=2 in eine echte absolute URL umwandelt, was requests erfordert. Das max_pages Cap ist ein Sicherheitsnetz, damit eine fehlerhafte Stoppbedingung nicht endlos laufen kann. Und time.sleep(delay) ist der kostengünstigste Rate-Limiter; wir werden ihn durch etwas Intelligenteres ersetzen, wenn wir zum Anti-Blocking kommen.

Dieses Paginierungsmuster lässt sich weit über Hacker News hinaus verallgemeinern. Überall dort, wo die nächste Seite ein echter Anker im Markup ist, kannst du einen anderen Selektor in select_one und der Rest der Schleife bleibt identisch. Bei Websites, die mit Infinite Scroll paginieren, hilft BeautifulSoup allein nicht weiter, und wir behandeln diese Einschränkung im JavaScript-Abschnitt später in diesem BeautifulSoup-Tutorial.

Exportieren von gescrapten Daten in CSV und JSON

Sobald Sie eine Liste von Dictionaries haben, ist das Speichern auf Festplatte reine Routine. Die beiden Formate, die jeder Analyst erwartet, sind CSV und JSON, und es gibt keinen Grund, nicht beide im selben Workflow zu erzeugen.

import csv, json
from pathlib import Path

def export(records: list[dict], out_dir: str = "out") -> None:
    out = Path(out_dir)
    out.mkdir(exist_ok=True)

    csv_path = out / "stories.csv"
    with csv_path.open("w", newline="", encoding="utf-8-sig") as f:
        writer = csv.DictWriter(f, fieldnames=["rank", "title", "url", "id"])
        writer.writeheader()
        writer.writerows(records)

    json_path = out / "stories.json"
    with json_path.open("w", encoding="utf-8") as f:
        json.dump(records, f, ensure_ascii=False, indent=2)

Ein paar Fallstricke bei der Kodierung verdienen eine eigene Erwähnung. Verwenden Sie encoding="utf-8-sig" für das CSV, wenn die Daten in Excel unter Windows geöffnet werden sollen, da die BOM Excel mitteilt, dass die Datei UTF-8 ist (ohne sie werden Zeichen mit Akzenten als Kauderwelsch dargestellt). Übergeben Sie newline="" an open beim Schreiben von CSV, um leere Zeilen unter Windows zu vermeiden. Bei JSON ensure_ascii=False behält Nicht-ASCII-Zeichen unverändert bei, anstatt sie \uXXXX Escape-Zeichen, was die Ausgabe für Menschen lesbar macht.

Für Analysten, die mit einem Notebook arbeiten, pandas.DataFrame(records).to_csv("stories.csv", index=False) ist die Einzeiler-Alternative. Sie ist aufwendiger, aber praktisch, wenn man ohnehin eine explorative Analyse derselben Daten durchführen will.

Häufige Fallstricke: fehlende Elemente, Kodierung und NoneType-Fehler

Der mit Abstand häufigste Fehler, auf den du in jedem BeautifulSoup-Tutorial-Code stoßen wirst, ist AttributeError: 'NoneType' object has no attribute 'get_text'. Das bedeutet immer find oder select_one zurückgegeben None, und dann haben Sie versucht, eine Methode darauf aufzurufen. Die Lösung besteht darin, vor dem Verketten immer zu prüfen.

# Brittle
title = row.find("span", class_="titleline").a.get_text()

# Defensive
line = row.find("span", class_="titleline")
anchor = line.find("a") if line else None
title = anchor.get_text(strip=True) if anchor else None

Zwei damit verbundene Gewohnheiten werden dir viel Zeit sparen:

  • Verwenden Sie .get(attr, default) anstelle von tag[attr]. Die Indizierung löst KeyError , wenn das Attribut fehlt, während .get stillschweigend Ihren Standardwert zurückgibt und die Schleife weiterlaufen lässt.
  • Verwenden Sie immer .get_text(strip=True) anstelle von .string. .string ist None immer dann, wenn ein Tag mehrere Kinder hat, was es überraschend anfällig macht.

Die Kodierung ist die zweite klassische Falle. Wenn du BeautifulSoup response.text und die Website in der Content-Type Header falsche Angaben zur Kodierung macht, erhalten Sie Mojibake. Wenn Sie ihm response.content (Bytes) ermöglicht es BeautifulSoup, die tatsächliche Kodierung aus dem Dokument zu ermitteln.

Schreibe deine Selektoren schließlich während der Entwicklung anhand einer gespeicherten HTML-Vorlage. Speichere das Rohmaterial response.content einmal und iterieren Sie lokal. Ihr Scraper lässt sich dann leicht unit-testen und Sie belasten die Zielseite nicht mehr jedes Mal, wenn Sie einen Selektor ändern.

Anti-Scraping-Abwehrmaßnahmen umgehen und dabei höflich bleiben

Selbst ein freundliches Ziel blockiert einen Scraper, der es mit Tausenden identischer Anfragen von einer IP-Adresse bombardiert. Höflichkeit ist teils eine technische Frage und teils das Richtige. Fünf Techniken decken das meiste ab, was du brauchst.

1. Wechsle die User-Agents. Ein echter Browser-Fingerabdruck plus ein kleiner Pool realistischer User-Agent-Strings reicht aus, damit einfache Filter dich ignorieren. Wähle einen pro Anfrage.

import random
UAS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) Safari/17.0",
    "Mozilla/5.0 (X11; Linux x86_64) Firefox/124.0",
]
headers = {"User-Agent": random.choice(UAS)}

2. Begrenze die Rate mit Jitter. Eine gleichmäßige time.sleep(1) ist ein Fingerabdruck für sich. Fügen Sie einen zufälligen Jitter hinzu, damit die Kadenz menschlich wirkt.

time.sleep(random.uniform(1.0, 2.5))

3. Wiederholen Sie die Anfrage mit exponentiellem Backoff. Vorübergehende Fehler (5xx, Verbindungsabbrüche, Timeouts) sind die Regel. Umgeben Sie Anfragen mit Backoff, damit eine kleine Störung den gesamten Ablauf nicht zum Scheitern bringt.

def fetch_with_retry(url, headers, attempts=4):
    for i in range(attempts):
        try:
            r = requests.get(url, headers=headers, timeout=15)
            if r.status_code == 200:
                return r.content
            if r.status_code in (429, 503):
                time.sleep(2 ** i)
                continue
            r.raise_for_status()
        except requests.RequestException:
            time.sleep(2 ** i)
    raise RuntimeError(f"giving up on {url}")

4. Proxys rotieren. Wenn Ihre private IP-Adresse nicht mehr ausreicht, leiten Sie Anfragen über einen Pool von privaten oder Rechenzentrums-Proxys weiter. requests akzeptiert ein proxies={"http": ..., "https": ...} Argument; die Rotationslogik befindet sich eine Ebene höher.

5. Lies robots.txt und die Nutzungsbedingungen. Googles Dokumentation zu robots.txt ist eine solide Einführung in das Protokoll. Die Einhaltung der Disallow Anweisungen ist nicht überall rechtlich bindend, aber es ist die Grenze zwischen einem höflichen und einem aufdringlichen Scraper, und wenn man sie ignoriert, landen Projekte auf Sperrlisten.

Wenn Websites auf ernstzunehmende Anti-Bot-Lösungen setzen (Cloudflares Bot Manager, PerimeterX, DataDome), übersteigen die Kosten für die Eigenentwicklung all dieser Komponenten die Kosten für die Nutzung eines verwalteten Unlockers. Unsere Scraper-API übernimmt Rotation, CAPTCHAs und Wiederholungsversuche hinter einem einzigen Endpunkt, sodass der BeautifulSoup-Parsing-Code in diesem Tutorial genau gleich bleibt und sich nur die Abrufebene ändert.

Wenn BeautifulSoup nicht ausreicht: JavaScript-gerenderte Seiten

BeautifulSoup parst das, was der Server gesendet hat. Wenn der Server eine fast leere HTML-Hülle gesendet hat und die Seite ihren Inhalt erst zusammenstellt, nachdem JavaScript im Browser ausgeführt wurde, parst BeautifulSoup die Hülle und findet nichts Brauchbares. Dies ist die größte Einschränkung dessen, was dieses BeautifulSoup-Tutorial für Sie leisten kann, und es lohnt sich, die Anzeichen dafür zu erkennen.

Verräterische Anzeichen dafür, dass Sie es mit einer Single-Page-App zu tun haben:

  • view-source: zeigt ein winziges <div id="root"></div> und eine Flut von <script> -Tags an, aber die gerenderte Seite im Browser ist voller Inhalt.
  • Ihr Scraper sieht ein anderes DOM als DevTools. DevTools zeigt das Live-DOM an, das auch durch JavaScript eingefügte Knoten enthält; requests sieht nur die ursprüngliche Antwort.
  • Der Netzwerk-Tab zeigt eine Flut von XHR oder fetch Aufrufe nach dem Laden der Seite.

Du hast drei gute Optionen:

  • Finden Sie die API. Beobachten Sie den Netzwerk-Tab. Wenn die Seite JSON von einem Backend abruft, rufen Sie diesen Endpunkt direkt mit requests und überspringe das Rendern komplett. Dies ist in der Regel der schnellste und stabilste Weg.
  • Steuere einen echten Browser an. Verwende Playwright oder Selenium, um die Seite zu laden, warte auf die Daten und übergebe dann das gerenderte HTML an BeautifulSoup zum Parsen.
  • Verwenden Sie eine verwaltete Browser-API. Für Fälle, in denen Sie den Browser nutzen möchten, ohne die Infrastruktur zu verwalten, gibt ein Cloud-Browser-Endpunkt den gerenderten HTML-Code zurück, und Sie können ihn weiterhin mit demselben find_all/select Code, den Sie bereits geschrieben haben.

Endgültiges Skript: Abrufen, Parsen, Paginierung und Export zusammenführen

Hier ist die konsolidierte Version des BeautifulSoup-Tutorial-Codes. Er paginiert, versucht es erneut, begrenzt die Rate mit Jitter, wechselt die User-Agents und exportiert sowohl CSV als auch JSON.

import csv, json, random, time
from pathlib import Path
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

BASE = "https://news.ycombinator.com/"
START = urljoin(BASE, "news")
UAS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) Safari/17.0",
]

def fetch(url, attempts=4):
    for i in range(attempts):
        try:
            r = requests.get(url, headers={"User-Agent": random.choice(UAS)}, timeout=15)
            if r.status_code == 200:
                return r.content
            if r.status_code in (429, 503):
                time.sleep(2 ** i); continue
            r.raise_for_status()
        except requests.RequestException:
            time.sleep(2 ** i)
    raise RuntimeError(f"failed: {url}")

def parse_stories(html):
    soup = BeautifulSoup(html, "lxml")
    out = []
    for row in soup.select("tr.athing"):
        rank = row.select_one("span.rank")
        link = row.select_one("span.titleline > a")
        if not (rank and link):
            continue
        out.append({
            "rank": rank.get_text(strip=True).rstrip("."),
            "title": link.get_text(strip=True),
            "url": link.get("href", ""),
            "id": row.get("id"),
        })
    return out

def next_page(html):
    soup = BeautifulSoup(html, "lxml")
    more = soup.select_one("a.morelink")
    return urljoin(BASE, more["href"]) if more else None

def crawl(start, max_pages=3):
    url, pages, rows = start, 0, []
    while url and pages < max_pages:
        html = fetch(url)
        rows.extend(parse_stories(html))
        url = next_page(html)
        pages += 1
        time.sleep(random.uniform(1.0, 2.5))
    return rows

def export(rows, out_dir="out"):
    out = Path(out_dir); out.mkdir(exist_ok=True)
    with (out / "stories.csv").open("w", newline="", encoding="utf-8-sig") as f:
        w = csv.DictWriter(f, fieldnames=["rank", "title", "url", "id"])
        w.writeheader(); w.writerows(rows)
    with (out / "stories.json").open("w", encoding="utf-8") as f:
        json.dump(rows, f, ensure_ascii=False, indent=2)

if __name__ == "__main__":
    rows = crawl(START)
    export(rows)
    print(f"saved {len(rows)} stories")

Fügen Sie das in hn_scraper.py, führen Sie python hn_scraper.py, und du solltest drei Seiten mit Artikeln sehen, die in out/stories.csv und out/stories.json.

Wie geht es mit diesem BeautifulSoup-Tutorial weiter?

Sie verfügen nun über einen vollständigen Scraper für statische Websites, doch derselbe Parser lässt sich auch in viel umfangreichere Workflows integrieren. Drei sinnvolle nächste Schritte:

  • Steigen Sie auf Scrapy um, wenn Sie Tausende von Seiten crawlen, URLs deduplizieren, Parallelität verwalten und geplante Jobs ausführen müssen. Scrapy verwendet ähnliche Selektor-Konstrukte, sodass sich das mentale Modell, das Sie in diesem BeautifulSoup-Tutorial aufgebaut haben, nahtlos übertragen lässt.
  • Fügen Sie einen Headless-Browser hinzu, wenn die Daten hinter JavaScript liegen. Sowohl Playwright als auch Selenium ermöglichen es Ihnen, die Seite zuerst zu rendern und anschließend den gerenderten HTML-Code mit BeautifulSoup zu parsen, wodurch Ihr bestehender Parsing-Code und Ihre CSS-Selektoren erhalten bleiben.
  • Lagern Sie die Abrufebene aus, wenn Blöcke zum Engpass werden. Eine verwaltete Scraping-API kümmert sich um Proxys, Header und das Lösen von CAPTCHAs, sodass Sie sich weiterhin auf Selektoren konzentrieren können, anstatt auf Fingerprinting.

Egal, welchen Weg du einschlägst, behalte die hier aufgebaute Trennung zwischen Parsing und Abruf bei. Dies ist die einzige Designentscheidung, die es einem Scraper ermöglicht, die unvermeidliche Neugestaltung der Website zu überstehen, und sie ist es, die den Code in diesem Leitfaden wiederverwendbar macht, wenn deine Anforderungen wachsen.

Wichtige Erkenntnisse

  • BeautifulSoup parst HTML, mehr nicht. Kombinieren Sie es mit requests für statische Seiten und mit einem echten Browser für JavaScript-gerenderte Seiten.
  • CSS-Selektoren lassen sich besser skalieren als verkettete find_all Aufrufe. Definieren Sie sie als benannte Konstanten am Anfang Ihres Moduls, damit eine Änderung am Markup mit einer einzigen Zeile behoben werden kann.
  • Schützen Sie sich immer vor None. Setze find_parent , bevor du .get("attr", "") sie dem Indexieren vor und prüfen Sie vor der Verkettung von Methodenaufrufen.
  • Die Paginierung ist eine Stoppbedingung. Erkenne den Anker für die nächste Seite, erstelle absolute URLs mit urljoinund begrenzen Sie die Schleife mit max_pages , damit ein Fehler nicht endlos ablaufen kann.
  • Höflichkeit ist Technik. UA-Rotation, Jittered Sleep, Exponential Backoff und die Beachtung robots.txt sind grundlegende Praktiken, keine optionalen Feinheiten, für jedes BeautifulSoup-Tutorial, das Sie mehr als einmal ausführen möchten.

FAQ

Was ist der Unterschied zwischen BeautifulSoups html.parser, lxml und html5lib?

html.parser ist im Lieferumfang von Python enthalten und muss nicht installiert werden, ist aber der langsamste der drei. lxml ist eine C-Erweiterung, die in der Praxis am schnellsten ist und die meisten fehlerhaften HTML-Strukturen gut verarbeitet; installiere sie mit pip install lxml. html5lib ist reines Python und am tolerantesten; es ahmt nach, wie ein echter Browser fehlerhafte Markups wiederherstellt, ist dafür aber merklich langsamer.

Wann sollte ich BeautifulSoup, Scrapy, Selenium oder Playwright verwenden?

Verwenden Sie BeautifulSoup für einmalige Skripte und statische Seiten, bei denen Sie den HTML-Code mit requests. Verwenden Sie Scrapy, wenn Sie einen echten Crawler mit Parallelität, Pipelines und Zeitplanung für Tausende von URLs benötigen. Verwenden Sie Selenium oder Playwright, wenn die Seite zur Darstellung von Inhalten auf JavaScript angewiesen ist, und übergeben Sie den gerenderten HTML-Code dann optional an BeautifulSoup zur Analyse.

Kann BeautifulSoup JavaScript-gerenderte Seiten selbst scrapen?

Nein. BeautifulSoup parst nur den empfangenen HTML-Code, und requests gibt die ursprüngliche Serverantwort zurück, ohne JavaScript auszuführen. Für Single-Page-Apps oder Inhalte, die nach dem Laden der Seite eingefügt werden, benötigen Sie einen Headless-Browser (Playwright, Selenium oder einen Cloud-Browser-Endpunkt), um das DOM zunächst zu rendern. Nach dem Rendern können Sie diesen HTML-Code weiterhin zur Analyse an BeautifulSoup übergeben.

Wie vermeide ich, dass meine IP-Adresse beim Scraping mit BeautifulSoup gesperrt wird?

Wechseln Sie die User-Agent-Strings, fügen Sie zufällige Verzögerungen zwischen den Anfragen ein und wiederholen Sie vorübergehende Fehler mit exponentiellem Backoff. Bei größeren Volumina leiten Sie den Datenverkehr über wechselnde Proxy-Server in Privathaushalten oder Rechenzentren. Beachten Sie robots.txt und vermeiden Sie das Scraping von Inhalten, die hinter einer Login-Barriere liegen. Aggressive Anti-Bot-Stacks wie Cloudflare erfordern oft einen verwalteten Unlocker anstelle von selbst vorgenommenen Header-Anpassungen.

Die Bibliothek selbst analysiert lediglich Text und ist nicht Gegenstand der rechtlichen Frage. Ob ein bestimmtes Scraping rechtmäßig ist, hängt im Allgemeinen von den Nutzungsbedingungen der Zielseite, den geltenden Urheberrechts- und Computermissbrauchsgesetzen in Ihrer Gerichtsbarkeit sowie davon ab, ob die Daten gemäß Vorschriften wie der DSGVO oder dem CCPA als personenbezogen gelten. Dies sind allgemeine Informationen und keine Rechtsberatung; konsultieren Sie einen Anwalt bei allen Fragen, die personenbezogene Daten, Paywalls oder die kommerzielle Weiterverbreitung betreffen.

Fazit

Sie haben dieses BeautifulSoup-Tutorial mit pip install und endeten mit einem Scraper, der paginiert, Wiederholungsversuche durchführt, User-Agents wechselt und saubere CSV- und JSON-Dateien exportiert. Die Struktur dieses Skripts ist wichtiger als jedes einzelne Code-Schnipsel: Trennen Sie das Abrufen vom Parsen, zielen Sie Elemente mit benannten CSS-Selektoren an, schützen Sie jeden verketteten Attributzugriff gegen Noneund behandeln Sie Anti-Block-Maßnahmen als Teil des Builds und nicht als nachträglichen Einfall. Websites werden sich ständig neu gestalten, Parser werden weiterhin blockiert werden, und die Codebasen, die sich langfristig bewähren, sind diejenigen, die diese Trennung von Anfang an beachten.

Wenn die Abrufebene mehr Zeit in Anspruch nimmt als die Parsing-Ebene, ist das ein Zeichen dafür, sie auszulagern. WebScrapingAPI übernimmt Proxy-Rotation, Header-Fingerprinting und CAPTCHA-Lösung hinter einem einzigen Endpunkt, sodass du den BeautifulSoup-Code, den du geschrieben hast, hier behalten und nur die Anfrage austauschen kannst, die ihm HTML zuführt. Viel Glück, und mögen deine Selektoren grün bleiben.

Über den Autor
Sorin-Gabriel Marica, Full-Stack-Entwickler @ WebScrapingAPI
Sorin-Gabriel MaricaFull-Stack-Entwickler

Sorin Marica ist Full-Stack- und DevOps-Entwickler bei WebScrapingAPI, wo er Produktfunktionen entwickelt und die Infrastruktur wartet, die für einen reibungslosen Betrieb der Plattform sorgt.

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.