Gescrapte Daten als CSV, JSON und in SQLite speichern

Veröffentlicht:

14 Min. Lesezeit

Acar Diveroli
Autor: Acar Diveroli
Ein zentraler Datensatz-Block, verbunden mit vier Karten: CSV, JSON, SQLITE und XLSX; die SQLite-Linie ist blau.

Ihr Scraper durchläuft zehn Seiten und gibt hundert saubere Datensätze aus. Am nächsten Morgen starten Sie ihn erneut, und die CSV-Datei hat zweihundert Zeilen, die Hälfte davon Duplikate. Eine Kollegin öffnet die Datei in Excel, und jedes typografische Anführungszeichen erscheint als “. Eine Woche später stürzt der Job mitten im Schreiben ab und hinterlässt eine JSON-Datei, die kein Parser mehr öffnet. Keines dieser Probleme hat mit dem Scraping selbst zu tun; entschieden wird all das in den wenigen Speicherzeilen am Ende des Skripts.

Dieser Leitfaden behandelt die drei Formate, in die die meisten Python-Scraper schreiben: CSV mit dem Modul csv und pandas, JSON und JSON Lines sowie SQLite mit einem Upsert, der Duplikate über mehrere Läufe hinweg entfernt. Sie sehen, wie Sie sicher anhängen, welche Kodierung Excel erwartet, wie groß jedes Format wird und wann der Wechsel von SQLite zu PostgreSQL sinnvoll ist. Alle Beispiele liefen am 28. September 2026 gegen quotes.toscrape.com, eine Übungsseite, die eigens für Scraping gebaut wurde, mit Python 3.13, Requests 2.34, beautifulsoup4 4.15, pandas 3.0 und SQLite 3.50. Das Parsen selbst behandelt das BeautifulSoup-Tutorial; hier setzen wir ein, sobald die Zeilen vorliegen.

Welche Möglichkeiten gibt es, gescrapte Daten zu speichern?

Es gibt zwei Familien. Flache Dateien (CSV, JSON, JSON Lines, XLSX) sind einzelne Dateien, die von oben nach unten geschrieben werden. Sie brauchen keinen Server und öffnen sich in vertrauten Werkzeugen, wissen aber nicht, was ein Duplikat ist. Datenbanken (SQLite, PostgreSQL, MySQL) speichern Zeilen in Tabellen mit Schlüsseln und Constraints: Sie führen Duplikate für Sie zusammen, beantworten Fragen per SQL und überstehen einen Absturz mitten im Schreiben.

SQLite liegt dazwischen. Es ist eine Datenbank, aber die gesamte Datenbank ist eine gewöhnliche Datei auf der Festplatte, und Python liefert das Modul sqlite3 in seiner Standardbibliothek mit. Für einen Scraper, der auf einem Rechner läuft, ist diese Kombination schwer zu schlagen.

Wie speichert ein Scraper seine Zeilen?

Unabhängig vom Format durchläuft ein sauber gebauter Scraper dieselben fünf Schritte:

  1. Jeden Datensatz in ein Dict normalisieren, jedes Mal mit denselben Schlüsseln: quote_id, text, author, tags und ein Zeitstempel.
  2. Jedem Datensatz einen stabilen Schlüssel geben. Hat die Website eine eigene ID, nutzen Sie diese (eine Produkt-SKU, eine Artikel-ID in der URL). quotes.toscrape.com hat keine, daher bilden wir aus Autor und Zitattext einen Hash mit 16 Zeichen.
  3. Den Schreibmodus wählen. Die Datei überschreiben (ein frischer Snapshot), an sie anhängen (ein wachsendes Log) oder per Upsert in eine Tabelle schreiben (eine Zeile pro Schlüssel, direkt aktualisiert).
  4. In einem Schritt schreiben. Entweder landet der ganze Stapel oder gar nichts davon: eine Datenbanktransaktion oder eine temporäre Datei, die die alte ersetzt, sobald sie vollständig ist.
  5. Zurücklesen. Öffnen Sie die Datei mit dem Reader, den der Empfänger nutzt (Excel, pandas, json.loads), bevor Sie ihr vertrauen.

