BeautifulSoup in Python ist die Bibliothek, nach der die meisten Menschen greifen, wenn sie strukturierte Daten aus einem unübersichtlichen HTML-Dokument extrahieren müssen. Sie verwandelt eine rohe Seite in einen navigierbaren Baum aus Python-Objekten und gibt Ihnen dann eine kleine, lesbare API, mit der Sie die gewünschten Elemente finden und deren Text oder Attribute lesen können. Sie müssen keine Abfragesprache lernen oder einen Parser schreiben; Sie beschreiben, was Sie möchten, mit einem Tag-Namen, einem Attribut oder einem CSS-Selektor, und BeautifulSoup gibt es zurück.

Dieser Leitfaden ist eine praktische Tour durch diese API. Wir installieren BeautifulSoup mit einem schnellen Parser, bauen eine Soup aus Beispiel-Markup, und gehen dann durch find und find_all, die CSS-Selektor-Methoden select und select_one, die Navigation im Baum nach Eltern und Geschwistern sowie das Lesen von Text gegenüber Attributen. Wir schließen mit einem realistischen Praxisbeispiel ab, das eine Liste von Datensätzen extrahiert und der Paginierung folgt. Etwas ist während der gesamten Anleitung wichtig zu bedenken: BeautifulSoup parst nur. Es ruft nie eine URL ab oder führt JavaScript aus, also muss das HTML, das Sie ihm übergeben, bereits die gewünschten Daten enthalten.

Was BeautifulSoup tut und was nicht

BeautifulSoup ist eine Parsing-Bibliothek. Sie übergeben ihr einen String aus HTML oder XML, und sie baut einen Baum auf, den Sie durchsuchen und navigieren können. Das ist die gesamte Aufgabe. Sie öffnet keine Netzwerkverbindungen, führt keine Skripte aus und hat keine Ahnung, was ein Browser rendern würde. Alles, was Sie extrahieren, muss im von Ihnen übergebenen Markup vorhanden sein.

Diese Grenze ist wichtig, weil die zwei Hälften eines Scrapes getrennte Anliegen sind. Die Seite abzurufen ist ein Problem; sie zu parsen ist ein anderes. Für statische Seiten können Sie BeautifulSoup mit der Bibliothek requests kombinieren, um das HTML zu holen. Für Seiten, die ihren Inhalt clientseitig mit JavaScript aufbauen, gibt ein einfacher Abruf eine nahezu leere Shell zurück, und BeautifulSoup findet nichts. Wir kommen später auf diesen Fall zurück. Behandeln Sie BeautifulSoup vorerst als die Parsing-Hälfte der Pipeline und nichts weiter.

BeautifulSoup und einen Parser installieren

BeautifulSoup selbst wird im Paket beautifulsoup4 ausgeliefert. Es benötigt auch einen Parser, um die eigentliche Arbeit des Lesens von HTML zu erledigen. Die Standardbibliothek enthält html.parser, der keine zusätzlichen Abhängigkeiten hat und für die meisten Aufgaben geeignet ist. Für Geschwindigkeit und Toleranz gegenüber fehlerhaftem Markup installieren Sie auch lxml und verwenden es als Parser.

bash
python -m venv bs_env
source bs_env/bin/activate

pip install beautifulsoup4 lxml requests

Unter Windows aktivieren Sie die Umgebung mit bs_env\Scripts\activate statt der source-Zeile. Die requests-Installation ist optional; wir verwenden sie nur, um statische Seiten im Praxisbeispiel abzurufen. Sobald alles eingerichtet ist, importieren Sie die Klasse aus bs4, nicht aus einem Paket mit dem Namen der Bibliothek.

Eine Soup erstellen

Das Erstellen einer Soup nimmt zwei Argumente: das Markup und den Namen des Parsers. Um ohne Zugriff auf eine Live-Seite mitmachen zu können, beginnen Sie mit einem Inline-HTML-String, damit die Eingabe vorhersehbar ist.

python
from bs4 import BeautifulSoup

html = """
<html>
  <body>
    <h1 id="title">Books</h1>
    <ul class="catalog">
      <li class="book"><a href="/b/1">Dune</a><span class="price">12.99</span></li>
      <li class="book"><a href="/b/2">Neuromancer</a><span class="price">9.50</span></li>
    </ul>
  </body>
</html>
"""

soup = BeautifulSoup(html, "lxml")
print(soup.title)  # None here; no <title> in the markup
print(soup.h1.get_text())  # Books

