Zurück zum Blog
Anleitungen
Andrei OgiolanLast updated on May 7, 202616 min read

Wie man HTML-Tabellen mit Python scrappt

Wie man HTML-Tabellen mit Python scrappt
Kurz gesagt: Die meisten HTML-Tabellen lassen sich mit einer einzigen Zeile pandas.read_html. Wenn die Tabelle paginiert ist, mit JavaScript gerendert wird oder zusammengeführte Kopfzeilen enthält, wechsle zu Requests + BeautifulSoup oder einem headless Browser wie Playwright. Dieser Leitfaden bietet dir eine Entscheidungsmatrix, funktionierenden Code für alle drei Ansätze und die Bereinigungsschritte, die die gescrapten Zeilen in pipeline-fähige Daten umwandeln.

Tabellarische Daten sind im öffentlichen Web allgegenwärtig, von Wikipedia-Infoboxen und Aktien-Screenern bis hin zu Regierungsstatistiken, Sportstatistiken und Produktvergleichsseiten. Wenn du weißt, wie man HTML-Tabellen mit Python scrapt, kannst du diese Zeilen innerhalb von Minuten in saubere DataFrames, JSON-Dokumente oder Zeilen in deiner eigenen Datenbank umwandeln.

Der Haken ist, dass HTML-Tabellen eine täuschend breite Kategorie sind. Manche Tabellen sind sauber in <table> Markup, das pandas mit einer einzigen Zeile parsen kann. Andere sind handgefertigte Raster aus <div>, die über Dutzende von Seiten verteilt sind oder erst nach Ausführung von JavaScript im Browser gefüllt werden. Eine Methode, die bei Wikipedia perfekt funktioniert, könnte bei einer Single-Page-App stillschweigend null Zeilen zurückgeben.

Dieser Leitfaden führt durch drei Python-Ansätze und gliedert den gesamten Artikel um zwei praktische Fragen: Welche Methode sollten Sie wählen, und wie stellen Sie sicher, dass Ihr Scraper weiterhin funktioniert, wenn die Website im nächsten Quartal ihr Markup ändert?

So scrapen Sie HTML-Tabellen mit Python: eine schnelle Entscheidungsmatrix

Bevor Sie eine einzige Zeile Code schreiben, entscheiden Sie, welches Tool zu der Tabelle vor Ihnen passt. Eine falsche Wahl ist der häufigste Grund, warum Tutorials den Kontakt mit echten Websites nicht überstehen. Nutzen Sie die untenstehende Matrix zur Selbstauswahl.

Kriterium

pandas.read_html

Requests + BeautifulSoup

Playwright (oder Selenium)

Am besten geeignet, wenn

Die Tabelle im Ausgangs-HTML vorhanden und wohlgeformt ist

Sie eine Steuerung oder Filterung auf Zellebene benötigen

Die Tabelle wird per JavaScript gerendert

Anzahl der Codezeilen

~3

30 bis 80

40 bis 100

Geschwindigkeit pro Seite

Schnell

Schnell

Langsam (vollständiger Browser)

Unterstützt JS

Nein

Nein

Ja

Paginierung

Manuelle Schleife

Manuelle Schleife oder versteckte API

Klicken und Scrollen

Resilienz gegenüber Markup-Änderungen

Mittel

Hoch (Sie schreiben Selektoren)

Hoch

Speicherbedarf

Gering

Niedrig

Hoch

Drei Faustregeln:

  • Wenn pd.read_html(url) die erwarteten Zeilen zurückgibt, hör dort auf. Der Einzeiler ist der wartungsfreundlichste Code, den du je schreiben wirst.
  • Wenn die Tabelle im HTML-Code vorhanden ist, du die Zellen jedoch filtern, zusammenführen oder normalisieren musst, bevor sie in ein DataFrame gelangen, greife zu Requests + BeautifulSoup.
  • Wenn „Seitenquelle anzeigen“ eine leere <div id="grid"> und die Daten erst nach dem Laden der Seite erscheinen, benötigen Sie Playwright oder einen versteckten JSON-Endpunkt.

Der Rest dieses Artikels zeigt, wie man HTML-Tabellen mit Python in jedem dieser Szenarien ausliest, sowie die Randfälle, die ansonsten funktionierenden Code zum Scheitern bringen.

Aufbau einer HTML-Tabelle (und was das Scraping schwierig macht)

Eine klassische HTML-Tabelle sieht so aus:

<table id="employees" class="stripe">
  <thead><tr><th>Name</th><th>Position</th><th>Salary</th></tr></thead>
  <tbody>
    <tr><td>Ada Lovelace</td><td>Engineer</td><td>$120,000</td></tr>
    <tr><td>Alan Turing</td><td>Researcher</td><td>$135,000</td></tr>
  </tbody>