Schritt 2 lassen die meisten Skripte aus, und genau daher kommen die Duplikate.

CSV, JSON Lines und SQLite im Vergleich

CSVJSON (ein Array)JSON LinesSQLite
Verschachtelte Felder (Tag-Listen)Abflachen, zum Beispiel love|lifeNativNativJSON-Text in einer Spalte, abfragbar mit json_each
Bei jedem Lauf anhängenJa, Kopfzeile nur einmal schreibenNein, die schließende ] muss neu geschrieben werdenJa, eine Zeile pro DatensatzJa, mit Upsert
Entfernt DuplikateNeinNeinNeinJa, über den Primärschlüssel
Öffnet sich in ExcelJa, mit UTF-8-BOMNeinNeinNein (erst exportieren)
Übersteht Absturz beim SchreibenLetzte Zeile kann abgeschnitten seinDatei kann unlesbar werdenNur die letzte Zeile geht verlorenJa, Transaktionen werden zurückgerollt
Abfrage ohne alles zu ladenNeinNeinZeile für ZeileJa, SQL und Indizes
Größe für unsere 100 Zitate25,9 KB42,4 KB (eingerückt)35,8 KB49,2 KB (mit einem Index)

Die Größen stammen aus unserem eigenen Testlauf. SQLite ist hier am größten, weil es Daten in festen Seiten zu 4 KB speichert (12 Seiten für diese Tabelle und ihren Index); der Overhead schrumpft, wenn die Tabelle wächst. Zum Vergleich: Dieselben Daten als XLSX waren 17,0 KB groß, da XLSX ein gezipptes Format ist.

Wie speichern Sie gescrapte Daten in Python als CSV?

Das Modul csv der Standardbibliothek kümmert sich um Quoting, Kommas in Feldern und Zeilenumbrüche in Werten mit Anführungszeichen. DictWriter ordnet jedes Dict über die Spaltennamen einer Zeile zu:

python
import csv
from pathlib import Path

FIELDS = ["quote_id", "text", "author", "author_url", "tags", "page", "scraped_at"]

