Geonode logo
Geonode Team

Geonode Team

Aktualisiert: 7. Oktober 2026

Veröffentlicht: 02.09.2026

pandas.read_html(): Umfassende Anleitung mit Beispielen

`pandas.read_html()` Extrahiert HTML-Tabellen in einer einzigen Zeile in DataFrames. Das ist wirklich nützlich, aber es ist auch die Funktion, die Anfängern am ehesten den falschen Eindruck vermittelt, wie einfach die Extraktion von Webdaten ist. Sie gibt eine *Liste* von DataFrames zurück, nicht nur einen. Sie findet ausschließlich Elemente vom Typ „`<table>`“. Und schon in der Dokumentation selbst wird darauf hingewiesen, dass anschließend eine Bereinigung erforderlich ist. Dieser Leitfaden behandelt die Funktionsweise der Funktion, die wissenswerten Parameter und die Fälle, in denen ein anderes Tool die richtige Wahl ist.

Unsere Position: Wir sind Geonode und verkaufen Proxys, daher stehen Tabellen im öffentlichen Web in direktem Zusammenhang mit unserem Geschäft. Der entscheidende Hinweis ist, dass read_html beim direkten Abrufen einer URL keine Kontrolle über die Anfrage bietet – keine benutzerdefinierten Header, keine Sitzung, keine Wiederholungslogik und keine Möglichkeit, die Anfrage über irgendetwas weiterzuleiten. Für eine einmalige Tabelle auf einer kooperativen Website ist das in Ordnung und die schnellste verfügbare Methode. Bei wiederkehrenden Abfragen sollten Sie den HTML-Code selbst mit einem geeigneten HTTP-Client abrufen und die Zeichenfolge an read_html übergeben. Diese Trennung kostet nur eine zusätzliche Zeile und bietet Ihnen alles, was die integrierte fetch-Funktion vorenthält.

Die Grundlagen

import pandas as pd

tables = pd.read_html("https://example.com/data")
print(len(tables))
df = tables[0]

Der entscheidende Punkt aus der pandas-Dokumentation: Die Funktion gibt „eine Liste von DataFrames“ zurück. Nicht einen einzigen DataFrame. Eine Seite mit sechs Tabellen liefert Ihnen sechs DataFrames in der Reihenfolge des Dokuments, und tables[0]

könnte durchaus ein Navigationslayout sein und nicht die Daten, die Sie eigentlich wollten.

In der Dokumentation wird zudem klargestellt, dass die Funktion „immer eine Liste von DataFrames zurückgibt oder vollständig fehlschlägt“ – sie gibt keine leere Liste zurück, außer in Ausnahmefällen wie „einer einzelnen Zeile mit <td>

, die nur Leerzeichen enthält“. Ein leeres Ergebnis ist also eher ein Hinweis darauf, dass etwas Ungewöhnliches passiert ist, als ein normales Ergebnis.

Sie können auch HTML direkt übergeben, was für alles, was über einen kurzen Überblick hinausgeht, die bevorzugte Vorgehensweise ist:

import requests
html = requests.get(url, headers={"User-Agent": "MyBot/1.0 (+https://example.com/bot)"}).text
tables = pd.read_html(html)

Was es erkennen kann und was nicht

Wenn man den Anwendungsbereich versteht, lassen sich die meisten Enttäuschungen vermeiden.

Es liest ausschließlich „<table>“-Elemente. In der Dokumentation heißt es, dass es „nach <table>-Elementen sucht und nur <tr>, <th>-Zeilen sowie <td>-Elemente innerhalb von Tabellen verarbeitet“. Ein Layout, das aus <div>-Elementen aufgebaut und als Raster gestaltet ist – was dem modernen Webdesign entspricht –, enthält keine Tabelle, die es finden könnte, unabhängig davon, wie tabellenähnlich es auf dem Bildschirm aussieht.

Es führt kein JavaScript aus. Wenn die Tabelle clientseitig gerendert wird, enthält der HTML-read_html, den es sieht, diese nicht. Dies ist der häufigste Grund dafür, dass auf einer Seite, die sichtbar eine Tabelle enthält, die Meldung „Es wurden keine Tabellen gefunden“ erscheint.