</table>

Fünf Tags übernehmen den Großteil der Arbeit: <table> ist der Container, <thead> und <tbody> gruppiert Zeilen, <tr> ist eine Zeile und <th> bzw. <td> sind jeweils Kopf- und Datenzellen. Zwei Attribute verkomplizieren die Sache: colspan bewirkt, dass eine Zelle sich über mehrere Spalten erstreckt, und rowspan bewirkt, dass sie sich über mehrere Zeilen erstreckt. Beide werden häufig in Finanz- und Sporttabellen verwendet.

In der Praxis wird die Hälfte dieser Konventionen ignoriert. Viele Seiten lassen <thead> und <tbody>, lassen schließende Tags weg oder stellen Tabellen als verschachtelte <div> Raster dar, die kein Parser überhaupt als Tabelle erkennt. Beim Scraping in der Praxis geht es meist darum, mit dieser Abweichung fertig zu werden, weshalb pandas allein nicht auf jeder Website ausreicht.

Methode 1: pandas.read_html, der Einzeiler

pandas.read_html ist eine Komfortfunktion in der Datenbearbeitungsbibliothek pandas, die eine URL oder einen HTML-String entgegennimmt und eine Liste von DataFrames zurückgibt, einen pro <table> , den sie finden kann. Laut der pandas-Dokumentation benötigt sie entweder lxml, html5lib, oder bs4 im Hintergrund und identifiziert Tabellen, indem sie nach Standard-Tabellenelementen sucht.

Der ganze Reiz liegt darin, dass man mit drei Zeilen Code einen typisierten, abfragbaren DataFrame erhält:

import pandas as pd

tables = pd.read_html("https://en.wikipedia.org/wiki/List_of_largest_companies_by_revenue")
df = tables[0]
print(df.head())

Der Haken ist, dass read_html nur das sieht, was bereits im Antworttext enthalten ist. Wenn die Tabelle nach dem Laden der Seite per JavaScript ausgefüllt wird, löst die Funktion ValueError: No tables found , obwohl die Tabelle in Ihrem Browser deutlich sichtbar ist. Wenn Sie diese Einschränkung von vornherein kennen, sparen Sie sich viel Debugging.

Einrichten Ihrer Python-Umgebung

Sie können jedes Beispiel in diesem Leitfaden mit einer neuen virtuellen Umgebung und drei Paketen ausführen:

python -m venv .venv
source .venv/bin/activate
pip install pandas requests beautifulsoup4 lxml html5lib playwright
playwright install chromium

lxml ist der schnellste für Python verfügbare HTML-Parser und wird von den meisten Profis standardmäßig verwendet. html5lib ist langsamer, folgt aber dem WHATWG-Parsing-Algorithmus, was ihn zur tolerantesten Wahl bei fehlerhaftem Markup macht. Installiere beide, damit du den Parser wechseln kannst, wenn einer versagt.

Eine vollständige Anleitung zu pandas.read_html

Lassen Sie uns eine echte, wohlgeformte Tabelle scrapen: die Liste der Länder nach BIP auf Wikipedia. Der gesamte Workflow umfasst vier Zeilen.

import pandas as pd

url = "https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)"
tables = pd.read_html(url)
print(f"Found {len(tables)} tables on the page")

gdp = tables[2]            # pick the right one by index
gdp.columns = [c[1] if isinstance(c, tuple) else c for c in gdp.columns]
print(gdp.head())

Drei Dinge sind zu beachten. Erstens read_html eine Liste zurückgegeben, die Sie also indizieren. Zweitens: Wikipedia-Tabellen haben oft mehrstufige Kopfzeilen, die pandas als MultiIndex. Die Listenkomprimierung glättet diese, indem sie die untere Ebene beibehält. Drittens: keine manuelle Zeileniteration – jede Zelle befindet sich bereits in einer typisierten Spalte, die du .sort_values, .groupbyoder .to_csv aufrufen können.

Wenn Sie die Daten nur für eine schnelle Analyse benötigen, ist dies wirklich der gesamte Code, den Sie schreiben sollten.

Fehlerbehebung bei pandas.read_html: Häufige Fehler

pd.read_html schlägt auf vorhersehbare Weise fehl. Merken Sie sich diese vier Punkte, und Sie werden die meisten Probleme in weniger als einer Minute lösen.

  1. ValueError: No tables found. Die Seite wird entweder per JavaScript gerendert oder ist hinter einer Login-Barriere verborgen. Springe zum Abschnitt über Playwright.
  2. Der interne Fetcher von pandas gibt HTTP 403 oder 429 zurück. Der Standard- urllib User-Agent wird blockiert. Rufen Sie den HTML-Code selbst mit Requests ab und übergeben Sie die Zeichenkette an read_html:
import requests, pandas as pd
headers = {"User-Agent": "Mozilla/5.0 (compatible; analytics-bot/1.0)"}
html = requests.get(url, headers=headers, timeout=15).text
tables = pd.read_html(html)
  1. Falscher Tabellenindex. Verwenden Sie match= , um nach einer Zeichenfolge zu filtern, die in der Zieltabelle vorkommt, zum Beispiel pd.read_html(html, match="Population"). Dies ist weitaus stabiler, als sich auf tables[3].
  2. Verzerrte Zeichen in Nicht-ASCII-Inhalten. Erzwinge eine Kodierung, indem du Bytes explizit liest: response = requests.get(url); response.encoding = "utf-8"; tables = pd.read_html(response.text).

Wenn du nach diesen Korrekturen immer noch auf Probleme stößt, benötigt die Tabelle mit ziemlicher Sicherheit Requests + BeautifulSoup oder einen Headless-Browser, keine weiteren read_html Workarounds.

Methode 2: Requests + BeautifulSoup – wenn Sie die volle Kontrolle benötigen

pandas.read_html ist ideal, wenn Sie jede Zelle genau so haben möchten, wie sie im HTML-Code erscheint. Sobald Sie jedoch während der Extraktion Zeilen filtern, Werte aus zwei Spalten zusammenführen, Währungssymbole im Handumdrehen entfernen oder href aus einer verknüpften Zelle extrahieren, ist dies nicht mehr das richtige Werkzeug.

Hier kommt Requests + BeautifulSoup ins Spiel. Requests übernimmt die HTTP-Ebene (Header, Cookies, Sitzungen, Wiederholungsversuche), und BeautifulSoup liefert dir einen Parse-Baum, den du mit CSS-Selektoren, Attributabgleich oder Geschwister-Navigation durchlaufen kannst. Wenn du noch keine Erfahrung mit BeautifulSoup hast, führt dich unser ausführlicher Leitfaden zum Extrahieren und Parsen von Webdaten mit Python und BeautifulSoup detailliert durch die API-Oberfläche. Diese Kombination ist auch das, wofür sich die meisten Produktions-Scraper letztendlich entscheiden, da Sie jeden Schritt (Abrufen, Parsen, Extrahieren, Transformieren) selbst steuern können.

Die nächsten drei Abschnitte zeigen, wie man HTML-Tabellen mit Python und diesem Stack scrapt: eine höfliche Anfrage, ein robuster Selektor für die Tabelle und eine Zeilenschleife, die nicht abbricht, wenn eine Spalte hinzugefügt wird.

Senden von höflichen, realistischen HTTP-Anfragen

Anti-Bot-Abwehrmaßnahmen basieren auf einigen einfachen Merkmalen: einem fehlenden oder standardmäßigen User-Agent, keine Accept-Language, keine Cookies und Datenverkehr, der eine Sitzung in einer Sekunde erschöpft. Imitiere einen echten Browser und verwende eine Verbindung wieder:

import requests
from bs4 import BeautifulSoup

session = requests.Session()
session.headers.update({
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/124.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
})

response = session.get("https://example.com/employees", timeout=15)
response.raise_for_status()
soup = BeautifulSoup(response.text, "lxml")

Drei kleine Gewohnheiten sind entscheidend. Session() behält Cookies und Verbindungspooling zwischen Aufrufen bei. raise_for_status() wandelt stille 4xx/5xx-Antworten in Ausnahmen um, die du erneut versuchen kannst. Und die Übergabe "lxml" als Parser ist etwa fünf- bis zehnmal schneller als der integrierte html.parser bei großen Seiten.

Die richtige Tabelle auf der Seite finden

Sobald Sie ein BeautifulSoup Objekt haben, besteht das nächste Problem darin, die richtige <table>. Seiten enthalten in der Regel acht bis fünfzehn davon (z. B. Layout-Tabellen, Sidebar-Widgets, versteckte Paginierungselemente). Probieren Sie Selektoren in dieser Reihenfolge der Zuverlässigkeit aus:

# 1. By stable id (best)
table = soup.find("table", id="employees")

# 2. By a class that's specific to this table
table = soup.find("table", class_="data-grid")

# 3. By a CSS selector
table = soup.select_one("section#payroll table.stripe")

# 4. By the heading that precedes it (when classes are dynamic)
heading = soup.find(["h2", "h3"], string=lambda s: s and "Employees" in s)
table = heading.find_next("table") if heading else None

Wenn Klassennamen automatisch generiert werden und sich bei jedem Deploy ändern (ein gängiges React-Muster), bevorzugen Sie XPath über lxml, da es „die dritte Tabelle innerhalb des Abschnitts, dessen Überschrift den Text ‚Employees‘ enthält“ in einem Ausdruck ausdrücken kann. Wir haben einen separaten Leitfaden zu XPath versus CSS-Selektoren, der näher auf diesen Kompromiss eingeht.