def append_csv(rows, path: Path) -> None:
    new_file = not path.exists() or path.stat().st_size == 0
    with path.open("a", newline="", encoding="utf-8-sig" if new_file else "utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        if new_file:
            writer.writeheader()
        for row in rows:
            writer.writerow({**row, "tags": "|".join(row["tags"])})

Drei Details in dieser Funktion verhindern die üblichen CSV-Beschwerden:

  • newline="". Die Python-Dokumentation zu csv verlangt es für Reader und Writer. Ohne diese Angabe werden Zeilenumbrüche in Feldern mit Anführungszeichen falsch gelesen, und unter Windows endet jede Zeile mit einem zusätzlichen \r, das viele Reader nach jedem Datensatz als Leerzeile darstellen.
  • utf-8-sig nur beim ersten Schreiben. Dieser Codec schreibt eine Byte-Order-Mark (EF BB BF) an den Dateianfang; die codecs-Dokumentation beschreibt ihn als die UTF-8-Variante, die Microsoft verwendet. Excel liest diese Markierung und dekodiert die Datei als UTF-8, sodass \u201c auch \u201c bleibt. Hängt das Skript später an, wechselt es zu reinem utf-8, sonst landet eine zweite Markierung mitten in der Datei. Unsere Testdatei enthielt nach zwei Läufen genau eine.
  • Die Kopfzeile nur bei einer neuen Datei. Wer bei jedem Lauf eine Kopfzeile anhängt, hinterlässt verirrte quote_id,text,...-Zeilen in den Daten.

Tags werden mit | verbunden, einem Trennzeichen, das in den Werten nie vorkommt.

Wenn Sie eine Datei mit BOM lesen, verwenden Sie wieder encoding="utf-8-sig". Mit reinem utf-8 kommt der erste Spaltenname als '\ufeffquote_id' zurück, und row["quote_id"] löst einen KeyError aus. Das ist uns im Test passiert; weitere Kodierungsfallen finden Sie unter Unicode-Kodierungsfehler in Python.

Mit pandas ist dieselbe Datei eine Zeile, und drop_duplicates repariert eine CSV- oder JSON-Lines-Datei, die bereits Wiederholungen enthält:

python
import pandas as pd

df = pd.read_json("data/quotes.jsonl", lines=True, convert_dates=False)
df = df.drop_duplicates(subset="quote_id", keep="last")
df.assign(tags=df["tags"].str.join("|")).to_csv(
    "data/quotes_clean.csv", index=False, encoding="utf-8-sig")
df.assign(tags=df["tags"].str.join(", ")).to_excel(
    "data/quotes.xlsx", index=False, sheet_name="quotes")  # benötigt openpyxl

Ein Excel-Arbeitsblatt fasst laut Microsofts Spezifikationen und Grenzwerten höchstens 1.048.576 Zeilen. Eine CSV-Datei hat keine solche Grenze, aber Excel zeigt nur so viele Zeilen davon an.

Wie speichern Sie gescrapte Daten als JSON oder JSON Lines?

Ein einzelnes JSON-Array eignet sich für einen Snapshot, den ein anderes Programm in einem Zug lädt. Schreiben Sie es mit ensure_ascii=False, damit nicht englischer Text lesbar bleibt, statt zu \u201c-Escapes zu werden, und schreiben Sie atomar: erst in eine temporäre Datei im selben Ordner, dann mit os.replace austauschen. Bricht der Prozess mittendrin ab, bleibt die alte Datei intakt.

python
import json, os, tempfile
from pathlib import Path

def write_json_atomic(data, path: Path) -> None:
    fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    os.replace(tmp, path)

Die Schwäche eines JSON-Arrays ist das Anhängen. Die Datei endet mit ], also bedeutet ein zusätzlicher Datensatz, die ganze Datei zu lesen und neu zu schreiben. JSON Lines vermeidet das: Jede Zeile ist ein vollständiger JSON-Wert, die Datei ist UTF-8, und Zeilen enden mit \n. Anhängen ist ein einfacher Schreibvorgang, und ein Absturz kann nur die letzte Zeile beschädigen:

python
def append_jsonl(rows, path: Path) -> None:
    with path.open("a", encoding="utf-8") as f:
        for row in rows:
            f.write(json.dumps(row, ensure_ascii=False) + "\n")

pandas liest die Datei mit pd.read_json(path, lines=True), und mit chunksize= verarbeiten Sie eine große Datei in Stücken. Eine Überraschung aus unserem Test: Laut Dokumentation zu read_json werden Spalten, deren Namen auf _at oder _time enden, standardmäßig als Datum geparst. Unsere Spalte scraped_at wurde zu einem zeitzonenbehafteten Datetime, und to_excel() brach anschließend mit „Excel does not support datetimes with timezones“ ab (Excel unterstützt keine Datumswerte mit Zeitzone). Übergeben Sie convert_dates=False, wenn der Text unverändert bleiben soll.

Lässt sich eine Datei mit JSONDecodeError nicht laden, finden Sie die üblichen Ursachen (ein abgebrochener Schreibvorgang, zwei Arrays in einer Datei, JSON Lines als ein Dokument gelesen) unter JSONDecodeError: Expecting value.

Wie speichern Sie gescrapte Daten in SQLite ohne Duplikate?

Deklarieren Sie den stabilen Schlüssel als Primärschlüssel und überlassen Sie der Datenbank die Entscheidung zwischen Einfügen und Aktualisieren. SQLite unterstützt das seit Version 3.24.0 mit der Upsert-Klausel; im Teil DO UPDATE verweist das spezielle Präfix excluded. auf die Werte, die das abgewiesene Insert schreiben wollte (SQLite-Dokumentation zu UPSERT).