Tauschen Sie "lxml" gegen "html.parser" aus, wenn Sie lxml nicht installiert haben; der Rest der API ist identisch. Auf ein Tag per Name zuzugreifen, wie soup.h1, gibt das erste übereinstimmende Element als Abkürzung zurück. Das ist praktisch für schnelle Prüfungen, aber begrenzt, daher erfolgt die eigentliche Suche über die unten beschriebenen Methoden.

Parser bewusst wählen

Der gewählte Parser ändert, wie fehlerhaftes HTML repariert wird. html.parser ist eingebaut und abhängigkeitsfrei. lxml ist schneller und toleranter gegenüber fehlerhaften Seiten, was die meisten echten Seiten sind. html5lib parst exakt wie ein Browser, ist aber langsamer. Wenn zwei Parser bei einer kniffligen Seite nicht übereinstimmen, ist das normalerweise die Ursache, also benennen Sie den Parser explizit, anstatt BeautifulSoup raten zu lassen.

find und find_all

Die zwei grundlegenden Methoden sind find und find_all. find gibt das erste übereinstimmende Element zurück oder None, wenn nichts übereinstimmt. find_all gibt eine Liste aller Übereinstimmungen zurück, die leer ist, wenn nichts übereinstimmt. Beide nehmen einen Tag-Namen und optionale Filter entgegen.

python
first_book = soup.find("li")
print(first_book.a.get_text())  # Dune

all_books = soup.find_all("li")
print(len(all_books))  # 2

for book in all_books:
    print(book.a.get_text())

Filter grenzen die Suche ein. Sie können eine CSS-Klasse, eine ID, ein beliebiges Attribut oder ein Dictionary mit Attributen abgleichen. Da class in Python ein reserviertes Wort ist, verwendet BeautifulSoup das Schlüsselwortargument class_ mit einem abschließenden Unterstrich.

python
# By class
prices = soup.find_all("span", class_="price")

# By id
heading = soup.find(id="title")

# By any attribute, via the attrs dict
links = soup.find_all("a", attrs={"href": True})

# Limit how many you get back
one_link = soup.find_all("a", limit=1)

Sie können auch eine Liste von Tag-Namen übergeben, um einen davon abzugleichen, oder einen kompilierten regulären Ausdruck, um Tag-Namen oder Attributwerte nach Muster abzugleichen. Für die meisten Scraping-Aufgaben decken Klassen- und Attributfilter das Feld ab, und die CSS-Selektor-Methoden unten sind oft sauberer für verschachtelte Bedingungen.

select und select_one mit CSS-Selektoren

Wenn Sie bereits in CSS-Selektoren denken, ermöglichen Ihnen select und select_one, dieses Wissen direkt zu nutzen. select gibt eine Liste aller Übereinstimmungen zurück; select_one gibt die erste Übereinstimmung oder None zurück. Sie akzeptieren dieselbe Selector-Syntax, die Sie in einem Stylesheet oder als Argument für document.querySelectorAll schreiben würden.

python
# Descendant: every <a> inside a .book li
titles = soup.select("li.book a")

# First price under the catalog list
first_price = soup.select_one("ul.catalog .price")

# Attribute selector
internal = soup.select("a[href^='/b/']")

# By id
heading = soup.select_one("#title")

Selektoren glänzen, wenn das Ziel durch seine Position im Baum definiert ist, wie "der Link im zweiten Listenelement." Eine lange Kette von find-Aufrufen liest sich schlechter als der äquivalente einzeilige Selektor. Ob Sie find_all oder select bevorzugen, ist meist Geschmackssache; die beiden sind für die meisten Aufgaben austauschbar, und ein einzelnes Skript mischt oft beides. Für einen tieferen Vergleich der Selektor-Stile lesen Sie Web-Scraping mit XPath und CSS-Selektoren.

Im Baum navigieren

Sobald Sie ein Element haben, können Sie sich relativ dazu im Baum bewegen, anstatt von oben neu zu suchen. Jedes Tag gibt seinen Elternteil, seine Kinder und seine Geschwister preis, was genau das ist, was Sie brauchen, wenn die gewünschten Daten in der Nähe eines bereits gefundenen Elements liegen.

python
price = soup.select_one(".price")

# Up: the <li> that contains this price
row = price.parent

# Down: direct children, ignoring whitespace text nodes
children = [c for c in row.children if c.name]

# Sideways: the <a> just before the price in the same <li>
title_link = price.find_previous_sibling("a")
print(title_link.get_text())  # Dune

