Ihr Scraper ist über Nacht fertig geworden, und die CSV-Datei sieht auf den ersten Blick gut aus. Dann wollen Sie den Durchschnittspreis berechnen, und pandas verweigert den Dienst, weil jeder Preis ein Text wie £51.77 ist. Dasselbe Buch taucht zweimal auf, weil es auf einer Katalogseite und auf einer Kategorieseite stand. Ein Titel lautet Shakespeare’s, sieben Bücher stecken in einer Kategorie namens „Default“, und eine Zeile hat überhaupt keine Beschreibung. Nichts davon ist ein Fehler des Scrapers. So sehen rohe Webdaten aus, und bevor jemand daraus ein Diagramm zeichnet, brauchen sie einen Durchgang zur Datenbereinigung.
Dieser Leitfaden zeigt diesen Durchgang mit pandas an einem echten Datensatz, gescrapt von books.toscrape.com, einer Übungsseite, die eigens für Scraping gebaut wurde. Wir behandeln die Reparatur von Text und Unicode, fehlende Werte, die Umwandlung von Preistexten in Zahlen, weitere Zahlen, die in Text versteckt sind, Datumswerte, Kategorielabels, Duplikate, Ausreißer, Validierungsprüfungen und das Speichern des Ergebnisses. Das vollständige Skript lief am 28. September 2026 mit pandas 3.0.6, Requests 2.34.2, beautifulsoup4 4.15.0, pyarrow 25.0.1 und Python 3.13.
Was bedeutet „sauber“ bei gescrapten Daten?
Saubere Daten haben eine Zeile pro realem Objekt, einen Typ pro Spalte und eine Schreibweise pro Wert. Eine Preisspalte enthält Zahlen, keinen Text mit Währungszeichen. Ein zweimal gescraptes Buch erscheint einmal. Eine fehlende Beschreibung wird als fehlend erfasst, nicht als leerer String, der vorhanden wirkt. Eine Kategorie namens „Default“ bekommt einen Namen, der dem Analysten sagt, dass sie keine Information trägt.
Sauber heißt nicht vollständig oder in jedem Detail korrekt. Eine Beschreibung, die die Seite nie hatte, können Sie nicht zurückholen, und ein Bereinigungsskript sollte keine erfinden. Seine Aufgabe ist es, jedes Problem sichtbar und jeden verbleibenden Wert vertrauenswürdig zu machen, damit der nächste Schritt (ein Diagramm, ein Modell, ein Preisalarm) mit Fakten arbeitet statt mit Parsing-Unfällen.
Warum sind gescrapte Daten unordentlich?
Webseiten werden für Menschen geschrieben, und ein Scraper liest die Teile, die fürs Auge gedacht sind. Das meiste Durcheinander stammt aus wenigen Quellen:
- Alles ist Text. HTML kennt keine Typen.
£51.77,In stock (22 available)undThree(eine CSS-Klasse für eine Sternebewertung) kommen alle als Strings an. - Kodierungsschätzungen gehen schief. books.toscrape.com sendet
Content-Type: text/htmlohne Charset. In diesem Fall folgt Requests der alten HTTP/1.1-Regel und dekodiert die Seite als ISO-8859-1 (Requests-Dokumentation zu Kodierungen), sodass aus dem UTF-8-Pfundzeichen£wird. Den Mechanismus erklärt der Beitrag Python-Unicode-Kodierungsfehler. - Derselbe Artikel ist über mehrere Wege erreichbar. Ein Produkt steht im Katalog, in seiner Kategorie und manchmal in einer „Neu“- oder „Angebote“-Liste. Crawlen Sie mehr als eine davon, erfassen Sie es doppelt. Paginierung, die sich zwischen Läufen verschiebt, bewirkt dasselbe.
- Vorlagen unterscheiden sich. Einer von tausend Seiten fehlt ein Block, den die anderen haben, und Ihr Selektor liefert
None. - Die Daten der Website sind unvollkommen. Platzhalterkategorien, überzählige Leerzeichen und „...more“-Anhängsel gehören zur Quelle und sind nicht Ihre Fehler.
Wie läuft ein Bereinigungsdurchgang ab?
Die Reihenfolge zählt, denn jeder Schritt baut auf dem vorigen auf:
- Rohdaten behalten. Speichern Sie genau das, was der Scraper gesammelt hat, in
books_raw.csv. Die Bereinigung arbeitet auf einer Kopie, sodass Sie sie mit neuen Regeln wiederholen können, ohne erneut zu scrapen. - Text normalisieren. Leerzeichen an beiden Enden entfernen, leere Strings in fehlende Werte umwandeln, fehlerhafte Kodierung reparieren und Unicode-Normalisierung anwenden.
- Fehlende Werte zählen. Halten Sie sie in einem Bericht fest und fügen Sie Markierungsspalten hinzu; füllen Sie sie nicht mit Vermutungen.
- Preise umwandeln. Ziehen Sie das Währungssymbol in eine eigene Spalte und wandeln Sie den Rest in eine Gleitkommazahl um.
- Weitere Zahlen extrahieren. Lagerbestände aus
In stock (22 available), Bewertungen aus Wörtern, Rezensionszahlen aus Text. - Datumswerte parsen und in Zeitstempel mit Zeitzone umwandeln.
- Kategorien normalisieren, sodass ein Begriff genau ein Label hat.
- Duplikate entfernen, anhand eines stabilen Schlüssels und erst, wenn die Typen stimmen.
- Ausreißer markieren, mit einer einfachen Regel, die Sie erklären können.
- Validieren und speichern. Bei einer fehlgeschlagenen Prüfung anhalten, andernfalls CSV und Parquet schreiben.
Die Deduplizierung kommt bewusst spät. Zwei Zeilen, die sich nur durch ein angehängtes Leerzeichen oder einen falsch dekodierten Apostroph unterscheiden, sind dasselbe Buch, und drop_duplicates erkennt das erst nach den Schritten 2 bis 7.
Roh- und bereinigte Werte im Vergleich
Das sind echte Werte aus dem Scraping und das, was das Skript daraus macht:
| Spalte | Rohwert | Problem | Bereinigter Wert | pandas-Werkzeug |
|---|---|---|---|---|
price | £51.77 | Text, fehlerhafte Kodierung | 51.77 (float64) + currency = £ | str.extract, pd.to_numeric |
availability | In stock (22 available) | Zahl in einem Satz | in_stock = 22 (Int64) | str.extract |
rating | Three | Wort statt Zahl | 3 (Int64) | map |
title | Shakespeare’s Globe | Mojibake (Zeichensalat) | Shakespeare’s Globe | encode/decode, unicodedata |
category | Default | Platzhalterlabel | Uncategorized (category) | replace, astype("category") |
description | … and their affair begins. ...more | Seitenelement im Text | … and their affair begins. | str.removesuffix |
scraped_at | 2026-09-28T14:51:11+00:00 | String | datetime64[us, UTC] | pd.to_datetime |
upc | derselbe Code in zwei Zeilen | Doppeltes Buch | eine Zeile, neueste Kopie | drop_duplicates |
Was hat sich in pandas 3.0 geändert?
pandas 3.0.0 erschien am 21. Januar 2026, aktuell ist Version 3.0.6. Drei Änderungen aus den Release Notes zu pandas 3.0 betreffen Bereinigungscode direkt:
- Strings haben einen eigenen Typ. Textspalten werden jetzt als
strstatt alsobjecterkannt, und fehlende Werte darin sindNaN, wie bei den anderen Standardtypen. Deshalb wählt das Skript Textspalten mitselect_dtypes(include=["object", "string"])aus: Das funktioniert unter pandas 2 und 3. - Copy-on-Write ist der einzige Modus. Verkettete Zuweisungen wie
df["price"][0] = 10änderndfnicht mehr; pandas 3.0.6 gibt nur eine WarnungChainedAssignmentErroraus. Verwenden Siedf.loc[0, "price"] = 10. Die alteSettingWithCopyWarningist verschwunden, und die vorsorglichen.copy()-Aufrufe, mit denen man sie zum Schweigen brachte, sind nicht mehr nötig. - Geparste Datumswerte haben standardmäßig Mikrosekunden.
pd.to_datetimeliefert für Strings jetztdatetime64[us]statt Nanosekunden, und genau das sehen Sie in der Ausgabe unten.
pandas 3 setzt außerdem Python 3.11 oder neuer voraus. pd.to_numeric akzeptiert nur errors="raise" oder errors="coerce" (Referenz zu to_numeric); ältere Code-Schnipsel mit errors="ignore" schlagen fehl.
Das vollständige Skript: books.toscrape.com scrapen und bereinigen
Installieren Sie die Bibliotheken in einer virtuellen Umgebung. pyarrow wird für die Parquet-Datei benötigt:
python -m venv .venv
.venv/bin/pip install pandas requests beautifulsoup4 pyarrow # Windows: .venv\Scripts\pipDer Scraper liest zwei Katalogseiten und zwei Kategorieseiten (Poetry und Classics), insgesamt 78 Bücher, mit einer Pause von einer Sekunde zwischen den Anfragen und automatischen Wiederholungen bei 429- und 5xx-Antworten. Das HTML-Parsing folgt dem Ansatz aus unserem BeautifulSoup-Tutorial. Der zweite Lauf liest books_raw.csv und wiederholt nur die Bereinigung.
"""books.toscrape.com scrapen und das Ergebnis mit pandas bereinigen."""
import time
import unicodedata
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urljoin
import pandas as pd
import requests
from bs4 import BeautifulSoup
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
BASE = "https://books.toscrape.com/"
START_URLS = [
BASE + "catalogue/page-1.html", # Hauptkatalog
BASE + "catalogue/category/books/poetry_23/index.html", # eine Kategorie
BASE + "catalogue/category/books/classics_6/index.html", # noch eine
]
MAX_PAGES = 2 # Listenseiten pro Start-URL
DELAY = 1.0 # Sekunden zwischen Anfragen
RAW_FILE = Path("books_raw.csv")
session = requests.Session()
session.headers["User-Agent"] = "pandas-cleaning-tutorial/1.0 (contact: you@example.com)"
retry = Retry(total=4, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504])
session.mount("https://", HTTPAdapter(max_retries=retry))
# session.proxies = {"https": "http://user:pass@pr.proxynet.io:8000"}
def get(url):
time.sleep(DELAY)
resp = session.get(url, timeout=30)
resp.raise_for_status()
return resp.text # absichtlich mit der Schätzung von Requests dekodiert: siehe Schritt 2
def parse_book(url):
soup = BeautifulSoup(get(url), "html.parser")
table = {th.get_text(): td.get_text() for th, td in
zip(soup.select("table.table th"), soup.select("table.table td"))}
desc = soup.select_one("#product_description + p")
return {
"url": url,
"title": soup.h1.get_text(),
"category": soup.select("ul.breadcrumb a")[-1].get_text(),
"price": table.get("Price (incl. tax)"),
"availability": table.get("Availability"),
"rating": soup.select_one("p.star-rating")["class"][1],
"upc": table.get("UPC"),
"reviews": table.get("Number of reviews"),
"description": desc.get_text() if desc else None,
"scraped_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
def scrape():
rows = []
for start in START_URLS:
url = start
for _ in range(MAX_PAGES):
soup = BeautifulSoup(get(url), "html.parser")
for a in soup.select("article.product_pod h3 a"):
rows.append(parse_book(urljoin(url, a["href"])))
nxt = soup.select_one("li.next a")
if not nxt:
break
url = urljoin(url, nxt["href"])
df = pd.DataFrame(rows)
df.to_csv(RAW_FILE, index=False, encoding="utf-8")
return df
def clean(raw):
df = raw.copy()
report = {"raw_rows": len(df)}
# 1. Leerzeichen in jeder Textspalte entfernen; leere Strings werden zu fehlenden Werten
text_cols = df.select_dtypes(include=["object", "string"]).columns
for col in text_cols:
df[col] = df[col].str.strip().replace("", pd.NA)
# 2. Unicode: Mojibake reparieren, dann auf NFKC normalisieren
def fix_text(s):
if pd.isna(s):
return s
try:
s = s.encode("latin-1").decode("utf-8") # "£" -> "£", "é" -> "é"
except (UnicodeEncodeError, UnicodeDecodeError):
pass # bereits korrekt
return unicodedata.normalize("NFKC", s)
for col in ["title", "category", "description", "price"]:
df[col] = df[col].map(fix_text)
# 3. Fehlende Werte: zählen, markieren, nicht erfinden
report["missing_before"] = df.isna().sum()[lambda s: s > 0].to_dict()
df["description"] = df["description"].str.removesuffix("...more").str.strip()
df["has_description"] = df["description"].notna()
# 4. Preistexte in Zahlen umwandeln
df["currency"] = df["price"].str.extract(r"([£$€])", expand=False)
df["price"] = pd.to_numeric(
df["price"].str.replace(r"[^\d.]", "", regex=True), errors="coerce"
)
# 5. Weitere Zahlen, die im Text stecken
df["in_stock"] = pd.to_numeric(
df["availability"].str.extract(r"(\d+)\s+available", expand=False),
errors="coerce",
).astype("Int64")
df["rating"] = df["rating"].map(
{"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
).astype("Int64")
df["reviews"] = pd.to_numeric(df["reviews"], errors="coerce").astype("Int64")
# 6. Datumswerte
df["scraped_at"] = pd.to_datetime(df["scraped_at"], utc=True, errors="coerce")
# 7. Kategorien: eine Schreibweise pro Wert, Platzhalterlabels zusammengeführt
df["category"] = (df["category"]
.replace({"Default": "Uncategorized",
"Add a comment": "Uncategorized"})
.astype("category"))
# 8. Duplikate: gleiche UPC = gleiches Buch; die neueste Kopie behalten
report["duplicate_rows"] = int(df.duplicated("upc").sum())
df = (df.sort_values("scraped_at")
.drop_duplicates("upc", keep="last")
.reset_index(drop=True))
# 9. Ausreißer: markieren, nicht löschen
q1, q3 = df["price"].quantile([0.25, 0.75])
iqr = q3 - q1
df["price_outlier"] = ~df["price"].between(q1 - 1.5 * iqr, q3 + 1.5 * iqr)
report["price_outliers"] = int(df["price_outlier"].sum())
df = df.drop(columns=["availability"])
report["clean_rows"] = len(df)
return df, report
def validate(df):
problems = []
if df["upc"].duplicated().any():
problems.append("duplicate UPCs")
if df["price"].isna().any():
problems.append("prices that did not parse")
if not df["price"].dropna().between(0, 1000).all():
problems.append("prices outside 0-1000")
if not df["rating"].between(1, 5).fillna(False).all():
problems.append("ratings missing or outside 1-5")
if df["title"].isna().any():
problems.append("rows without a title")
if problems:
raise ValueError("Validation failed: " + ", ".join(problems))
if __name__ == "__main__":
raw = pd.read_csv(RAW_FILE) if RAW_FILE.exists() else scrape()
clean_df, report = clean(raw)
validate(clean_df)
clean_df.to_csv("books_clean.csv", index=False, encoding="utf-8-sig")
clean_df.to_parquet("books_clean.parquet", index=False)
print(report)
print(clean_df.dtypes)
print(clean_df[["title", "category", "price", "rating", "in_stock"]].head())Der erste Lauf dauerte etwa zwei Minuten, das meiste davon die höfliche Pause. Die ausgegebene Berichtszeile:
{'raw_rows': 78, 'missing_before': {'description': 1}, 'duplicate_rows': 5, 'price_outliers': 0, 'clean_rows': 73}Fünf Poetry-Bücher standen sowohl auf den Katalogseiten als auch auf der Poetry-Seite, deshalb wurden aus 78 Zeilen 73. Ein Classics-Titel, Alice in Wonderland, hat auf seiner Seite keinen Beschreibungsblock. Sieben Bücher trugen die Kategorie „Default“. Die Preise reichten von 12,84 bis 58,63, daher markierte die Ausreißerregel nichts, und das ist für diese Website die richtige Antwort. Die Ausgabe von dtypes zeigt str für Text, float64 für den Preis, Int64 für Zählwerte, category für die Kategorie und datetime64[us, UTC] für den Zeitstempel.
Was jeder Schritt tut und warum
Text und Unicode
str.strip() entfernt die Leerzeichen und Zeilenumbrüche, die das HTML-Layout um Werte herum hinterlässt. Leere Strings "" in fehlende Werte umzuwandeln ist wichtiger, als es aussieht: isna() zählt leere Strings nicht mit, ohne diesen Schritt gilt ein leeres Feld also als gefüllt.
Die Kodierungsreparatur macht die falsche Schätzung rückgängig. s.encode("latin-1").decode("utf-8") nimmt die Zeichen, die Requests erzeugt hat, wandelt sie in die ursprünglichen Bytes zurück und dekodiert diese Bytes als UTF-8. Ein String, der bereits korrekt war, bleibt entweder unverändert (reines ASCII) oder löst in einem der beiden Aufrufe einen Fehler aus und wird dann in Ruhe gelassen. Die sauberere Lösung setzt an der Quelle an: Übergeben Sie response.content an BeautifulSoup oder setzen Sie response.encoding = "utf-8", bevor Sie response.text lesen. Die Reparatur lohnt sich trotzdem für bereits gespeicherte Dateien. Danach führt unicodedata.normalize("NFKC", s) Kompatibilitätszeichen wie Ligaturen und geschützte Leerzeichen auf ihre einfachen Formen zurück (Dokumentation zu unicodedata), sodass zwei Schreibweisen desselben Titels als gleich gelten.
Fehlende Werte
Das Skript zählt fehlende Werte pro Spalte und fügt eine Markierung has_description hinzu. Es schreibt nicht „No description“ in die Lücke, denn dieser String sähe später wie echter Text aus. Bei Zahlen gehört das Auffüllen mit Mittelwert oder Median in den Analyseschritt, wo die Analystin es im Diagramm kenntlich machen kann, nicht in die Bereinigung.
Zahlen, Bewertungen und Datumswerte
pd.to_numeric(..., errors="coerce") macht aus allem, was es nicht parsen kann, NaN, statt abzubrechen. Das ist der richtige Standard für einen Bereinigungsdurchgang, sofern eine Validierungsprüfung diese NaN-Werte anschließend zählt. Der nullbare Typ Int64 (mit großem I) hält ganze Zahlen ganzzahlig, auch wenn ein Wert fehlt; das einfache int64 kann kein NaN aufnehmen und würde stillschweigend zu float. Datumswerte werden mit utc=True geparst, damit Zeitstempel aus verschiedenen Läufen und von verschiedenen Rechnern korrekt vergleichbar sind.
Kategorien und Duplikate
astype("category") speichert jedes Label einmal und hält pro Zeile nur einen kleinen Code. Neben der Speicherersparnis macht das die Liste der Labels explizit: df["category"].cat.categories zeigt auf einen Blick, ob „Poetry“ und „poetry “ beide überlebt haben.
Wählen Sie für Duplikate einen Schlüssel, der das Objekt identifiziert, nicht die Zeile. Hier ist das die UPC, die auf jeder Buchseite steht. Die URL würde auf dieser Website auch funktionieren, doch in echten Shops hat dasselbe Produkt oft mehrere URLs (Tracking-Parameter, Kategoriepfade). Wenn Sie vor drop_duplicates(keep="last") nach scraped_at sortieren, bleibt die neueste Kopie erhalten, und genau das wollen Sie, wenn sich Preise zwischen Läufen ändern.
Ausreißer und Validierung
Die Regel des Interquartilsabstands (IQR) markiert Werte, die mehr als das 1,5-Fache des IQR unter dem ersten oder über dem dritten Quartil liegen. Sie ist leicht zu erklären und setzt keine Normalverteilung voraus. Das Skript fügt eine Spalte price_outlier hinzu, statt Zeilen zu löschen: Ein Preis von 0,99 in einem echten Shop kann ein echtes Sonderangebot sein oder ein Parsing-Fehler, der 0.99 aus 10.99 gelesen hat. Bevor etwas gelöscht wird, sollte ein Mensch draufschauen.
validate() ist die letzte Verteidigungslinie. Als wir einen Rohpreis auf N/A und eine Bewertung auf Six änderten, brach der Lauf mit Validation failed: prices that did not parse, ratings missing or outside 1-5 ab, statt eine fehlerhafte Datei zu schreiben. Orientieren Sie diese Prüfungen an den tatsächlichen Regeln Ihrer Daten; eine Prüfung, die nie fehlschlagen kann, schützt nichts.
Wo bereinigte Scraping-Daten eingesetzt werden
- Preisüberwachung: Numerische Preise mit Zeitstempel sind das, was eine Preishistorie und ein Alarm brauchen. Siehe Preisüberwachung und Wettbewerberpreise verfolgen.
- Marktforschung: Kategorieanteile, Verteilungen von Bewertungen und Lagerbestände über einen ganzen Katalog. Siehe Marktforschung.
- Data Mining und Modellierung: Modelle brauchen typisierte, deduplizierte Eingaben. Hintergrund im Beitrag Was ist Data Mining?.
- Pipelines: Dieses Skript ist der Transform-Schritt eines kleinen ETL-Jobs; das vollständige Muster steht in Was ist ETL?.
- Speicherung: Sobald die Spalten typisiert sind, hilft Gescrapte Daten als CSV, JSON und in SQLite speichern bei der Wahl zwischen CSV, JSON und SQLite.
- Datenerfassung im großen Maßstab: Größere Scraping-Projekte und die Infrastruktur dafür finden Sie auf unserer Seite zu Data Scraping.
Häufige Fehler
- Die einzige Kopie bereinigen. Wer die Rohdatei überschreibt, muss bei jeder Regeländerung neu scrapen.
- Zuerst deduplizieren.
drop_duplicatesvergleicht exakte Werte, daher überleben"Olio"und"Olio "beide, wenn Sie es vor dem Entfernen der Leerzeichen ausführen. astype(float)auf Preistexte. Das scheitert am ersten£. Entfernen Sie nicht numerische Zeichen, verwenden Sie dannpd.to_numericmiterrors="coerce"und zählen Sie die entstehendenNaN-Werte.- Lücken mit null füllen. Ein fehlender Lagerbestand ist kein Bestand von null. Verwenden Sie
Int64und lassen Sie den Wert fehlen. - Verkettete Zuweisung. Unter pandas 3 ändert
df[df.price > 50]["flag"] = Truenichts und gibt nur eine Warnung aus. Verwenden Siedf.loc[df.price > 50, "flag"] = True. - Für Excel ohne BOM speichern. Excel unter Windows liest eine einfache UTF-8-CSV mit einer veralteten Codepage, und das
£geht wieder kaputt;encoding="utf-8-sig"verhindert das. - Härter scrapen statt besser bereinigen. Fehlen Daten, weil Anfragen fehlgeschlagen sind, beheben Sie das bei der Erfassung: Beachten Sie die robots.txt, drosseln Sie bei HTTP 429 das Tempo und nutzen Sie eine offizielle API, wo es eine gibt.
Entscheidungshilfe
| Bedarf | Empfehlung |
|---|---|
| Als Text gespeicherte Zahlen | Symbole entfernen, pd.to_numeric(errors="coerce"), dann NaN-Werte zählen |
| Ganze Zahlen mit Lücken | Nullbares Int64, nicht int64 oder float |
Text mit £, ’ oder é | Dekodierung an der Quelle korrigieren; gespeicherte Dateien per latin-1-Rundreise reparieren |
| Derselbe Artikel von mehreren Seiten | drop_duplicates auf einer stabilen ID, neueste Kopie behalten |
| Einige auffällige Preise | Mit der IQR-Regel markieren und prüfen; nicht automatisch löschen |
| Wiederkehrende Labels wie Kategorien | astype("category") und eine explizite Zuordnung für Platzhalter |
| Zeitstempel aus verschiedenen Läufen | pd.to_datetime(..., utc=True) |
| Typen zwischen Schritten erhalten | Parquet neben der CSV speichern |
| Große oder regelmäßige Scraping-Läufe | Rate Limits, Wiederholungen und, wo das Volumen es erfordert, Rotierender Proxy über pr.proxynet.io:8000 |
Häufige Fragen
Sollte ich Daten beim Scrapen oder danach bereinigen?
Erledigen Sie beim Scrapen nur das Nötigste (Text übernehmen, Leerzeichen entfernen) und den Rest danach in pandas. Solange Sie die Rohdatei behalten, können Sie die Bereinigungsregeln ändern und in Sekunden neu anwenden, ohne eine einzige neue Anfrage an die Website zu senden.
Warum zeigt meine Preisspalte £ statt £?
Der Server hat kein Charset angegeben, deshalb hat Requests UTF-8-Bytes als ISO-8859-1 dekodiert. Übergeben Sie response.content an Ihren Parser oder setzen Sie response.encoding = "utf-8". Bei bereits gespeicherten Daten macht s.encode("latin-1").decode("utf-8") den Fehler rückgängig.
Ist dropna() der richtige Umgang mit fehlenden Werten?
Nur wenn eine Zeile ohne dieses Feld für Ihre Fragestellung nutzlos ist. dropna() ohne Argumente verwirft eine Zeile, sobald irgendeine Spalte fehlt, und kann so wegen eines einzigen optionalen Felds den Großteil eines gescrapten Datensatzes löschen. Verwenden Sie dropna(subset=[...]) für die Spalten, die Sie wirklich brauchen, und markieren Sie den Rest.
Wie wähle ich die Spalte zum Entfernen von Duplikaten?
Verwenden Sie eine Kennung, die auf der Seite steht, etwa eine UPC, SKU, ISBN oder Produkt-ID. URLs funktionieren nur auf Websites, auf denen jeder Artikel genau eine URL hat. Gibt es keine ID, kombinieren Sie mehrere normalisierte Felder, zum Beispiel Titel und Marke.
Funktioniert dieser Code mit pandas 2?
Getestet haben wir ihn nur mit pandas 3.0.6, aber jeder verwendete Aufruf existiert auch in pandas 2.x, und die Auswahl der Textspalten deckt sowohl den alten Typ object als auch den neuen Typ str ab. Rechnen Sie mit kleinen Unterschieden in der Ausgabe, etwa object statt str und Zeitstempeln in Nanosekunden statt Mikrosekunden. Was sich zwischen beiden Versionen wirklich unterschiedlich verhält, ist Code, der auf verketteter Zuweisung beruht.
Brauche ich einen Proxy, um Daten zu bereinigen?
Nein. Die Bereinigung läuft auf Ihrem Rechner und erzeugt keinen Netzwerkverkehr. Ein Proxy spielt nur bei der Erfassung eine Rolle, und auch dann erst ab einem Volumen, bei dem eine einzelne IP-Adresse die Rate Limits einer Website überschreiten würde. Der Bereinigungscode bleibt gleich, egal über welches Netzwerk die Seiten kamen.
Fazit
Gescrapte Daten kommen als Text an, der für menschliche Leser geformt ist. Ein verlässlicher Bereinigungsdurchgang behält die Rohdatei, korrigiert zuerst Text und Kodierung, wandelt jede Spalte in einen echten Typ um, entfernt Duplikate anhand eines stabilen Schlüssels, markiert auffällige Werte, statt sie zu löschen, und hält bei einer fehlgeschlagenen Prüfung an, bevor gespeichert wird. Das Skript oben erledigt all das für 78 echte Zeilen in weniger als einer Sekunde, sobald die Seiten heruntergeladen sind. Wenn Ihre Datenerfassung über eine Übungsseite hinauswächst, decken die Tarife auf unserer Proxy-Seite die Netzwerkseite ab.