Es behandelt „spanning“ korrekt. Die Attribute „colspan“ und „rowspan“ „werden korrekt verarbeitet“, was mehr ist, als viele selbst entwickelte Parser leisten.

Es bevorzugt „<thead>“ für Überschriften und greift auf den Textkörper zurück, wenn dort keine vorhanden ist.

Es berücksichtigt standardmäßig „display: none“. Die Standardeinstellung „displayed_only=True“ „schließt Elemente mit display: none aus“, was in der Regel gewünscht ist und gelegentlich Daten verbirgt, die eine Website bewusst nur für bestimmte Viewports zur Verfügung gestellt hat.

Zwei Hinweise zu den Abhängigkeiten: Die Parser-Engines sind zunächst „lxml“, mit einem Fallback auf „bs4“ sowie „html5lib“ – in der Dokumentation wird darauf hingewiesen, dass „‚bs4‘ und ‚html5lib‘ als Variantenbezeichnungen synonym sind“. Außerdem gibt es eine URL-Eigenart, die man kennen sollte: „lxml akzeptiert nur die URL-Protokolle http, ftp und file. Wenn Sie eine URL haben, die mit 'https' beginnt, können Sie versuchen, das 's' zu entfernen.“ In der Praxis lässt sich dies vollständig vermeiden, indem man die Seite selbst abruft.

Die wichtigen Parameter

Die vollständige Signatur umfasst achtzehn Parameter. Sechs davon übernehmen den Großteil der Arbeit.

**match

** (Standard: '.+'

) filtert Tabellen heraus, deren Text einem regulären Ausdruck entspricht. Dies ist der nützlichste Parameter überhaupt, der jedoch viel zu selten genutzt wird:

tables = pd.read_html(html, match="Population")

Anstatt einen Index in einer Liste zu erraten, geben Sie einfach etwas an, das die Tabelle enthält. Dies ist weitaus robuster, wenn sich das Layout einer Seite ändert, da der Inhalt in der Regel auch bei einer Neugestaltung erhalten bleibt, bei der die Position der Tabelle verschoben wird.

**attrs

** filtert nach HTML-Attributen, was die andere Möglichkeit darstellt, eine bestimmte Tabelle zu identifizieren:

tables = pd.read_html(html, attrs={"id": "results", "class": "data"})

**header

** gibt die Kopfzeile an. Die Dokumentation weist auf ein Detail zur Reihenfolge hin, das vielen Nutzern entgeht: „Das Argument ``header`

wird **nach** der Anwendung von ``skiprows

angewendet“. Wenn Sie also zwei Zeilen überspringen und dann nach ``header=0

` fragen, erhalten Sie die erste Zeile nach dem Überspringen.

**skiprows

** entfernt Zeilen vor dem Parsen – nützlich für Tabellen mit Titelzeilen oberhalb der eigentlichen Kopfzeile.

**index_col

** legt eine Spalte als Index fest.

**thousands

** (Standard: ','

) und **decimal

** (Standard: '.'

) regeln die numerische Formatierung. Diese sind wichtiger, als es den Anschein hat: Eine europäische Tabelle, die 1.234,56

verwendet, benötigt thousands='.'

und decimal=','

; ohne diese wird jede Zahl stillschweigend zu einer Zeichenkette oder einem falschen Wert.

Zwei weitere wissenswerte Funktionen:

**converters

** wendet beim Parsen eine Funktion pro Spalte an, was übersichtlicher ist, als die Typen nachträglich zu korrigieren.

**extract_links

** erfasst die href

von Links innerhalb von Zellen und nicht nur deren Text – wirklich nützlich, wenn die Zeilen der Tabelle auf Detailseiten verweisen, die Sie ebenfalls anzeigen möchten.

Die Aufräumarbeit, der sich niemand entziehen kann

Die Dokumentation stellt die Erwartungen ehrlich dar, und es lohnt sich, den Haftungsausschluss zu lesen, anstatt ihn erst später zu entdecken:

Rechnen Sie damit, dass Sie nach dem Aufruf dieser Funktion etwas Aufräumarbeit leisten müssen. Beispielsweise müssen Sie möglicherweise Spaltennamen manuell zuweisen, wenn diese beim Übergeben des Arguments „header=0

“ in NaN umgewandelt werden.

Und:

Wir versuchen, so wenig wie möglich über die Struktur der Tabelle anzunehmen, und überlassen es dem Nutzer, sich mit den Eigenheiten des in der Tabelle enthaltenen HTMLs auseinanderzusetzen.

Dieser zweite Satz bringt die Designphilosophie klar auf den Punkt. ``read_html`

` liefert Ihnen den Inhalt des HTMLs; es ist Ihre Aufgabe, diesen zu bereinigen.

Die Bereinigung, die jedes Mal anfällt:

MultiIndex-Spalten aus mehrzeiligen Kopfzeilen. Eine Tabelle mit einer zweizeiligen Kopfzeile erzeugt ein „MultiIndex

“, was zwar korrekt, aber unschön ist. Glätten Sie es:

df.columns = [" ".join(str(c) for c in col).strip() for col in df.columns]

Leerzeichen und geschützte Leerzeichen. HTML ist voll von &nbsp;

, was als \xa0

ausgegeben wird und eine einfache .strip()

zunichte macht:

df = df.replace("\xa0", " ", regex=True)
df.columns = df.columns.str.replace("\xa0", " ", regex=False).str.strip()

Zahlen, die Zeichenfolgen sind. Währungssymbole, Prozentzeichen und Fußnotenmarkierungen:

df["Price"] = (df["Price"].astype(str)
               .str.replace(r"[^\d.,-]", "", regex=True)
               .str.replace(",", "")
               .pipe(pd.to_numeric, errors="coerce"))

errors="coerce"

wandelt nicht parsbare Werte in NaN

um, anstatt einen Fehler auszulösen. So können Sie zählen, wie viele Fehler aufgetreten sind, anstatt den gesamten Vorgang wegen einer fehlerhaften Zelle zu verlieren.

Fußnotenzeilen und Summen. Viele Tabellen enden mit einer Zusammenfassungszeile, die keine Daten enthält. Filtern Sie diese explizit heraus, anstatt davon auszugehen, dass die letzte Zeile unbedenklich ist.

Die richtige Tabelle auswählen

Vier Ansätze, in aufsteigender Reihenfolge ihrer Robustheit.

Nach Index — tables[0]

. Gut zur Erkundung geeignet, aber in einem Skript anfällig. Eine neue Tabelle, die über Ihrer hinzugefügt wird, führt zu einem stillen Fehler, da der Index 0 weiterhin existiert und nun etwas anderes enthält.

**Nach „match

“** — die beste Standardeinstellung. Geben Sie eine Zeichenkette an, die ausschließlich in der gewünschten Tabelle vorkommt.

**Nach „attrs

“** — am besten geeignet, wenn die Tabelle eine ID oder eine eindeutige Klasse hat, da diese bewusst vom Entwickler gewählt wurden.

Nach Form, nach dem Laden — wenn nichts anderes die Tabelle identifiziert:

candidates = [t for t in pd.read_html(html)
              if {"Name", "Price"}.issubset(t.columns)]
if len(candidates) != 1:
    raise ValueError(f"expected 1 matching table, found {len(candidates)}")
df = candidates[0]

Diese Überprüfung ist der entscheidende Teil. Ein Skript, das stillschweigend die erste von drei Übereinstimmungen übernimmt, wird monatelang falsche Daten liefern. Ein Fehlschlag bei unerwarteter Anzahl verwandelt ein Datenproblem in eine Fehlermeldung.

Mehrere Seiten in einen DataFrame einlesen

Der logische nächste Schritt, sobald eine einzelne Tabelle funktioniert – und hier können ein paar Gewohnheiten echte Probleme ersparen.