Zeilen durchlaufen und Zellen auf sichere Weise extrahieren

Die meisten Scraping-Tutorials zeigen Zeilenschleifen, die Zellen positionell indizieren: cells[0] ist Name, cells[1] ist Position, cells[2] ist Gehalt. Dieser Code funktioniert nicht mehr, sobald jemand eine Spalte „Abteilung“ hinzufügt. Das robuste Muster besteht darin, die Kopfzeilen einmal zu lesen und sie mit jeder Zeile zu verknüpfen.

# Read headers from <thead> if present, else from the first row
header_cells = table.select("thead th") or table.select("tr:first-of-type th, tr:first-of-type td")
headers = [th.get_text(strip=True) for th in header_cells]

rows = []
for tr in table.select("tbody tr") or table.select("tr")[1:]:
    cells = [td.get_text(strip=True) for td in tr.find_all(["td", "th"])]
    if not cells:
        continue
    rows.append(dict(zip(headers, cells)))

print(f"Extracted {len(rows)} rows with {len(headers)} columns")

Das bringt dir drei Vorteile. Neue Spalten werden automatisch berücksichtigt, da die Schlüssel aus den Kopfzeilen stammen und nicht aus Indizes. Leere Zeilen (die oft als visuelle Trennzeichen dienen) werden übersprungen. Und jede Zelle durchläuft get_text(strip=True), wodurch Leerzeichen zusammengefasst und die \n Zeichen entfernt, die naive cell.text Aufrufe heimsuchen. Dies ist die Zeilenschleife, die du in jedes BeautifulSoup-Projekt kopieren solltest.

Gescrapte Zeilen als JSON, CSV oder Parquet speichern

Sobald du eine Liste von Dicts hast, lässt sich deren Speicherung pro Format mit einer einzigen Zeile erledigen:

import json
import pandas as pd

# JSON, human-readable, UTF-8 safe
with open("employees.json", "w", encoding="utf-8") as f:
    json.dump(rows, f, indent=2, ensure_ascii=False)

# CSV via pandas (handles quoting, encoding, and missing keys)
df = pd.DataFrame(rows)
df.to_csv("employees.csv", index=False, encoding="utf-8")

# Parquet for analytical pipelines (smaller files, typed columns)
df.to_parquet("employees.parquet", index=False)

Verwenden Sie JSON, wenn der Verbraucher ein Skript oder ein Frontend ist, CSV, wenn ein Mensch die Datei in Excel oder BigQuery öffnen wird, und Parquet, wenn der Datensatz mehrere hunderttausend Zeilen umfasst oder in Spark, Snowflake oder DuckDB eingespeist wird. Parquet-Dateien sind in der Regel 5- bis 10-mal kleiner als entsprechende CSV-Dateien und bewahren die Datentypen bei. Für alles, was für eine relationale Datenbank bestimmt ist, springen Sie direkt zu df.to_sql , damit Sie die Zwischendatei komplett überspringen.

Umgang mit komplexen Kopfzeilen mit colspan und rowspan

Zweizeilige Kopfzeilen sind in Finanzdaten, staatlichen Statistiken und Sporttabellen üblich. Die obere Zeile gruppiert Spalten („Q1 2024“, „Q2 2024“), und die untere Zeile beschriftet sie („Umsatz“, „Gewinn“). Das Festschreiben von Spaltennamen wie ["Name", "Position", "Contact"] funktioniert einmal und bricht dann für immer. Hier ist ein generischer Algorithmus, der colspan und rowspan.

def expand_header(table):
    # Return a flat list of column labels from a multi-row <thead>
    rows = table.select("thead tr")
    if not rows:
        return [th.get_text(strip=True) for th in table.select("tr:first-of-type th")]

    grid = []  # grid[row_index] = list of column labels at that row
    for r, tr in enumerate(rows):
        while len(grid) <= r:
            grid.append([])
        col = 0
        for th in tr.find_all(["th", "td"]):
            # skip already-filled slots from previous rowspans
            while col < len(grid[r]) and grid[r][col] is not None:
                col += 1
            text = th.get_text(strip=True)
            colspan = int(th.get("colspan", 1))
            rowspan = int(th.get("rowspan", 1))
            for dr in range(rowspan):
                while len(grid) <= r + dr:
                    grid.append([])
                row_buf = grid[r + dr]
                # pad
                while len(row_buf) < col + colspan:
                    row_buf.append(None)
                for dc in range(colspan):
                    row_buf[col + dc] = text
            col += colspan

    # Combine the columns of each row, top-down, into a single label per column
    n_cols = max(len(r) for r in grid)
    flat = []
    for c in range(n_cols):
        parts = [grid[r][c] for r in range(len(grid)) if c < len(grid[r]) and grid[r][c]]
        # de-dup adjacent identical strings: ['Q1 2024', 'Q1 2024', 'Revenue'] -> 'Q1 2024 Revenue'
        seen = []
        for p in parts:
            if not seen or seen[-1] != p:
                seen.append(p)
        flat.append(" ".join(seen))
    return flat

