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.
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.
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.
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.
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.
# 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.
# 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.
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.
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.
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.
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.
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.
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.
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.
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.
-
beautifulsoup4plus einen Parser installieren. Verwenden Siehtml.parserfür null Abhängigkeiten oderlxmlfür Geschwindigkeit und Toleranz gegenüber fehlerhaftem Markup, und benennen Sie den Parser explizit. -
Vier Methoden lernen.
findundfind_allsuchen nach Tag und Filtern;selectundselect_onesuchen 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"]oderelement.get("href")liefert einen Attributwert. -
Bereichsbeschränken, absichern und paginieren. Den sich wiederholenden Container auswählen, jedes Feld darin abfragen, auf
Noneprüfen und Nächste-Seite-Links miturljoinund 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.
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.