Verketten Sie die Daten, anstatt sie in einer Schleife anzuhängen. Das schrittweise Erstellen eines DataFrame ist langsam und führt zu einem fragmentierten Index. Sammeln Sie die Frames und führen Sie sie anschließend einmalig zusammen:

frames = []
for page in range(1, 11):
    df = fetch_table(f"https://example.com/data?page={page}",
                     match="Population",
                     expected_columns=["Country", "Population"])
    df["source_page"] = page
    frames.append(df)

combined = pd.concat(frames, ignore_index=True)

Speichern Sie, woher jede Zeile stammt. Die oben genannte Spalte „source_page

“ kostet nichts und beantwortet die Frage, die Ihnen irgendwann gestellt wird – welche Seite hat diesen seltsamen Wert erzeugt? Fügen Sie bei allem, was im Laufe der Zeit gesammelt wird, auch einen Zeitstempel hinzu. Ein Datensatz ohne Herkunftsangabe ist sehr schwer zu debuggen und lässt sich unmöglich prüfen.

Passen Sie das Tempo der Schleife an. Zehn Seiten, die so schnell abgerufen werden, wie es die Verbindung zulässt, sind ein Datenstoß, der aus Sicht des Servers wie ein Angriff aussieht. Eine Pause von einer Sekunde zwischen den Anfragen ist sowohl höflich als auch – auf den meisten Websites – der Unterschied zwischen erfolgreichem Abschluss und einer Ratenbegrenzung:

import time, random
time.sleep(1 + random.random())

**Behandeln Sie Fehler pro Seite, anstatt den Lauf abzubrechen.**Eine Seite, deren Layout sich geändert hat, sollte nicht dazu führen, dass Sie die anderen neun verlieren:

frames, failures = [], []
for page in range(1, 11):
    try:
        frames.append(fetch_table(url_for(page), match="Population", expected_columns=COLS))
    except Exception as exc:
        failures.append((page, str(exc)))

if failures:
    print(f"{len(failures)} pages failed:", failures)

Überprüfen Sie vor dem Verketten, ob die Strukturen übereinstimmen. Wenn Seite sieben eine Spalte enthält, die die anderen nicht haben, erzeugt pd.concat

ohne Weiteres einen Frame voller „NaN

“ für die nicht übereinstimmenden Zeilen – gültig, wohlgeformt und falsch. Wenn Sie die Spaltensätze vor dem Zusammenführen vergleichen, wird daraus ein Fehler, den Sie erkennen können.

Und entfernen Sie anschließend Duplikate. In paginierten Tabellen wiederholen sich Zeilen häufig über Seitengrenzen hinweg, insbesondere wenn sich die zugrunde liegenden Daten während des Crawls ändern. combined.drop_duplicates()

auf eine aussagekräftige Teilmenge von Spalten ist eine kostengünstige Maßnahme, um zu verhindern, dass derselbe Datensatz doppelt gezählt wird.

Wann man etwas anderes verwenden sollte „

read_html“ ist ein praktischer Wrapper. In vier Situationen ist ein anderes Tool angebracht.

Wenn die Daten nicht im Format „<table>“ vorliegen. Kartenlayouts, Definitionslisten, „<div>“-Raster. Verwenden Sie einen echten Parser – lxml oder BeautifulSoup mit CSS-Selektoren oder XPath – und erstellen Sie den DataFrame selbst:

from lxml import html as lh
tree = lh.fromstring(page)
rows = [{"name": c.cssselect("h3")[0].text_content().strip(),
         "price": c.cssselect(".price")[0].text_content().strip()}
        for c in tree.cssselect("div.product-card")]
df = pd.DataFrame(rows)

Wenn die Seite JavaScript benötigt. Rendern Sie sie zunächst mit Playwright oder einem ähnlichen Tool und übergeben Sie dann den gerenderten HTML-Code an read_html. Diese Kombination funktioniert gut und ist oft der kürzeste Weg:

html = page.content()          # rendered DOM, not source
tables = pd.read_html(html)

