---
title: "Gescrapte Daten mit pandas bereinigen: Python-Anleitung"
description: "Gescrapte Daten mit pandas bereinigen: erst Text, dann Typen, Duplikate, Lücken und Ausreißer korrigieren und vor dem Speichern validieren. Mit Skript."
url: https://proxynet.io/de/blog/clean-scraped-data-with-pandas
date: 2026-09-28
author: "Acar Diveroli"
category: "Anleitungen, Web Scraping"
lang: de
---

# Gescrapte Daten mit pandas bereinigen: Python-Anleitung

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](https://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.

> **Hinweis: Kurzantwort**
>
> Laden Sie die Rohzeilen in einen DataFrame und lassen Sie die Rohdatei unangetastet. Bereinigen Sie zuerst den Text: Leerzeichen an den Rändern entfernen, leere Strings in fehlende Werte umwandeln und fehlerhafte Kodierung reparieren. Wandeln Sie danach die Spalten in echte Typen um, mit `pd.to_numeric(..., errors="coerce")`, `pd.to_datetime(..., utc=True)`, dem nullbaren Typ `Int64` und `category`. Entfernen Sie dann Duplikate anhand eines stabilen Schlüssels wie eines Produktcodes und behalten Sie die neueste Kopie. Markieren Sie Ausreißer, statt sie zu löschen, führen Sie einige `assert`-artige Prüfungen aus, die das Skript bei Problemen anhalten, und speichern Sie das Ergebnis als CSV und Parquet.

## 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)` und `Three` (eine CSS-Klasse für eine Sternebewertung) kommen alle als Strings an.
- **Kodierungsschätzungen gehen schief.** books.toscrape.com sendet `Content-Type: text/html` ohne Charset. In diesem Fall folgt Requests der alten HTTP/1.1-Regel und dekodiert die Seite als ISO-8859-1 ([Requests-Dokumentation zu Kodierungen](https://requests.readthedocs.io/en/latest/user/advanced/#encodings)), sodass aus dem UTF-8-Pfundzeichen `Â£` wird. Den Mechanismus erklärt der Beitrag [Python-Unicode-Kodierungsfehler](/de/blog/python-unicode-encoding-errors).
- **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](/de/blog/pagination-web-scraping), 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:

1. **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.
2. **Text normalisieren.** Leerzeichen an beiden Enden entfernen, leere Strings in fehlende Werte umwandeln, fehlerhafte Kodierung reparieren und Unicode-Normalisierung anwenden.
3. **Fehlende Werte zählen.** Halten Sie sie in einem Bericht fest und fügen Sie Markierungsspalten hinzu; füllen Sie sie nicht mit Vermutungen.
4. **Preise umwandeln.** Ziehen Sie das Währungssymbol in eine eigene Spalte und wandeln Sie den Rest in eine Gleitkommazahl um.
5. **Weitere Zahlen extrahieren.** Lagerbestände aus `In stock (22 available)`, Bewertungen aus Wörtern, Rezensionszahlen aus Text.
6. **Datumswerte parsen** und in Zeitstempel mit Zeitzone umwandeln.
7. **Kategorien normalisieren**, sodass ein Begriff genau ein Label hat.
8. **Duplikate entfernen**, anhand eines stabilen Schlüssels und erst, wenn die Typen stimmen.
9. **Ausreißer markieren**, mit einer einfachen Regel, die Sie erklären können.
10. **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](https://pandas.pydata.org/docs/whatsnew/v3.0.0.html) betreffen Bereinigungscode direkt:

- **Strings haben einen eigenen Typ.** Textspalten werden jetzt als `str` statt als `object` erkannt, und fehlende Werte darin sind `NaN`, wie bei den anderen Standardtypen. Deshalb wählt das Skript Textspalten mit `select_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` ändern `df` nicht mehr; pandas 3.0.6 gibt nur eine Warnung `ChainedAssignmentError` aus. Verwenden Sie `df.loc[0, "price"] = 10`. Die alte `SettingWithCopyWarning` ist 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_datetime` liefert für Strings jetzt `datetime64[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](https://pandas.pydata.org/docs/reference/api/pandas.to_numeric.html)); ä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:

```bash
python -m venv .venv
.venv/bin/pip install pandas requests beautifulsoup4 pyarrow   # Windows: .venv\Scripts\pip
```

Der 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](/de/blog/beautifulsoup-tutorial). Der zweite Lauf liest `books_raw.csv` und wiederholt nur die Bereinigung.

```python
"""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:

```text
{'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](https://docs.python.org/3/library/unicodedata.html)), 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](/de/price-monitoring) und [Wettbewerberpreise verfolgen](/de/blog/competitor-price-tracking).
- **Marktforschung:** Kategorieanteile, Verteilungen von Bewertungen und Lagerbestände über einen ganzen Katalog. Siehe [Marktforschung](/de/market-research).
- **Data Mining und Modellierung:** Modelle brauchen typisierte, deduplizierte Eingaben. Hintergrund im Beitrag [Was ist Data Mining?](/de/blog/what-is-data-mining).
- **Pipelines:** Dieses Skript ist der Transform-Schritt eines kleinen ETL-Jobs; das vollständige Muster steht in [Was ist ETL?](/de/blog/what-is-etl).
- **Speicherung:** Sobald die Spalten typisiert sind, hilft [Gescrapte Daten als CSV, JSON und in SQLite speichern](/de/blog/save-scraped-data-csv-json-sqlite) 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](/de/data-scraping).

## Häufige Fehler

- **Die einzige Kopie bereinigen.** Wer die Rohdatei überschreibt, muss bei jeder Regeländerung neu scrapen.
- **Zuerst deduplizieren.** `drop_duplicates` vergleicht 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 dann `pd.to_numeric` mit `errors="coerce"` und zählen Sie die entstehenden `NaN`-Werte.
- **Lücken mit null füllen.** Ein fehlender Lagerbestand ist kein Bestand von null. Verwenden Sie `Int64` und lassen Sie den Wert fehlen.
- **Verkettete Zuweisung.** Unter pandas 3 ändert `df[df.price > 50]["flag"] = True` nichts und gibt nur eine Warnung aus. Verwenden Sie `df.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](/de/blog/robots-txt), drosseln Sie bei [HTTP 429](/de/blog/http-429-too-many-requests) 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](https://proxynet.io/de/rotating-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](/de/proxy)-Seite die Netzwerkseite ab.