Kombiniere dies mit derselben zip(headers, cells) Zeilenschleife von zuvor, und Sie erhalten eine Kopfzeilenauswertung, die jede Kombination von zusammengeführten Zellen übersteht. Die gleiche Idee (ein 2D-Raster, das Sie colspan-für-colspan ausfüllen) lässt sich auf den Hauptteil übertragen, wenn rowspans Werte in Spalten wiederholen: Verfolgen Sie, welche Felder bereits belegt sind, und überspringen Sie diese in nachfolgenden <tr> Iterationen.

Scraping von paginierten HTML-Tabellen (drei Strategien)

Die Paginierung ist der am meisten unterschätzte Aspekt beim Scrapen von HTML-Tabellen mit Python. Die meisten Tutorials zeigen nur „Klicken Sie in einem Headless-Browser auf die Schaltfläche ‚Weiter‘“, was der langsamste und anfälligste Ansatz ist. Probieren Sie zunächst diese drei aus, in der Reihenfolge ihrer Präferenz.

1. Erhöhen Sie den Abfrageparameter „page-size“. Viele Tabellen akzeptieren ?per_page=500 oder ?length=1000. Eine Anfrage, alle Zeilen, keine Schleifen. Überprüfen Sie die URL, wenn Sie auf das Dropdown-Menü für die Seitengröße klicken, und Sie werden dies oft kostenlos finden.

2. Greifen Sie auf die zugrunde liegende JSON-API zu. Öffnen Sie DevTools, wechseln Sie zum Reiter „Netzwerk“, filtern Sie nach Fetch/XHRund klicken Sie auf die nächste Seite. Fast jede moderne Datentabelle wird von einem Endpunkt unterstützt, der JSON zurückgibt. Durch den direkten Aufruf wird das Parsen von HTML komplett übersprungen:

import requests
url = "https://example.com/api/employees"
all_rows = []
for page in range(1, 20):
    payload = requests.get(url, params={"page": page, "size": 100}, timeout=15).json()
    if not payload["items"]:
        break
    all_rows.extend(payload["items"])

3. Durchlaufen Sie die Abfrage-Strings der Seiten. Wenn die URL die Seitenzahl enthält (?page=2, &start=20), durchlaufen Sie diese explizit und hören Sie auf, wenn die Tabelle leer zurückkommt. Dies ist zuverlässiger als die Steuerung eines Browsers, da es nichts zu klicken gibt und keine Animation abgewartet werden muss.

Ein Headless-Browser ist dein letzter Ausweg, nicht deine erste Wahl. Hebe ihn dir für Tabellen auf, bei denen der Link zur nächsten Seite an einen JavaScript-Handler gebunden ist, ohne dass sich die URL ändert.

Methode 3: Playwright für JavaScript-gerenderte Tabellen

Wenn die Tabelle erst nach dem Laden der Seite erscheint, benötigen Sie etwas, das JavaScript ausführt. Playwright ist die moderne Wahl: Es enthält offizielle Python-Bindungen, läuft unter Chromium, Firefox oder WebKit und verfügt über ein solides Auto-Wait-Verhalten. Hier ist die vollständige Vorlage dafür, wie man HTML-Tabellen mit Python scrapt, die von JS abhängen:

from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup
import pandas as pd

URL = "https://example.com/dashboard"

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page(user_agent="Mozilla/5.0 ... Chrome/124.0 Safari/537.36")
    page.goto(URL, wait_until="domcontentloaded")

    # Wait for the actual data, not just the page load
    page.wait_for_selector("table#grid tbody tr", timeout=15000)

    html = page.content()
    browser.close()

# Hand the rendered HTML off to your existing parser
soup = BeautifulSoup(html, "lxml")
table = soup.find("table", id="grid")
# ... use the same row loop from earlier ...

# Or, when the table is well-formed, skip BeautifulSoup entirely:
df = pd.read_html(html, match="Department")[0]
print(df.head())

Das Muster ist immer dasselbe: navigieren, auf Daten warten (nicht nur load), abrufen page.content()und füttere diesen String dann in denselben Parsing-Code ein, den du für statisches HTML verwenden würdest. Informationen zu Installation, asynchronen APIs und Tracing findest du in der Playwright-Dokumentation für Python.