Wenn Sie die Anfrage steuern müssen. Header, Cookies, Sitzungen, Wiederholungsversuche, Timeouts, Proxys – nichts davon macht „read_html“ sichtbar, wenn es die Daten für Sie abruft. Rufen Sie die Daten separat ab und übergeben Sie die Zeichenkette.

Wenn die Website strukturierte Daten anbietet. Ein CSV-Download, eine API oder in die Seite eingebettetes JSON-LD. Jede dieser Möglichkeiten ist hinsichtlich der Stabilität besser als das Parsen von gerenderten HTML-Daten, und es lohnt sich, dies vor dem Schreiben von Code dreißig Sekunden lang zu prüfen.

Die richtige Vorgehensweise in einem Skript

Das Muster für alles, was mehr als einmal ausgeführt wird.

import pandas as pd
import requests

HEADERS = {"User-Agent": "AcmeDataBot/1.0 (+https://acme.example.com/bot)"}

def fetch_table(url, match, expected_columns):
    resp = requests.get(url, headers=HEADERS, timeout=30)
    resp.raise_for_status()

    tables = pd.read_html(resp.text, match=match)
    if len(tables) != 1:
        raise ValueError(f"{url}: expected 1 table matching {match!r}, got {len(tables)}")

    df = tables[0]
    df.columns = [str(c).replace("\xa0", " ").strip() for c in df.columns]

    missing = set(expected_columns) - set(df.columns)
    if missing:
        raise ValueError(f"{url}: missing columns {missing}; got {list(df.columns)}")

    if df.empty:
        raise ValueError(f"{url}: table matched but contains no rows")

    return df

Fünf Punkte darin machen den Unterschied zwischen einem Skript, das lautstark fehlschlägt, und einem, das stillschweigend fehlschlägt.

**raise_for_status()

** fängt HTTP-Fehler ab, da read_html

auf einer Fehlerseite entweder keine Tabellen oder die falschen Tabellen findet.

**match

statt eines Index**, sodass eine Layoutänderung einen Fehler auslöst, anstatt die falsche Tabelle zu liefern.

Die Überprüfung auf genau eine Übereinstimmung, sodass ein mehrdeutiges Ergebnis gestoppt wird, anstatt stillschweigend das erste Ergebnis zu übernehmen.

Überprüfung der erwarteten Spalten, wodurch eine Neugestaltung erkannt wird, bei der diese umbenannt oder neu angeordnet werden – ein Fehler, der andernfalls auf unbestimmte Zeit wohlgeformte, aber falsche Daten erzeugt.

Überprüfung auf Leere, da eine übereinstimmende Tabelle ohne Zeilen fast immer ein Symptom und nicht das Ergebnis ist.

Ein ehrlicher User-Agent mit einer Kontakt-URL kostet nichts und macht Sie zu einem Client, den ein Betreiber zulassen kann, anstatt ihn blockieren zu müssen.

Häufig gestellte Fragen

Was macht „pandas.read_html“?

Die Funktion analysiert HTML und gibt jedes gefundene „<table>“-Element als Liste von DataFrames zurück. Sie verarbeitet „colspan“ und „rowspan“, verwendet „<thead>“ für vorhandene Kopfzeilen und schließt standardmäßig Elemente aus, die mit „display: none“ ausgeblendet sind.

Warum gibt read_html eine Liste zurück?

Weil eine Seite eine beliebige Anzahl von Tabellen enthalten kann und pandas alle in der Reihenfolge des Dokuments zurückgibt. Die Funktion gibt immer eine Liste zurück oder schlägt fehl – sie gibt nur in Ausnahmefällen eine leere Liste zurück –, sodass ein leeres Ergebnis selbst ein Hinweis darauf ist, dass etwas schiefgelaufen ist.

Warum meldet „read_html“ „Keine Tabellen gefunden“?

Entweder enthält die Seite keine „<table>“-Elemente – moderne Layouts verwenden stattdessen häufig gestylte „<div>“-Elemente – oder die Tabelle wird durch JavaScript gerendert, das pandas nicht ausführt. Überprüfen Sie den rohen HTML-Quellcode anstelle der vom Browser gerenderten Ansicht, um festzustellen, welcher Fall vorliegt.