Einige Hinweise, die Verwirrung ersparen. .children und .contents enthalten Textknoten wie den Leerraum zwischen Tags, also filtert das Filtern auf c.name nur echte Elemente heraus. .find_next_sibling und .find_previous_sibling überspringen diese Textknoten für Sie und akzeptieren einen Tag-Namen zum Abgleich. Verwenden Sie .find_parent, um zu einem bestimmten Vorfahren statt nur dem unmittelbaren Elternteil hochzugehen. Relative Navigation ist der zuverlässigste Weg, Seiten zu handhaben, bei denen der nützliche Wert neben einem stabilen Label liegt.

Text und Attribute lesen

Die Extraktion läuft auf zwei Dinge hinaus: den Text innerhalb eines Elements und die Werte seiner Attribute. Für Text gibt get_text den gesamten String-Inhalt eines Elements und seiner Nachkommen zusammengefügt zurück. Übergeben Sie strip=True, um umgebende Leerzeichen zu trimmen, was Sie fast immer möchten.

python
link = soup.select_one("li.book a")

# Text content
print(link.get_text(strip=True))  # Dune

# Attribute by key; raises KeyError if absent
print(link["href"])  # /b/1

# Safe attribute read with a default
print(link.get("title", ""))

Ein Attribut mit eckigen Klammern zu lesen, wie link["href"], löst einen KeyError aus, wenn das Attribut fehlt, also bevorzugen Sie link.get("href"), wenn ein Attribut möglicherweise nicht vorhanden ist. Der Unterschied zwischen Text und Attributen verwirrt Anfänger: Das sichtbare Label eines Links kommt aus get_text, aber seine Ziel-URL kommt aus dem Attribut href, und die beiden haben nichts miteinander zu tun.

Gegen fehlende Elemente absichern

Wenn ein Selektor nichts findet, geben find und select_one None zurück, und .get_text() auf None aufzurufen löst einen AttributeError aus. Echte Seiten sind inkonsistent: Nicht jede Zeile hat einen Preis, nicht jede Karte hat eine Bewertung. Prüfen Sie, ob ein Element vorhanden ist, bevor Sie davon lesen, oder kapseln Sie einen kleinen Helfer, der None zurückgibt, wenn die Abfrage scheitert, damit ein fehlendes Feld keinen ganzen Lauf zum Absturz bringt.

Ein Praxisbeispiel: Eine Liste von Datensätzen extrahieren

Setzen Sie nun die Teile auf einer statischen Seite zusammen, die für Übungs-Scraping gebaut wurde. Die Seite quotes.toscrape.com liefert einfaches server-gerendertes HTML, sodass requests sie abrufen und BeautifulSoup sie direkt parsen kann. Jedes Zitat sitzt in einem div.quote-Block mit dem Text, dem Autor und einer Liste von Tags, was ein sauberer Ersatz für die Art von wiederholtem Datensatz ist, den Sie bei echten Aufgaben scrapen.

python
import requests
from bs4 import BeautifulSoup

def parse_quotes(html):
    soup = BeautifulSoup(html, "lxml")
    records = []
    for block in soup.select("div.quote"):
        text_el = block.select_one("span.text")
        author_el = block.select_one("small.author")
        tags = [t.get_text(strip=True) for t in block.select("a.tag")]
        records.append({
            "quote": text_el.get_text(strip=True) if text_el else None,
            "author": author_el.get_text(strip=True) if author_el else None,
            "tags": tags,
        })
    return records

url = "https://quotes.toscrape.com/"
resp = requests.get(url, timeout=15)
if resp.status_code == 200:
    for row in parse_quotes(resp.text):
        print(row)

Das Muster hier ist das, das Sie überall wiederverwenden: Den sich wiederholenden Container mit select auswählen, dann eine zweite, bereichsbeschränkte Abfrage innerhalb jedes Containers ausführen, um einzelne Felder zu ziehen. Die feldspezifischen Abfragen auf block statt auf das gesamte Dokument zu beschränken verhindert, dass der Autor von Zeile zwei in Zeile eins landet. Jedes Element vor dem Aufruf von get_text zu prüfen bedeutet, dass ein Zitat ohne Autor None ergibt statt die Schleife zum Absturz zu bringen.

Der Paginierung folgen

Eine Seite ist eine Demo; ein vollständiger Datensatz umfasst normalerweise viele. Die Übungsseite verlinkt die nächste Seite über ein Element li.next > a, also ist die Schleife unkompliziert: Die aktuelle Seite parsen, nach dem Nächste-Seite-Link suchen, ihn gegen die Basis-URL auflösen und stoppen, wenn der Link weg ist.

python
import time
from urllib.parse import urljoin

base = "https://quotes.toscrape.com/"
next_url = base
all_rows = []