Selenium und Pyppeteer sind gültige Alternativen. Selenium verfügt über das größere Ökosystem und ist die sichere Wahl, wenn Ihr Team es bereits für End-to-End-Tests nutzt; unser Selenium-Schritt-für-Schritt-Tutorial behandelt die entsprechende Einrichtung. Pyppeteer ist schlanker, wird aber weniger aktiv gepflegt. Einen umfassenderen Vergleich von Headless-Tools finden Sie in unserem Playwright-Leitfaden zum Web-Scraping. Für neue Projekte ist Playwright in der Regel die ergonomischste Lösung.

Auswahl eines HTML-Parsers und Umgang mit leeren Zellen

BeautifulSoup ist ein Wrapper. Das eigentliche Parsen wird an eines von drei Backends delegiert, und die Wahl ist wichtiger, als die meisten Tutorials zugeben.

Parser

Geschwindigkeit

Toleranz gegenüber fehlerhaftem HTML

Installieren

html.parser

Langsam

Mittel (in Python integriert)

Keine

lxml

Schnell

Ziemlich streng, aber pragmatisch

pip install lxml

html5lib

Am langsamsten

Am höchsten, folgt WHATWG

pip install html5lib

Standardmäßig lxml. Wechseln Sie html5lib nur, wenn lxml auf einer Seite mit fehlerhaftem Markup (fehlende schließende </td>, nicht geschlossene <tr>, verirrte < Zeichen). Das lässt sich schnell überprüfen:

import time
from bs4 import BeautifulSoup

for parser in ["lxml", "html.parser", "html5lib"]:
    t0 = time.perf_counter()
    soup = BeautifulSoup(html, parser)
    rows = soup.select("tbody tr")
    print(f"{parser:10} {len(rows):4} rows in {time.perf_counter()-t0:.3f}s")

Schreiben Sie für leere Zellen einen Helper, der einen sinnvollen Standardwert zurückgibt, anstatt abzustürzen:

def cell_text(cell, default=""):
    if cell is None:
        return default
    text = cell.get_text(" ", strip=True)
    return text if text else default

Verwenden Sie ihn überall dort, wo Sie in eine Zeile indexieren. None Prüfungen an jeder Aufrufstelle überladen die Schleife und übersehen den Fall, in dem die Zelle zwar existiert, aber nur &nbsp;. Dieser Helper behandelt beides.

Blöcke vermeiden: Header, Sessions und Proxys

Ein 200-Statuscode bedeutet, dass die Anfrage akzeptiert wurde. Alles andere (insbesondere 403, 429 oder 503) bedeutet in der Regel, dass die Website deinen Scraper entdeckt hat. Gehe diese Leiter der Reihe nach ab und halte an der ersten Sprosse an, die funktioniert.

  1. Realistische Header. Setze User-Agent, Accept-Languageund Referer auf Werte, die eine echte Chrome-Sitzung senden würde. Allein dadurch lassen sich überraschend viele Blockierungen beheben.
  2. Persistente Sitzungen. Verwenden Sie requests.Session() , damit die von der Startseite gesetzten Cookies bei nachfolgenden Aufrufen gesendet werden. Viele Websites vergeben beim ersten Aufruf ein Sitzungscookie und lehnen Anfragen ab, bei denen dieses fehlt.
  3. Exponentieller Backoff bei 429 und 503. Warte 2 ** attempt Sekunden warten und bis zu fünfmal wiederholen. Beachten Sie Retry-After Header, wenn der Server sie bereitstellt.
  4. Rechenzentrums-Proxys. Günstig, schnell und für die meisten statischen Websites ausreichend. Wechsle die IPs in deinem Worker-Pool.
  5. Residential-Proxys. Echte Residential-IPs aus 195 Ländern, die verwendet werden, wenn Rechenzentrumsbereiche bereits blockiert sind. Langsamer, aber schwerer zu erkennen.
  6. Verwaltete Scraping-APIs. Wenn Sie sich auf das Parsen statt auf die Infrastruktur konzentrieren möchten, übernehmen Dienste wie unsere Scraper-API bei WebScrapingAPI die Proxy-Rotation, die Header-Generierung und Wiederholungsversuche hinter einem einzigen Endpunkt, sodass derselbe BeautifulSoup- oder Pandas-Code weiterhin funktioniert.

Die meisten Projekte benötigen die Stufen eins bis drei. Für eine ausführlichere Checkliste mit Erkennungssignalen befasst sich unser Leitfaden darüber, warum Scraper blockiert oder IP-gesperrt werden, mit TLS-Fingerprinting, Header-Reihenfolge und Rate-Limit-Berechnungen. Wenn Sie bei einem Wikipedia-Artikel blockiert werden, liegt ein anderes Problem vor.

Bereinigung, Typumwandlung und Export für die Produktion