Wie wähle ich eine bestimmte Tabelle aus?

Verwenden Sie „match“ mit einem regulären Ausdruck, der auf Text innerhalb der gewünschten Tabelle passt, oder „attrs“, um nach einer ID oder Klasse zu filtern. Beide Methoden sind weitaus robuster als das Indizieren der Liste, was stillschweigend fehlschlägt, wenn eine Tabelle über Ihrer hinzugefügt wird.

Funktioniert read_html mit JavaScript-gerenderten Seiten?

Nein. Es analysiert HTML und führt keine Skripte aus. Rendern Sie die Seite zunächst mit einem Browser-Automatisierungstool und übergeben Sie dann die resultierende HTML-Zeichenkette an read_html – diese Kombination funktioniert gut und ist in der Regel der kürzeste Weg.

Wie bereinige ich den DataFrame anschließend?

Rechnen Sie damit, Spalten mit dem Format „MultiIndex“ aus übergreifenden Kopfzeilen zu glätten, geschützte Leerzeichen, die als „\xa0“ vorliegen, zu entfernen, Währungs- und Prozentzeichenfolgen mit „pd.to_numeric(errors="coerce")“ in Zahlen umzuwandeln sowie Fußnoten- oder Summenzeilen zu entfernen. In der pandas-Dokumentation wird ausdrücklich darauf hingewiesen, dass eine Bereinigung erforderlich ist.

Kann ich mit read_html einen Proxy oder benutzerdefinierte Header verwenden?

Nicht, wenn die Funktion die URL selbst abruft – sie stellt keine Anfrageoptionen zur Verfügung. Rufen Sie die Seite mit requests oder einem anderen Client ab, bei dem Sie Header, Timeouts, Sitzungen und Proxys steuern können, und übergeben Sie dann die HTML-Zeichenkette an read_html.

Was bewirken die Parameter „thousands“ und „decimal“?

Sie teilen pandas mit, wie Zahlen formatiert werden sollen; die Standardwerte sind „','“ bzw. „'.'“. Für europäische Formatierungen wie „1.234,56“ benötigen Sie „thousands='.'“ und „decimal=','“ – ohne diese werden Werte stillschweigend falsch geparst oder als Zeichenketten belassen.

Fazit „

read_html“ ist eine wirklich gute Hilfsfunktion mit einem engen Anwendungsbereich: Sie findet „<table>“-Elemente im übergebenen HTML-Code und wandelt sie in DataFrames um. Innerhalb dieses Anwendungsbereichs bewältigt sie die kniffligen Aspekte – übergreifende Zellen, Erkennung von Kopfzeilen, versteckte Elemente – besser als die meisten handgeschriebenen Parser.

Zwei Dinge sollte man sich verinnerlichen: Erstens gibt die Funktion eine Liste statt eines DataFrame zurück, und zweitens weist die Dokumentation selbst darauf hin, dass mit einer Bereinigung zu rechnen ist. Wenn man statt über den Index über match oder attrs auswählt und sicherstellt, dass genau eine Tabelle übereinstimmt, wird der häufigste stillschweigende Fehler in eine Fehlermeldung umgewandelt.

Für alles, was mehr als einmal ausgeführt wird, solltest du den HTML-Code selbst abrufen. Eine zusätzliche Zeile verschafft dir Header, Timeouts, Wiederholungsversuche, Sitzungen und alles andere, was die integrierte fetch-Funktion vorenthält – und umgeht gleichzeitig die Eigenart des lxml-URL-Protokolls.

Und wenn die Seite keine Tabellen enthält, hören Sie auf, nach Parametern zu suchen. Ein „<div>“-Raster oder eine vom Client gerenderte Seite ist ein anderes Problem, und die Lösung ist ein echter Parser, ein Rendering-Schritt oder – am besten – die strukturierten Daten, die die Website wahrscheinlich bereits an einer Stelle veröffentlicht, an der Sie noch nicht nachgeschaut haben.