while next_url:
    resp = requests.get(next_url, timeout=15)
    if resp.status_code != 200:
        break

    soup = BeautifulSoup(resp.text, "lxml")
    all_rows.extend(parse_quotes(resp.text))

    next_link = soup.select_one("li.next a")
    next_url = urljoin(base, next_link["href"]) if next_link else None
    time.sleep(1)

print(f"Collected {len(all_rows)} quotes")

Zwei Details machen das robust. urljoin verwandelt einen relativen href wie /page/2/ ohne String-Akrobatik in eine vollständige URL, sodass es weiterhin funktioniert, wenn sich die Pfadform ändert. Das time.sleep(1) verteilt Anfragen, damit Sie den Server nicht überlasten, was sowohl höflich als auch der einfachste Weg ist, unter einem Rate-Limit zu bleiben. Eine ausführlichere Behandlung des Abrufens und Strukturierens von Daten von Anfang bis Ende finden Sie unter wie man eine Website mit Python scrapt.

Wenn BeautifulSoup nicht ausreicht: JavaScript-Seiten

Alles oben setzt voraus, dass die Daten in dem abgerufenen HTML vorhanden sind. Viele moderne Seiten funktionieren nicht so. Sie senden eine minimale HTML-Shell und bauen den echten Inhalt im Browser mit JavaScript auf, wobei Daten aus Hintergrund-API-Aufrufen nach dem Laden der Seite gezogen werden. Rufen Sie eine davon mit requests ab und der Body, den Sie BeautifulSoup übergeben, hat leere Container, wo die Datensätze sein sollten. BeautifulSoup macht seinen Job korrekt; die Daten waren nie im String.

Sie haben zwei Auswege. Sie können selbst einen echten Browser mit einem Tool wie Selenium oder Playwright ausführen, auf das Rendern des Inhalts warten und die gerenderte page_source an BeautifulSoup übergeben. Das funktioniert, bedeutet aber das Betreiben und Warten einer Browser-Flotte, und auf geschützten Seiten müssen Sie auch Proxys und Challenges handhaben. Der andere Weg ist, den Abruf-und-Render-Schritt an einen Dienst auszulagern, der fertiges HTML zurückgibt, und dann dieses HTML mit demselben BeautifulSoup-Code zu parsen, den Sie bereits geschrieben haben. In jedem Fall ändert sich die Parsing-Schicht nicht; nur wie Sie das HTML erhalten. Mehr zu dieser Aufteilung finden Sie unter wie man JavaScript-Seiten mit Python scrapt.

Crawlbase Crawling API

BeautifulSoup parst nur; es kann keine JavaScript-Seite rendern oder Sie an einer aggressiven Sperrung vorbeibringen. Die Crawling API erledigt die Abruf-und-Render-Hälfte für Sie: Senden Sie eine URL mit einem JS-Token, sie führt die Seite in einem echten Browser hinter rotierenden Residential-IPs aus und gibt fertiges HTML zurück. Sie parsen dann dieses HTML mit demselben BeautifulSoup-Code in diesem Leitfaden. Probieren Sie es zuerst im kostenlosen Tarif aus.

Hier ist die Form dieser Kombination. Der Abruf geht über die Crawling API mit einem JavaScript-Token, und der zurückgegebene Body fließt direkt in Ihren bestehenden Parser.

python
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})

response = api.get("https://example.com/spa-page", {"ajax_wait": "true", "page_wait": 4000})

if response["status_code"] == 200:
    html = response["body"].decode("utf-8")
    soup = BeautifulSoup(html, "lxml")
    # same find/select calls as before
    print(soup.select_one("h1").get_text(strip=True))

Wenn Sie lieber Ihren eigenen Client über rotierende IPs leiten möchten statt einen verwalteten Endpunkt aufzurufen, gibt Ihnen der Smart AI Proxy Residential-Rotation als Drop-in-Proxy, und für vorparsiertes JSON gibt die Crawling API strukturierte Felder für unterstützte Seiten zurück, ohne BeautifulSoup überhaupt zu benötigen.

Zusammenfassung

Wichtigste Erkenntnisse

  • BeautifulSoup parst nur. Es baut einen durchsuchbaren Baum aus bereits vorhandenem HTML; es ruft keine URL ab und führt kein JavaScript aus.
  • beautifulsoup4 plus einen Parser installieren. Verwenden Sie html.parser für null Abhängigkeiten oder lxml für Geschwindigkeit und Toleranz gegenüber fehlerhaftem Markup, und benennen Sie den Parser explizit.
  • Vier Methoden lernen. find und find_all suchen nach Tag und Filtern; select und select_one suchen nach CSS-Selektor. Sie sind für die meisten Aufgaben austauschbar.
  • Text und Attribute separat lesen. get_text(strip=True) liefert den sichtbaren Inhalt; element["href"] oder element.get("href") liefert einen Attributwert.
  • Bereichsbeschränken, absichern und paginieren. Den sich wiederholenden Container auswählen, jedes Feld darin abfragen, auf None prüfen und Nächste-Seite-Links mit urljoin und einer kleinen Verzögerung folgen.
  • Bei JavaScript-Seiten den Abruf korrigieren. Die Crawling API oder einen Headless-Browser verwenden, um gerendertes HTML zu erhalten, dann mit demselben BeautifulSoup-Code parsen.