sql
CREATE TABLE IF NOT EXISTS quotes (
    quote_id   TEXT PRIMARY KEY,
    text       TEXT NOT NULL,
    author     TEXT NOT NULL,
    author_url TEXT,
    tags       TEXT,              -- JSON-Array
    first_seen TEXT NOT NULL,
    last_seen  TEXT NOT NULL
);

INSERT INTO quotes (quote_id, text, author, author_url, tags, first_seen, last_seen)
VALUES (:quote_id, :text, :author, :author_url, :tags, :scraped_at, :scraped_at)
ON CONFLICT(quote_id) DO UPDATE SET
    tags       = excluded.tags,
    author_url = excluded.author_url,
    last_seen  = excluded.last_seen;

first_seen wird nur geschrieben, wenn die Zeile neu ist; last_seen rückt bei jedem Lauf vor. Dieses Paar liefert Ihnen eine inkrementelle Historie gratis: Eine Zeile, deren last_seen älter ist als der letzte Lauf, ist von der Website verschwunden, und eine Zeile, deren first_seen dem letzten Lauf entspricht, ist neu. Preis-Tracker und Änderungsmonitore beruhen genau auf diesem Muster.

Zwei Läufe des vollständigen Skripts unten gaben aus:

text
scraped 100 rows: 100 new, 0 already in the database
scraped 100 rows: 0 new, 100 already in the database

Die CSV- und JSON-Lines-Dateien derselben zwei Läufe enthielten je 200 Datensätze. Die Datenbank enthielt 100.

Sobald die Daten in SQLite liegen, werden Fragen zu Abfragen. Diese liefen gegen unsere Testdatenbank:

python
import sqlite3

con = sqlite3.connect("data/quotes.db")

# Autoren mit den meisten Zitaten
for author, n in con.execute(
        "SELECT author, COUNT(*) AS n FROM quotes GROUP BY author ORDER BY n DESC LIMIT 3"):
    print(author, n)

# Zitate mit dem Tag "love" (Tags sind als JSON-Array gespeichert)
print(con.execute("""
    SELECT COUNT(*) FROM quotes, json_each(quotes.tags)
    WHERE json_each.value = 'love'""").fetchone()[0])

# Zeilen, die beim letzten Lauf nicht auf der Website standen
print(con.execute("""
    SELECT COUNT(*) FROM quotes
    WHERE last_seen < (SELECT MAX(last_seen) FROM quotes)""").fetchone()[0])
con.close()
text
Albert Einstein 10
J.K. Rowling 9
Marilyn Monroe 7
14
0

Für Analysen liefert pd.read_sql_query(sql, con) das Ergebnis als DataFrame. Die Bereinigungsschritte, die meist folgen, beschreibt Gescrapte Daten mit pandas bereinigen.

Vollständiges Skript: scrapen, dann CSV, JSON Lines und SQLite schreiben

Das Skript folgt dem Link „Next“ der Website, bis er verschwindet, wiederholt Anfragen bei 429- und 5xx-Antworten mit exponentiellem Backoff (urllib3s Retry beachtet auch einen Retry-After-Header), wartet zwischen den Seiten eine Sekunde und schreibt pro Lauf einen einzigen Zeitstempel, damit die last_seen-Vergleiche funktionieren. quotes.toscrape.com hat 10 Seiten und 100 Zitate.

bash
pip install requests beautifulsoup4 lxml pandas openpyxl
python
"""Scrapt quotes.toscrape.com und speichert die Zeilen als CSV, JSON Lines und in SQLite."""
import csv
import hashlib
import json
import sqlite3
import time
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

BASE = "https://quotes.toscrape.com/"
FIELDS = ["quote_id", "text", "author", "author_url", "tags", "page", "scraped_at"]