Gescrapte Tabellen sind fast nie bereit für die Analyse. Währungssymbole, Prozentzeichen, Fußnotenmarkierungen und nachgestellte Leerzeichen schleichen sich alle als Zeichenfolgen ein. Beheben Sie diese in einem Durchgang, bevor Sie speichern:

import pandas as pd

df = pd.DataFrame(rows)

# 1. Strip whitespace on every text column
str_cols = df.select_dtypes(include="object").columns
df[str_cols] = df[str_cols].apply(lambda s: s.str.strip())

# 2. Coerce numeric columns (errors='coerce' turns junk into NaN)
df["salary"] = pd.to_numeric(df["salary"].str.replace(r"[^0-9.\-]", "", regex=True),
                             errors="coerce")
df["growth_pct"] = pd.to_numeric(df["growth_pct"].str.rstrip("%"), errors="coerce")

# 3. Coerce dates
df["hired_at"] = pd.to_datetime(df["hired_at"], errors="coerce")

# 4. Drop rows where the primary key failed to parse
df = df.dropna(subset=["employee_id"])

# 5. Persist
df.to_parquet("employees.parquet", index=False)
df.to_sql("employees", con=engine, if_exists="replace", index=False)

Das errors="coerce" Flag ist der unterschätzte Held dieser Pipeline: Fehlerhafte Zellen werden NaN statt einen Fehler auszulösen, und du kannst sie später mit df[df["salary"].isna()]. Für Produktionspipelines sollten Sie Parquet zur Speicherung verwenden und to_sql , um bereinigte Daten in Postgres oder dem Warehouse Ihrer Wahl zu speichern.

Rechtliche und ethische Leitlinien

Dies ist eine Anleitung zur Risikominderung, keine Rechtsberatung. Sprechen Sie mit einem Anwalt, bevor Sie sensible Daten scrapen.

  • Lesen Sie robots.txt. Es drückt die Präferenz des Website-Betreibers aus, ist aber keine gesetzliche Vorschrift; es zu ignorieren ist jedoch ein schneller Weg, gesperrt zu werden. Die Spezifikation ist in RFC 9309 dokumentiert.
  • Lesen Sie die Nutzungsbedingungen. Insbesondere das Scraping nach dem Einloggen verstößt oft gegen die Nutzungsbedingungen, selbst wenn robots.txt nichts dazu sagt.
  • Begrenze deine eigene Rate. Eine Anfrage pro Sekunde ist ein vernünftiger Standard für kleine Projekte. Füge Jitter hinzu, damit du nicht wie eine Uhr aussiehst.
  • Vermeiden Sie personenbezogene Daten, es sei denn, Sie haben eine rechtmäßige Grundlage. Die DSGVO und ähnliche Gesetze gelten auch dann, wenn die Daten technisch gesehen öffentlich sind.
  • Geben Sie bei der Weiterveröffentlichung die Quelle an. Geben Sie die Quell-URL und das Datum des Scrapings an.

Zu wissen, wie man HTML-Tabellen mit Python scrapt, ist zur Hälfte eine technische und zur Hälfte eine ethische Frage. Die technische Seite kann einmal versagen; die ethische Seite kann Ihr Unternehmen ruinieren.

Wichtige Erkenntnisse

  • Wähle das einfachste Tool, das funktioniert. pandas.read_html Für saubere statische Tabellen: Requests + BeautifulSoup für die Steuerung, Playwright für JS-gerenderte oder interaktionsgesteuerte Tabellen.
  • Kopfzeilen, keine Indizes. Verknüpfe den Kopfzeilentext mit dem Zellentext, damit dein Scraper eine hinzugefügte Spalte übersteht. Fest codiertes cells[0], cells[1] ist technische Schuld.
  • Die Paginierung hat drei Ebenen. Versuche per_page=500, dann eine versteckte JSON-API, dann Schleifen mit Seitenzahlen. Ein Headless-Browser ist der letzte Ausweg.
  • Bereinigen Sie die Daten, bevor Sie sie speichern. pd.to_numeric, pd.to_datetime, und errors="coerce" verwandeln Sie unsaubere, gescrapte Zeilen in einen typisierten DataFrame, der für die Analyse bereit ist.
  • Behandle die Website mit Respekt. Halte dich an robots.txt, drossle Anfragen und vermeide personenbezogene Daten, es sei denn, du hast eine klare rechtliche Grundlage.

FAQ

Was ist der Unterschied zwischen pandas.read_html und BeautifulSoup beim Scrapen von Tabellen?