Häufig gestellte Fragen

Wie installiere ich BeautifulSoup in Python?

Installieren Sie es mit pip install beautifulsoup4. Der Import-Name unterscheidet sich vom Paketnamen: Sie schreiben from bs4 import BeautifulSoup in Ihrem Code. BeautifulSoup benötigt auch einen Parser, um die Arbeit zu erledigen. Das eingebaute html.parser benötigt nichts extra, aber die Installation von lxml mit pip install lxml gibt Ihnen einen schnelleren, toleranteren Parser, was es für echte Seiten wert ist.

Was ist der Unterschied zwischen find und find_all?

find gibt das einzelne erste Element zurück, das Ihren Kriterien entspricht, oder None, wenn nichts übereinstimmt. find_all gibt eine Liste jedes übereinstimmenden Elements zurück, die leer ist, wenn keine Übereinstimmungen vorliegen. Verwenden Sie find, wenn Sie genau ein Element erwarten, wie die Hauptüberschrift einer Seite, und find_all, wenn Sie viele sammeln, wie jede Zeile in einer Liste. Die CSS-Selektor-Äquivalente sind select_one und select.

Wie lese ich den Text in einem Element gegenüber einem Attribut?

Verwenden Sie element.get_text(strip=True) für den sichtbaren Textinhalt, einschließlich Text aus verschachtelten Tags, mit gekürztem umgebendem Leerraum. Verwenden Sie element["href"], um einen Attributwert zu lesen, oder element.get("href"), um ihn sicher mit einem Standard zu lesen, wenn das Attribut möglicherweise fehlt. Das Label eines Links und seine Ziel-URL sind getrennt: Das Label ist Text, die URL ist das Attribut href.

Warum gibt BeautifulSoup auf manchen Seiten ein leeres Ergebnis zurück?

Fast immer weil die Daten nicht in dem geparsten HTML vorhanden sind. Viele Seiten rendern Inhalte im Browser mit JavaScript, sodass ein einfacher Abruf eine leere Shell zurückgibt und BeautifulSoup korrekt nichts findet. BeautifulSoup führt kein JavaScript aus. Um solche Seiten zu handhaben, holen Sie zuerst gerendertes HTML, entweder mit einem Headless-Browser wie Selenium oder Playwright oder mit der Crawling API, dann parsen Sie dieses gerenderte HTML mit demselben Code.

Kann BeautifulSoup die Paginierung allein handhaben?

Nicht von selbst, weil BeautifulSoup keine Seiten abruft. Sie handhaben die Paginierung mit einer Schleife: Die aktuelle Seite parsen, BeautifulSoup verwenden, um den Nächste-Seite-Link zu finden, diese URL mit Ihrem HTTP-Client abrufen und wiederholen, bis kein nächster Link mehr vorhanden ist. Relative Links mit urllib.parse.urljoin auflösen und eine kurze Verzögerung zwischen Anfragen hinzufügen, damit Sie den Server nicht überlasten.

Soll ich lxml oder html.parser als Parser verwenden?

Verwenden Sie lxml, wenn Sie können: Es ist schneller und behandelt fehlerhaftes HTML besser, was die meisten echten Seiten abdeckt. Verwenden Sie das eingebaute html.parser, wenn Sie keine zusätzlichen Abhängigkeiten möchten und die Seiten wohlgeformt sind. Für Markup, das genau so geparst werden muss, wie es ein Browser tun würde, ist html5lib am genauesten, auf Kosten der Geschwindigkeit. Übergeben Sie immer den Parser-Namen explizit, damit das Verhalten über Maschinen hinweg konsistent bleibt.

Jetzt loslegen

Crawlen Sie jede Website im großen Maßstab, ohne gegen die Infrastruktur zu kämpfen.

Crawlbase übernimmt Proxys, Fingerprints und CAPTCHAs, damit Ihr Team Datenpipelines ausliefert, statt Crawl-Infrastruktur zu pflegen. 1.000 Anfragen kostenlos, keine Karte erforderlich.

Self-Service · Kein Verkaufsgespräch erforderlich · Enterprise-Crawl-Volumen verfügbar