def make_session() -> requests.Session:
    retry = Retry(total=4, backoff_factor=1,
                  status_forcelist=[429, 500, 502, 503, 504],
                  allowed_methods=["GET"])
    session = requests.Session()
    session.mount("https://", HTTPAdapter(max_retries=retry))
    session.headers["User-Agent"] = "quotes-storage-demo/1.0 (contact: you@example.com)"
    return session


def quote_key(text: str, author: str) -> str:
    # Die Website hat keine Zitat-IDs, daher leiten wir einen stabilen Schlüssel aus dem Inhalt ab.
    return hashlib.sha1(f"{author}\n{text}".encode("utf-8")).hexdigest()[:16]


def scrape(session: requests.Session, delay: float = 1.0):
    url, page = BASE, 1
    run_at = datetime.now(timezone.utc).isoformat(timespec="seconds")  # ein Zeitstempel pro Lauf
    while url:
        resp = session.get(url, timeout=20)
        resp.raise_for_status()
        soup = BeautifulSoup(resp.content, "lxml")
        for q in soup.select("div.quote"):
            text = q.select_one("span.text").get_text(strip=True)
            author = q.select_one("small.author").get_text(strip=True)
            yield {
                "quote_id": quote_key(text, author),
                "text": text,
                "author": author,
                "author_url": urljoin(BASE, q.select_one("span a")["href"]),
                "tags": [t.get_text(strip=True) for t in q.select("a.tag")],
                "page": page,
                "scraped_at": run_at,
            }
        nxt = soup.select_one("li.next a")
        url = urljoin(url, nxt["href"]) if nxt else None
        page += 1
        time.sleep(delay)


