Wikipedia ist die häufigste Quelle für Webtabellendaten. Gleichzeitig ist es ein Minenfeld an Edge Cases, die naive Scraper zum Absturz bringen.
Ich habe die fünf Muster gesammelt, die beim Entwickeln des HTML Table Exporter die meisten Probleme verursachen — mit Erkennungscode und Lösungen für jedes Muster.
Muster 1: Navigationszeilen („v t e")
Das Problem:
<table>
<tr>
<td colspan="5">v t e Länder nach Einwohnerzahl</td>
</tr>
<tr>
<td>Rang</td><td>Land</td><td>Einwohner</td>...
</tr>
...
</table>
Die erste Zeile enthält „v t e"-Links (View/Talk/Edit) zu Wikipedia-Vorlagenseiten. Wenn der Scraper Zeile 0 als Header interpretiert, geht alles schief.
Was pd.read_html liefert:
v t e Länder nach Einwohnerzahl
0 Rang Land ...
1 1 China ...
Erkennung:
def is_nav_row(row_values):
"""Wikipedia-Navigationspräfix erkennen."""
if not row_values:
return False
first_cell = str(row_values[0]).strip().lower()
patterns = [
r'^v\s+t\s+e\s', # "v t e "
r'^v\s*\|\s*t\s*\|\s*e', # "v | t | e"
r'^\[v\]\s*\[t\]\s*\[e\]' # "[v] [t] [e]"
]
import re
return any(re.match(p, first_cell) for p in patterns)
Lösung:
import pandas as pd
def read_wikipedia_table(url, table_index=0):
tables = pd.read_html(url)
df = tables[table_index]
# Prüfen ob erste Zeile Navigation ist
if is_nav_row(df.iloc[0].values):
# Zweite Zeile als Header verwenden
df.columns = df.iloc[1]
df = df.iloc[2:].reset_index(drop=True)
return df
Muster 2: Horizontal duplizierte Tabellen
Das Problem:
Um vertikalen Platz zu sparen, zeigt Wikipedia manche Tabellen in mehreren Spalten an:
| Rang | Name | Einw. | Rang | Name | Einw. |
|------|--------|-------|------|---------|-------|
| 1 | Tokio | 37M | 11 | Paris | 11M |
| 2 | Delhi | 32M | 12 | Kairo | 10M |
Das ist logisch EINE Tabelle mit wiederholter Spaltenstruktur.
Was pd.read_html liefert:
Rang Name Einw. Rang.1 Name.1 Einw..1
0 1 Tokio 37M 11 Paris 11M
1 2 Delhi 32M 12 Kairo 10M
Pandas sieht 6 Spalten. Wenn man nach „Name" filtert, fehlt die Hälfte der Daten.
Erkennung:
def detect_horizontal_duplication(columns):
"""Prüfen ob Spalten sich wiederholen (Rang, Name, Einw., Rang, Name, Einw.)."""
cols = list(columns)
n = len(cols)
# Durch 2, 3, 4 teilen versuchen
for divisor in [2, 3, 4]:
if n % divisor != 0:
continue
chunk_size = n // divisor
base_pattern = [c.rstrip('.0123456789') for c in cols[:chunk_size]]
is_duplicate = True
for i in range(1, divisor):
chunk = cols[i * chunk_size : (i + 1) * chunk_size]
normalized = [c.rstrip('.0123456789') for c in chunk]
if normalized != base_pattern:
is_duplicate = False
break
if is_duplicate:
return chunk_size
return None
Lösung:
def normalize_duplicated_table(df, base_columns):
"""Horizontal duplizierte Tabellen vertikal stapeln."""
n_repeats = len(df.columns) // base_columns
frames = []
for i in range(n_repeats):
start = i * base_columns
end = start + base_columns
chunk = df.iloc[:, start:end].copy()
chunk.columns = df.columns[:base_columns]
# Zeilen entfernen wo alle Werte NaN sind (leere zweite Hälfte)
chunk = chunk.dropna(how='all')
frames.append(chunk)
return pd.concat(frames, ignore_index=True)
# Verwendung
df = pd.read_html(url)[0]
chunk_size = detect_horizontal_duplication(df.columns)
if chunk_size:
df = normalize_duplicated_table(df, chunk_size)
Muster 3: Titelzeilen (über alle Spalten)
Das Problem:
<table>
<tr>
<td colspan="4">Liste der höchsten Gebäude der Welt</td>
</tr>
<tr>
<td>Rang</td><td>Gebäude</td><td>Stadt</td><td>Höhe</td>
</tr>
...
</table>
Die erste Zeile ist ein Titel, keine Daten. Nach colspan-Expansion wird daraus:
['Liste der höchsten...', 'Liste der höchsten...', 'Liste der höchsten...', 'Liste der höchsten...']
Erkennung:
def is_title_row(row_values, next_row_values):
"""Titelzeilen über die volle Breite erkennen."""
if not row_values or not next_row_values:
return False
# Alle Werte sind gleich (colspan expandiert)
unique_values = set(str(v).strip() for v in row_values if str(v).strip())
# Titelzeile: 1 einzigartiger Wert, nächste Zeile hat mehrere
# Und der Wert ist lang (> 20 Zeichen typisch für Titel)
if len(unique_values) == 1:
title = list(unique_values)[0]
next_unique = len(set(str(v).strip() for v in next_row_values if str(v).strip()))
return len(title) > 20 and next_unique > 2
return False
Lösung:
def skip_title_rows(df):
"""Titelzeilen am Anfang eines DataFrames entfernen."""
skip_count = 0
for i in range(min(3, len(df) - 1)):
current_row = df.iloc[i].values
next_row = df.iloc[i + 1].values if i + 1 < len(df) else None
if is_title_row(current_row, next_row):
skip_count = i + 1
else:
break
if skip_count > 0:
# Zeile nach Titeln als Header verwenden
df.columns = df.iloc[skip_count]
df = df.iloc[skip_count + 1:].reset_index(drop=True)
return df
Muster 4: Gruppierte Header (Zweistufig)
Das Problem:
| | | Statistiken | Statistiken |
| Rang | Land | BIP (nominal) | BIP (KKP) |
|--------|---------|-------------------|------------------|
| 1 | USA | 25,5 Billionen | 25,5 Billionen |
Zeile 0 sind Kategorie-Header. Zeile 1 sind die eigentlichen Spaltenheader. Beide sind semantisch „Header".
Was pd.read_html liefert:
Oft beschädigt oder mit einem MultiIndex, der umständlich zu handhaben ist.
Erkennung:
def has_grouped_headers(df):
"""Zweistufige gruppierte Header erkennen."""
if len(df) < 3:
return False
row0 = df.iloc[0].values
row1 = df.iloc[1].values
# Aufeinanderfolgende gleiche Werte in Zeile 0 zählen
repeat_count = 0
for i in range(1, len(row0)):
if str(row0[i]).strip() == str(row0[i-1]).strip() and str(row0[i]).strip():
repeat_count += 1
repeat_ratio = repeat_count / max(1, len(row0) - 1)
# Gruppierte Header haben typischerweise 40%+ wiederholte Werte
# UND Zeile 1 hat mehr einzigartige nicht-leere Werte als Zeile 0
unique0 = len(set(str(v).strip() for v in row0 if str(v).strip()))
unique1 = len(set(str(v).strip() for v in row1 if str(v).strip()))
return repeat_ratio > 0.3 and unique1 > unique0
Lösung:
def merge_grouped_headers(df):
"""Zweistufige Header zu einer einzelnen Ebene zusammenführen."""
group_row = df.iloc[0].values
header_row = df.iloc[1].values
merged = []
for i, (group, header) in enumerate(zip(group_row, header_row)):
g = str(group).strip()
h = str(header).strip()
if not g or g == h:
merged.append(h)
elif not h:
merged.append(g)
else:
merged.append(f"{g} - {h}")
df.columns = merged
return df.iloc[2:].reset_index(drop=True)
# Verwendung
if has_grouped_headers(df):
df = merge_grouped_headers(df)
Muster 5: Verschachtelte Infobox-Tabellen
Das Problem:
Wikipedia-Infoboxen enthalten Tabellen innerhalb von Tabellenzellen:
<table class="infobox">
<tr>
<td>Einwohner</td>
<td>
<table> <!-- Verschachtelt! -->
<tr><td>Stadt</td><td>8,3 Mio.</td></tr>
<tr><td>Metropole</td><td>20,1 Mio.</td></tr>
</table>
</td>
</tr>
</table>
Was pd.read_html liefert:
Sowohl die äußere als auch die innere Tabelle werden zurückgegeben. Bei der Suche nach „allen Tabellen auf der Seite" erhält man Duplikate und verschachtelten Datenmüll.
Erkennung und Filterung:
from bs4 import BeautifulSoup
import requests
def get_top_level_tables(url):
"""Nur Top-Level-Tabellen abrufen, keine verschachtelten."""
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
all_tables = soup.find_all('table')
top_level = []
for table in all_tables:
# Prüfen ob diese Tabelle in einer anderen steckt
parent = table.parent
is_nested = False
while parent:
if parent.name == 'table':
is_nested = True
break
parent = parent.parent
if not is_nested:
top_level.append(table)
return top_level
def read_top_level_tables(url):
"""Nur Top-Level-Tabellen als DataFrames lesen."""
import pandas as pd
tables = get_top_level_tables(url)
dfs = []
for table in tables:
try:
# Einzelne Tabelle in DataFrame konvertieren
df = pd.read_html(str(table))[0]
dfs.append(df)
except Exception:
continue
return dfs
Kompletter Wikipedia-Tabellenleser
Alle Lösungen kombiniert:
import pandas as pd
import requests
from bs4 import BeautifulSoup
import re
class WikipediaTableReader:
def __init__(self, url):
self.url = url
self.soup = None
def _fetch(self):
if self.soup is None:
response = requests.get(self.url)
self.soup = BeautifulSoup(response.text, 'html.parser')
def _is_nav_row(self, values):
if not values:
return False
first = str(values[0]).strip().lower()
return bool(re.match(r'^v\s+t\s+e\s', first))
def _is_title_row(self, values, next_values):
unique = set(str(v).strip() for v in values if str(v).strip())
if len(unique) != 1:
return False
title = list(unique)[0]
next_unique = len(set(str(v).strip() for v in next_values if str(v).strip()))
return len(title) > 20 and next_unique > 2
def get_tables(self, skip_infobox=True):
"""Alle Datentabellen der Seite abrufen."""
self._fetch()
tables = self.soup.find_all('table')
results = []
for table in tables:
# Verschachtelte Tabellen überspringen
if table.find_parent('table'):
continue
# Infoboxen überspringen wenn gewünscht
if skip_infobox and 'infobox' in table.get('class', []):
continue
try:
df = pd.read_html(str(table))[0]
df = self._clean_table(df)
if len(df) > 0 and len(df.columns) > 1:
results.append(df)
except Exception:
continue
return results
def _clean_table(self, df):
"""Alle Bereinigungsschritte anwenden."""
# Navigationszeilen überspringen
while len(df) > 0 and self._is_nav_row(df.iloc[0].values):
df.columns = df.iloc[1] if len(df) > 1 else df.columns
df = df.iloc[2:].reset_index(drop=True) if len(df) > 2 else df.iloc[1:]
# Titelzeilen überspringen
if len(df) > 1:
while self._is_title_row(df.iloc[0].values, df.iloc[1].values if len(df) > 1 else []):
df.columns = df.iloc[1]
df = df.iloc[2:].reset_index(drop=True)
return df
# Verwendung
reader = WikipediaTableReader("https://de.wikipedia.org/wiki/Liste_der_Staaten_der_Erde")
tables = reader.get_tables()
Wann man stattdessen eine Erweiterung verwenden sollte
Wenn man Ad-hoc-Extraktion betreibt (keine Pipeline baut), behandelt eine Browser-Erweiterung all diese Muster automatisch.
HTML Table Exporter erkennt diese Muster und normalisiert die Ausgabe. Ein Klick statt Edge-Case-Debugging.
Einen praktischen Überblick über die besten Erweiterungen dafür finden Sie in unserem Vergleich der besten Chrome-Erweiterungen zum Exportieren von Tabellen.
Für automatisierte Pipelines verwenden Sie den obigen Code. Für gelegentliche Exporte nutzen Sie das richtige Werkzeug für den Job.
Erfahren Sie mehr auf gauchogrid.com/de/html-table-exporter oder probieren Sie es kostenlos im Chrome Web Store aus.
Eine Wikipedia-Tabelle gefunden, die diesen Code zum Absturz bringt? Teilen Sie die URL — ich nehme sie in meine Testsuite auf.
Top comments (0)