pandas.read_html ist eine hochrangige Abkürzung: Es gibt DataFrames direkt zurück, verarbeitet jedoch nur Tabellen, die bereits in der HTML-Antwort enthalten sind. BeautifulSoup ist ein HTML-Parser auf niedrigerer Ebene, der dir die volle Kontrolle darüber gibt, welche Zellen du behältst, wie du sie transformierst und wie du mit nicht standardmäßigen Markups umgehst. Verwende read_html für analysefertige Daten, BeautifulSoup, wenn die von Ihnen benötigten Regeln nicht als „Gib mir Tabelle N“ ausgedrückt werden können.

Wie scrape ich eine HTML-Tabelle, die erst nach Ausführung von JavaScript erscheint?

Stellen Sie zunächst sicher, dass sie tatsächlich per JavaScript gerendert wird: Zeigen Sie den Quellcode der Seite an (Strg+U), suchen Sie nach einem Wort aus der Tabelle, und wenn es fehlt, wird die Tabelle clientseitig geladen. Die schnellste Lösung ist, den zugrunde liegenden JSON-Endpunkt im Netzwerk-Tab der DevTools zu finden und ihn direkt aufzurufen. Wenn das nicht möglich ist, steuern Sie einen Headless-Browser wie Playwright an, warten Sie auf einen Zeilen-Selektor und übergeben Sie dann page.content() an Ihren üblichen Parser weiter.

Was soll ich tun, wenn eine Tabelle zusammengeführte Zellen (rowspan oder colspan) enthält?

Behandeln Sie die Tabelle als 2D-Raster, das Sie Zelle für Zelle ausfüllen, wobei Sie colspan und rowspan , anstatt als Liste von Zeilen. Für jeden <th> oder <td>wiederholen Sie dessen Wert über die Slots, die seine Spanne abdeckt, und überspringen Sie Slots, die bereits durch eine frühere rowspan gefüllt wurden. Dies erzeugt eine rechteckige Matrix, die Sie an pd.DataFrame , ohne dass es zu Abweichungen bei der Spaltenanzahl kommt.

Wie stelle ich sicher, dass numerische und Datumsspalten nach dem Scraping einer Tabelle den richtigen Typ behalten?

Entfernen Sie nicht-numerische Zeichen mit einem regulären Ausdruck (str.replace(r"[^0-9.\-]", "", regex=True)), und rufen Sie dann pd.to_numeric(series, errors="coerce") , damit nicht analysierbare Werte NaN statt einen Fehler auszulösen. Für Datumsangaben pd.to_datetime(series, errors="coerce", format="%Y-%m-%d") ist das Äquivalent. Das Hinzufügen des format Arguments beschleunigt das Parsen bei großen Spalten um etwa das Zehnfache und verhindert Fehlalarme durch mehrdeutige Zeichenfolgen.

Kann ich pandas.read_html auf eine lokale HTML-Datei oder einen rohen HTML-String anwenden?

Ja. pd.read_html akzeptiert eine URL, einen Pfad zu einer lokalen Datei oder einen reinen HTML-String. Übergeben Sie pd.read_html(open("page.html").read()) , um eine Zeichenkette zu übergeben, oder pd.read_html("page.html") für einen Dateipfad. Dies ist nützlich für Unit-Tests (Commit einer als fehlerfrei bekannten HTML-Fixture) und zur Trennung von Abruf und Parsing in Produktions-Scrapern.

Zusammenfassung

Um HTML-Tabellen mit Python zu scrapen, kommt es vor allem darauf an, das richtige Tool für die jeweilige Tabelle zu wählen. Beginnen Sie pandas.read_html zu, steigen Sie auf Requests + BeautifulSoup um, wenn Sie Kontrolle auf Zellenebene benötigen, und setzen Sie Playwright nur ein, wenn JavaScript die Daten rendert. Ergänzen Sie dies durch header-bewusste Zeilenschleifen, generisches Parsen von colspan/rowspan, intelligente Paginierung und einen Pandas-Bereinigungsdurchlauf, und Sie haben einen Scraper, der Änderungen am Markup übersteht, anstatt beim nächsten Deploy zu versagen.

Wenn du über die selbst erstellte Proxy-Rotation und das JavaScript-Rendering hinausgewachsen bist, bietet WebScrapingAPI eine Scraper-API, die die Anforderungsschicht hinter einem einzigen Endpunkt abwickelt, sodass dein Parsing-Code weiterhin funktioniert. Von hier aus kannst du unsere ausführlicheren Anleitungen zu JavaScript-Tabellen und zur Vermeidung von Blockierungen durchstöbern.

Über den Autor
Andrei Ogiolan, Full-Stack-Entwickler @ WebScrapingAPI
Andrei OgiolanFull-Stack-Entwickler

Andrei Ogiolan ist Full-Stack-Entwickler bei WebScrapingAPI, wo er in verschiedenen Bereichen des Produkts mitwirkt und dabei hilft, zuverlässige Tools und Funktionen für die Plattform zu entwickeln.

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.