def append_csv(rows, path: Path) -> None:
    new_file = not path.exists() or path.stat().st_size == 0
    with path.open("a", newline="", encoding="utf-8-sig" if new_file else "utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        if new_file:
            writer.writeheader()
        for row in rows:
            writer.writerow({**row, "tags": "|".join(row["tags"])})


def append_jsonl(rows, path: Path) -> None:
    with path.open("a", encoding="utf-8") as f:
        for row in rows:
            f.write(json.dumps(row, ensure_ascii=False) + "\n")


SCHEMA = """
CREATE TABLE IF NOT EXISTS quotes (
    quote_id   TEXT PRIMARY KEY,
    text       TEXT NOT NULL,
    author     TEXT NOT NULL,
    author_url TEXT,
    tags       TEXT,              -- JSON-Array
    first_seen TEXT NOT NULL,
    last_seen  TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_quotes_author ON quotes(author);
"""

UPSERT = """
INSERT INTO quotes (quote_id, text, author, author_url, tags, first_seen, last_seen)
VALUES (:quote_id, :text, :author, :author_url, :tags, :scraped_at, :scraped_at)
ON CONFLICT(quote_id) DO UPDATE SET
    tags       = excluded.tags,
    author_url = excluded.author_url,
    last_seen  = excluded.last_seen
"""


def save_sqlite(rows, path: Path) -> tuple[int, int]:
    con = sqlite3.connect(path)
    try:
        con.executescript(SCHEMA)
        before = con.execute("SELECT COUNT(*) FROM quotes").fetchone()[0]
        with con:  # eine Transaktion: Commit bei Erfolg, Rollback bei Fehler
            con.executemany(UPSERT, [{**r, "tags": json.dumps(r["tags"])} for r in rows])
        after = con.execute("SELECT COUNT(*) FROM quotes").fetchone()[0]
        return after - before, len(rows) - (after - before)
    finally:
        con.close()


if __name__ == "__main__":
    out = Path("data")
    out.mkdir(exist_ok=True)
    rows = list(scrape(make_session()))
    append_csv(rows, out / "quotes.csv")
    append_jsonl(rows, out / "quotes.jsonl")
    new, seen = save_sqlite(rows, out / "quotes.db")
    print(f"scraped {len(rows)} rows: {new} new, {seen} already in the database")

with con: fasst den Upsert in eine Transaktion: Schlägt eine Zeile fehl, wird keine geschrieben. Die Verbindung schließt es nicht, das übernimmt der finally-Block. Benannte Platzhalter (:quote_id) erlauben es executemany, dieselben Dicts zu verwenden wie der CSV-Writer, und verhindern, dass Apostrophe das SQL zerbrechen.

Für eine echte Website behalten Sie die Struktur bei und ändern Selektoren, Schlüssel und Wartezeit. Prüfen Sie vorher die Nutzungsbedingungen und die robots.txt der Website (robots.txt erklärt), und nutzen Sie eine offizielle API, wenn es eine gibt. Dieselbe Wartezeit von einer Sekunde und dieselben Retry-Einstellungen halten einen Job fern von HTTP-429-Fehlern. Wächst ein Scraper zu einem geplanten Crawler, der seine Anfragen über mehrere Exit-IPs verteilt, lassen sich Rotierender Proxy und Residential-Proxy über die Einstellung proxies in dieselbe requests.Session einbinden; der Speichercode ändert sich nicht.

Wann sollten Sie von SQLite zu PostgreSQL wechseln?

Der Leitfaden des SQLite-Projekts benennt die Grenzen klar: SQLite unterstützt pro Datenbankdatei nur einen Schreiber gleichzeitig und ist die falsche Wahl, wenn Daten und Anwendung auf verschiedenen Rechnern liegen. Für ein Scraping-Projekt ergeben sich daraus drei Signale:

  • Mehrere Worker schreiben gleichzeitig. Crawler-Prozesse auf drei Servern brauchen einen Datenbankserver; einige Prozesse auf einem Rechner können sich abwechseln.
  • Die Datenbank muss über das Netzwerk erreichbar sein. Ein Dashboard, eine API und ein Scraper auf getrennten Hosts sollten sich PostgreSQL teilen, nicht eine .db-Datei auf einer Netzwerkfreigabe.
  • Andere Personen brauchen Berechtigungen. Benutzer, Rollen und reiner Lesezugriff sind Serverfunktionen.

Die Größe allein ist selten der Auslöser: Das dokumentierte Maximum liegt bei etwa 281 TB, weit über dem, was ein einzelner Scraper erzeugt. Der Umzug selbst ist klein. PostgreSQL verwendet dieselbe Syntax INSERT ... ON CONFLICT (key) DO UPDATE mit EXCLUDED, der Upsert oben lässt sich also fast unverändert übernehmen. Pipelines, die zu mehreren Stufen wachsen, behandelt Was ist ETL?.

Anwendungsfälle

Häufige Fehler

  • Kein stabiler Schlüssel. Ohne ihn hängt jeder Lauf den vollständigen Datensatz erneut an. Nutzen Sie die ID der Website oder bilden Sie einen Hash aus den Feldern, die einen Datensatz identifizieren.
  • CSV-Dateien ohne newline="" öffnen. Zusätzliche \r-Zeichen unter Windows und zerbrochene mehrzeilige Felder.
  • Reines utf-8 für eine CSV, die in Excel geöffnet werden soll. Umlaute, Akzente und typografische Zeichen werden zu Folgen wie é.
  • Die Ausgabedatei direkt überschreiben. Ein Absturz hinterlässt eine halbe Datei. Schreiben Sie in eine temporäre Datei und tauschen Sie sie mit os.replace aus.
  • Ein Commit pro Zeile in SQLite. Jeder Commit synchronisiert auf die Festplatte. Fassen Sie einen Stapel mit executemany in eine Transaktion.
  • SQL mit f-Strings zusammenbauen. Ein Zitat mit Apostroph zerbricht die Anweisung. Verwenden Sie Platzhalter.
  • Bei einem großen Crawl alles im Speicher halten. Schreiben Sie pro Seite oder pro Stapel, damit ein Absturz eine Seite kostet und nicht den ganzen Lauf.

Entscheidungshilfe

BedarfEmpfehlung
Eine Datei, die eine Kollegin in Excel öffnetCSV mit utf-8-sig oder XLSX über pandas
Verschachtelte Datensätze für ein anderes ProgrammJSON (ein Array), atomar geschrieben
Ein fortlaufendes Log aller LäufeJSON Lines
Wiederholte Läufe ohne DuplikateSQLite mit Primärschlüssel und Upsert
Historie: wann wurde jeder Eintrag zuerst und zuletzt gesehenSQLite mit first_seen und last_seen
Mehrere Rechner schreiben gleichzeitigPostgreSQL mit demselben ON CONFLICT-Upsert
Ad-hoc-AnalyseJede der obigen Varianten, in pandas geladen

Häufige Fragen

Welches Format eignet sich am besten zum Speichern gescrapter Daten?

Das hängt davon ab, wer die Daten liest: CSV für Tabellenkalkulationen, JSON Lines für verschachtelte Datensätze und fortlaufende Logs, SQLite für geplante Scraper, die keine Zeilen verdoppeln dürfen. Viele Projekte halten SQLite als maßgebliche Quelle und exportieren CSV für Menschen.

Warum sieht meine gescrapte CSV in Excel verstümmelt aus?

Hat eine CSV-Datei keine Byte-Order-Mark, dekodiert Excel unter Windows sie oft mit der alten Codepage des Systems statt mit UTF-8, sodass jedes Mehrbyte-Zeichen zu zwei oder drei falschen wird. Schreiben Sie die Datei mit encoding="utf-8-sig" oder importieren Sie sie über den Datenimport von Excel und wählen Sie UTF-8.

Wie hänge ich gescrapte Daten an eine bestehende CSV an, ohne die Kopfzeile zu wiederholen?

Öffnen Sie die Datei im Modus "a" und schreiben Sie die Kopfzeile nur, wenn die Datei neu oder leer ist, wie es append_csv oben tut. Anhängen entfernt keine Duplikate; dafür laden Sie die Datei mit pandas und rufen drop_duplicates auf oder speichern die Daten mit einem Schlüssel in SQLite.

Was ist der Unterschied zwischen JSON und JSON Lines?

Eine JSON-Datei enthält einen einzigen Wert, meist ein Array von Datensätzen, und muss daher als Ganzes gelesen und geschrieben werden. JSON Lines enthält einen JSON-Wert pro Zeile, sodass Sie einen Datensatz mit einem einzigen Schreibvorgang anhängen und eine große Datei Zeile für Zeile lesen können.

Kommt SQLite mit einem großen Scraping-Projekt zurecht?

Auf einem Rechner meistens ja; die Größengrenze liegt weit über dem, was ein Scraper erzeugt. Entscheidend ist die Nebenläufigkeit: ein Schreiber gleichzeitig pro Datei. Schreiben mehrere Rechner gleichzeitig, brauchen Sie PostgreSQL.

Wie vermeide ich doppelte Zeilen, wenn ich einen Scraper erneut ausführe?

Geben Sie jedem Datensatz einen stabilen Schlüssel, machen Sie ihn in SQLite zum Primärschlüssel und fügen Sie mit ON CONFLICT(key) DO UPDATE ein. Bei flachen Dateien entfernen Sie Duplikate nach dem Laden mit drop_duplicates(subset="key").

Fazit

Die Speicherung entscheidet darüber, ob ein Scraper nach seinem ersten Lauf nützlich bleibt. CSV mit utf-8-sig und newline="" ist die Übergabe an Tabellenkalkulationen, JSON Lines der einfachste sichere Weg, strukturierte Datensätze anzuhängen. SQLite mit stabilem Schlüssel und Upsert macht aus wiederholten Läufen eine abfragbare Tabelle mit Historie zu erstem und letztem Auftreten; PostgreSQL übernimmt, sobald mehrere Rechner gleichzeitig schreiben. Wachsen Ihre Läufe so weit, dass eine einzelne IP-Adresse nicht mehr reicht, um die Daten in maßvollem Tempo zu sammeln, finden Sie die passenden Proxy-Tarife von Proxynet.

ChatGPT fragenClaude fragen