ProxynetProxynet

ذخیره داده‌های اسکرپینگ در CSV، JSON و SQLite

تاریخ انتشار:

15 دقیقه مطالعه

Acar Diveroli
نویسنده: Acar Diveroli
یک بلوک مرکزی از رکوردهای اسکرپ‌شده که به چهار کارت CSV، JSON، SQLITE و XLSX وصل است؛ اتصال SQLite آبی است.

اسکرپر شما ده صفحه را پیمایش می‌کند و صد رکورد تمیز چاپ می‌کند. صبح روز بعد دوباره اجرایش می‌کنید و فایل CSV دویست ردیف دارد که نیمی از آن‌ها تکراری است. همکارتان فایل را در Excel باز می‌کند و هر گیومه خمیده به شکل “ نمایش داده می‌شود. یک هفته بعد، کار در میانه نوشتن از کار می‌افتد و یک فایل JSON باقی می‌گذارد که هیچ تجزیه‌گری نمی‌تواند بازش کند. هیچ‌کدام از این‌ها مشکل اسکرپینگ نیست؛ همه را چند خط ذخیره‌سازی در انتهای اسکریپت تعیین می‌کند.

این راهنما سه قالبی را پوشش می‌دهد که بیشتر اسکرپرهای Python در آن‌ها می‌نویسند: CSV با ماژول csv و pandas، JSON و JSON Lines، و SQLite با یک upsert که رکوردهای تکراری را میان اجراها حذف می‌کند. خواهید دید چگونه داده را با خیال راحت به انتهای فایل اضافه کنید، Excel چه کدگذاری‌ای انتظار دارد، حجم هر قالب چقدر است و چه زمانی باید از SQLite به PostgreSQL بروید. همه نمونه‌ها در 28 سپتامبر 2026 روی quotes.toscrape.com، سایتی آزمایشی که برای تمرین اسکرپینگ ساخته شده، با ⁦Python 3.13⁩، ⁦Requests 2.34⁩، ⁦beautifulsoup4 4.15⁩، ⁦pandas 3.0⁩ و ⁦SQLite 3.50⁩ اجرا شدند. بخش تجزیه در آموزش BeautifulSoup آمده است؛ این‌جا از لحظه‌ای شروع می‌کنیم که ردیف‌ها آماده‌اند.

برای ذخیره داده‌های اسکرپینگ چه گزینه‌هایی وجود دارد؟

دو خانواده وجود دارد. فایل‌های تخت (CSV، JSON، JSON Lines، XLSX) یک فایل واحدند که از ابتدا تا انتها نوشته می‌شوند. به سرور نیاز ندارند و در ابزارهای آشنا باز می‌شوند، اما هیچ تصوری از رکورد تکراری ندارند. پایگاه‌های داده (SQLite، PostgreSQL، MySQL) ردیف‌ها را در جدول‌هایی با کلید و قید نگه می‌دارند: تکراری‌ها را خودشان ادغام می‌کنند، با SQL به پرسش‌ها پاسخ می‌دهند و از خرابی در میانه نوشتن جان سالم به در می‌برند.

SQLite میان این دو قرار دارد. پایگاه داده است، اما کل پایگاه داده یک فایل معمولی روی دیسک است و Python ماژول sqlite3 را در کتابخانه استاندارد خود دارد. برای اسکرپری که روی یک ماشین اجرا می‌شود، به‌سختی می‌توان گزینه‌ای مناسب‌تر از این ترکیب یافت.

اسکرپر ردیف‌هایش را چگونه ذخیره می‌کند؟

قالب هر چه باشد، یک اسکرپر درست‌رفتار از همین پنج گام می‌گذرد:

  1. هر رکورد را به یک dict یکدست تبدیل کنید که همیشه همان کلیدها را داشته باشد: quote_id، text، author، tags و یک مهر زمانی.
  2. به هر رکورد یک کلید پایدار بدهید. اگر خود سایت شناسه دارد از آن استفاده کنید (SKU محصول، شناسه مقاله در URL). quotes.toscrape.com شناسه‌ای ندارد، پس از نام نویسنده و متن نقل‌قول یک هش می‌گیریم و کلیدی 16 نویسه‌ای می‌سازیم.
  3. حالت نوشتن را انتخاب کنید. بازنویسی فایل (یک عکس فوری تازه)، افزودن به انتهای فایل (لاگی رو به رشد) یا upsert در یک جدول (یک ردیف برای هر کلید که در جای خود به‌روز می‌شود).
  4. در یک گام بنویسید. یا کل دسته ثبت می‌شود یا هیچ‌چیز: یک تراکنش پایگاه داده، یا یک فایل موقت که پس از کامل‌شدن جای فایل قدیمی را می‌گیرد.
  5. دوباره بخوانید. پیش از اعتماد به فایل، آن را با همان خواننده‌ای باز کنید که مصرف‌کننده به کار می‌برد (Excel، pandas، json.loads).

گام 2 همانی است که بیشتر اسکریپت‌ها از آن می‌گذرند، و دلیل پیدا شدن رکوردهای تکراری هم همین است.

مقایسه CSV، JSON Lines و SQLite

CSVJSON (یک آرایه)JSON LinesSQLite
فیلدهای تودرتو (فهرست برچسب‌ها)باید تخت شوند، مثلاً love|lifeبومیبومیمتن JSON در یک ستون، پرس‌وجو با json_each
افزودن در هر اجرابله، سرستون فقط یک بارخیر، ] پایانی باید بازنویسی شودبله، یک خط برای هر رکوردبله، با upsert
حذف ردیف‌های تکراریخیرخیرخیربله، با کلید اصلی
باز شدن در Excelبله، با BOM در UTF-8خیرخیرخیر (اول خروجی بگیرید)
تاب‌آوری در برابر خرابی میانه نوشتنممکن است خط آخر ناقص بماندممکن است فایل خوانا نباشدفقط خط آخر از دست می‌رودبله، تراکنش‌ها برگردانده می‌شوند
پرس‌وجو بدون بارگذاری همه دادهخیرخیرخط به خطبله، با SQL و ایندکس
حجم برای 100 نقل‌قول ما⁦25.9 KB⁩⁦42.4 KB⁩ (با تورفتگی)⁦35.8 KB⁩⁦49.2 KB⁩ (با یک ایندکس)

این حجم‌ها از آزمایش خود ما به دست آمده‌اند. SQLite این‌جا بزرگ‌ترین است، چون داده را در صفحه‌های ثابت ⁦4 KB⁩ ذخیره می‌کند (12 صفحه برای این جدول و ایندکس آن)؛ این سربار با بزرگ‌تر شدن جدول کم می‌شود. برای مقایسه، همین داده در قالب XLSX حجمی برابر ⁦17.0 KB⁩ داشت، چون XLSX قالبی فشرده است.

چگونه داده‌های اسکرپینگ را با Python در CSV ذخیره کنیم؟

ماژول csv کتابخانه استاندارد نقل‌قول‌گذاری، ویرگول‌های درون فیلد و شکست خط درون مقدارهای نقل‌قول‌دار را مدیریت می‌کند. DictWriter هر dict را بر اساس نام ستون به یک ردیف تبدیل می‌کند:

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"])})

سه جزئیات در این تابع جلوی گلایه‌های همیشگی از CSV را می‌گیرد:

  • newline="". مستندات csv در Python آن را هم برای خواننده‌ها و هم برای نویسنده‌ها لازم می‌داند. بدون آن، شکست خط درون فیلدهای نقل‌قول‌دار اشتباه خوانده می‌شود و در Windows هر ردیف با یک \r اضافه تمام می‌شود که بسیاری از خواننده‌ها آن را به یک ردیف خالی پس از هر رکورد تبدیل می‌کنند.
  • utf-8-sig فقط در نخستین نوشتن. این کدک یک نشانه ترتیب بایت یا BOM (EF BB BF) در ابتدای فایل می‌نویسد؛ مستندات codecs آن را گونه‌ای از UTF-8 معرفی می‌کند که Microsoft به کار می‌برد. Excel این نشانه را می‌خواند و فایل را به‌صورت UTF-8 رمزگشایی می‌کند، پس \u201c همان \u201c می‌ماند. وقتی اسکریپت بعدها داده اضافه می‌کند، به utf-8 ساده برمی‌گردد؛ وگرنه نشانه دوم وسط فایل می‌نشست. فایل آزمایشی ما پس از دو اجرا دقیقاً یک نشانه داشت.
  • سرستون فقط وقتی فایل تازه است. اگر در هر اجرا سرستون اضافه شود، ردیف‌های سرگردان quote_id,text,... در میان داده‌ها باقی می‌ماند.

برچسب‌ها با | به هم وصل می‌شوند، جداکننده‌ای که هرگز در مقدارها نمی‌آید.

وقتی فایلی را می‌خوانید که BOM دارد، باز هم از encoding="utf-8-sig" استفاده کنید. با utf-8 ساده، نام نخستین ستون به شکل '\ufeffquote_id' برمی‌گردد و row["quote_id"] خطای KeyError می‌دهد. ما در آزمایش به همین برخوردیم؛ دام‌های بیشتر کدگذاری در خطاهای کدگذاری Unicode در Python آمده است.

با pandas همین فایل در یک خط نوشته می‌شود، و drop_duplicates فایل CSV یا JSON Lines دارای رکورد تکراری را اصلاح می‌کند:

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")  # needs openpyxl

طبق مشخصات و محدودیت‌های Microsoft، هر کاربرگ Excel حداکثر 1,048,576 ردیف دارد. فایل CSV چنین محدودیتی ندارد، اما Excel فقط همین تعداد ردیف از آن را نشان می‌دهد.

چگونه داده‌های اسکرپینگ را در JSON یا JSON Lines ذخیره کنیم؟

یک آرایه JSON واحد برای عکس فوری‌ای مناسب است که برنامه دیگری یک‌جا بارگذاری‌اش می‌کند. آن را با ensure_ascii=False بنویسید تا متن غیرانگلیسی خوانا بماند و به دنباله‌های گریز مثل \u201c تبدیل نشود، و آن را به‌صورت اتمی بنویسید: محتوا را در یک فایل موقت در همان پوشه بریزید و سپس با os.replace جایگزینش کنید. اگر فرایند در میانه کار از بین برود، فایل قدیمی سالم می‌ماند.

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)

نقطه ضعف آرایه JSON افزودن داده است. فایل با ] تمام می‌شود، پس افزودن یک رکورد یعنی خواندن و بازنویسی کل فایل. JSON Lines از این مشکل پرهیز می‌کند: هر خط یک مقدار کامل JSON است، کدگذاری فایل UTF-8 است و خط‌ها با \n تمام می‌شوند. افزودن یک نوشتن ساده است و خرابی فقط می‌تواند به خط آخر آسیب بزند:

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 این قالب را با pd.read_json(path, lines=True) می‌خواند و chunksize= امکان می‌دهد فایل بزرگ را تکه‌تکه پردازش کنید. یک غافلگیری در آزمایش ما: مستندات read_json می‌گوید ستون‌هایی که نامشان به _at یا _time ختم می‌شود به‌طور پیش‌فرض تاریخ تفسیر می‌شوند. ستون scraped_at ما به یک datetime با منطقه زمانی تبدیل شد و to_excel() با پیام Excel does not support datetimes with timezones (یعنی Excel از تاریخ‌های دارای منطقه زمانی پشتیبانی نمی‌کند) متوقف شد. اگر می‌خواهید متن همان‌طور بماند، convert_dates=False را بدهید.

اگر فایلی با خطای JSONDecodeError بارگذاری نمی‌شود، علت‌های رایج آن (نوشتن ناقص، دو آرایه در یک فایل، خواندن JSON Lines به‌عنوان یک سند واحد) در JSONDecodeError: Expecting value آمده است.

چگونه داده‌های اسکرپینگ را بدون تکرار در SQLite ذخیره کنیم؟

کلید پایدار را کلید اصلی تعریف کنید و تصمیم میان درج و به‌روزرسانی را به پایگاه داده بسپارید. SQLite از نسخه ⁦3.24.0⁩ این کار را با عبارت upsert پشتیبانی می‌کند (درجی که اگر کلید از قبل وجود داشته باشد، به به‌روزرسانی تبدیل می‌شود)؛ در بخش DO UPDATE، پیشوند ویژه excluded. به مقدارهایی اشاره دارد که درج ردشده می‌خواست بنویسد (مستندات UPSERT در SQLite).

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 فقط وقتی نوشته می‌شود که ردیف تازه باشد؛ last_seen در هر اجرا جلو می‌رود. این جفت بی‌هزینه یک تاریخچه افزایشی به شما می‌دهد: ردیفی که last_seen آن از آخرین اجرا قدیمی‌تر است از سایت حذف شده، و ردیفی که first_seen آن با آخرین اجرا برابر است تازه است. ردیاب‌های قیمت و ابزارهای پایش تغییر دقیقاً بر همین الگو ساخته می‌شوند.

دو اجرای اسکریپت کامل پایین این خروجی را چاپ کرد:

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

فایل‌های CSV و JSON Lines همین دو اجرا هر کدام 200 رکورد داشتند. پایگاه داده 100 رکورد داشت.

وقتی داده در SQLite باشد، پرسش‌ها به پرس‌وجو تبدیل می‌شوند. این پرس‌وجوها روی پایگاه داده آزمایشی ما اجرا شدند:

python
import sqlite3

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

# Top authors
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)

# Quotes tagged "love" (tags are stored as a JSON array)
print(con.execute("""
    SELECT COUNT(*) FROM quotes, json_each(quotes.tags)
    WHERE json_each.value = 'love'""").fetchone()[0])

# Rows that were not on the site during the latest run
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

برای تحلیل، pd.read_sql_query(sql, con) نتیجه را به‌صورت DataFrame برمی‌گرداند. گام‌های پاک‌سازی که معمولاً پس از این می‌آیند در پاک‌سازی داده‌های اسکرپینگ با pandas در Python آمده است.

اسکریپت کامل: اسکرپینگ و نوشتن در CSV، JSON Lines و SQLite

اسکریپت پیوند "Next" سایت را تا وقتی ناپدید شود دنبال می‌کند، در پاسخ‌های 429 و 5xx با فاصله‌گذاری نمایی دوباره تلاش می‌کند (Retry در urllib3 سرآیند Retry-After را هم رعایت می‌کند)، میان صفحه‌ها یک ثانیه صبر می‌کند و برای هر اجرا فقط یک مهر زمانی می‌نویسد تا مقایسه‌های last_seen درست کار کنند. quotes.toscrape.com ده صفحه و 100 نقل‌قول دارد.

bash
pip install requests beautifulsoup4 lxml pandas openpyxl
python
"""Scrape quotes.toscrape.com and store the rows in CSV, JSON Lines and 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:
    # The site has no quote IDs, so we derive a stable key from the content.
    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")  # one timestamp per run
    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:  # one transaction: commit on success, roll back on error
            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: عملیات upsert را در یک تراکنش واحد می‌پیچد: اگر هر ردیفی شکست بخورد، هیچ ردیفی نوشته نمی‌شود. این عبارت اتصال را نمی‌بندد، پس بلوک finally این کار را انجام می‌دهد. نگه‌دارنده‌های نام‌دار (:quote_id) باعث می‌شوند executemany همان dictهایی را بپذیرد که نویسنده CSV به کار می‌برد و نمی‌گذارند آپاستروف‌ها دستور SQL را خراب کنند.

برای یک سایت واقعی، همین ساختار را نگه دارید و سلکتورها، کلید و فاصله انتظار را تغییر دهید. پیش از هر چیز شرایط استفاده سایت و robots.txt آن را بررسی کنید (توضیح robots.txt) و اگر API رسمی وجود دارد از آن استفاده کنید. همین فاصله یک‌ثانیه‌ای و همین تنظیمات تلاش دوباره است که کار را از خطاهای ⁦HTTP 429⁩ دور نگه می‌دارد. وقتی اسکرپر به یک خزنده زمان‌بندی‌شده تبدیل می‌شود که درخواست‌هایش را میان چند IP خروجی پخش می‌کند، پروکسی چرخشی و پروکسی مسکونی از طریق تنظیم proxies به همان requests.Session وصل می‌شوند؛ کد ذخیره‌سازی تغییری نمی‌کند.

چه زمانی باید از SQLite به PostgreSQL رفت؟

راهنمای خود پروژه SQLite درباره محدودیت‌ها صریح است: در هر فایل پایگاه داده فقط یک نویسنده در هر لحظه پشتیبانی می‌شود، و وقتی داده و برنامه روی ماشین‌های جداگانه‌اند، SQLite انتخاب درستی نیست. در یک پروژه اسکرپینگ، این به سه نشانه ترجمه می‌شود:

  • چند worker هم‌زمان می‌نویسند. فرایندهای خزنده روی سه سرور به یک سرور پایگاه داده نیاز دارند؛ چند فرایند روی یک ماشین می‌توانند نوبتی بنویسند.
  • پایگاه داده باید از راه شبکه در دسترس باشد. داشبورد، API و اسکرپری که روی میزبان‌های جداگانه‌اند باید یک PostgreSQL مشترک داشته باشند، نه یک فایل .db روی پوشه اشتراکی شبکه.
  • دیگران به مجوز دسترسی نیاز دارند. کاربران، نقش‌ها و دسترسی فقط‌خواندنی از قابلیت‌های سرور هستند.

حجم به‌تنهایی به‌ندرت دلیل مهاجرت است: حداکثر مستندشده حدود ⁦281 TB⁩ است، بسیار فراتر از آنچه یک اسکرپر تولید می‌کند. خود مهاجرت کوچک است. PostgreSQL همان نحو INSERT ... ON CONFLICT (key) DO UPDATE را با EXCLUDED به کار می‌برد، پس upsert بالا تقریباً بدون تغییر منتقل می‌شود. خط لوله‌هایی که به چند مرحله می‌رسند در ETL چیست؟ بررسی شده‌اند.

کاربردها

  • پایش قیمت: یک ردیف برای هر محصول در هر روز، با کلید SKU و تاریخ، پشتوانه پایش قیمت و ردیابی قیمت رقبا است.
  • تشخیص تغییر: first_seen و last_seen اقلام تازه و حذف‌شده را نشان می‌دهند، که هسته پایش تغییرات وب‌سایت است.
  • مجموعه‌داده‌های پژوهشی: فایل‌های JSON Lines را به‌راحتی می‌توان به تحلیلگران داد و برای تحقیقات بازار در نوت‌بوک‌ها بارگذاری کرد.
  • خزش‌های بزرگ: یک خزنده وب که هزاران URL را مرور می‌کند می‌تواند صف نشانی‌های در انتظار و نتایجش را در SQLite نگه دارد، همان‌طور که در ساخت خزنده وب با Python آمده است.
  • کارهای اسکرپینگ تکرارشونده: اجراهای زمان‌بندی‌شده استخراج داده که نباید ردیف‌های دیروز را تکرار کنند.

اشتباهات رایج

  • نبودن کلید پایدار. بدون آن، هر اجرا کل مجموعه داده را دوباره اضافه می‌کند. از شناسه سایت استفاده کنید یا از فیلدهایی که رکورد را مشخص می‌کنند هش بگیرید.
  • باز کردن فایل‌های CSV بدون newline="". نویسه‌های \r اضافه در Windows و فیلدهای چندخطی خراب.
  • استفاده از utf-8 ساده برای فایل CSV مخصوص Excel. نویسه‌های دارای علامت و گیومه‌های خمیده به دنباله‌هایی مثل é تبدیل می‌شوند.
  • نوشتن مستقیم روی فایل خروجی. یک خرابی نیمی از فایل را باقی می‌گذارد. در یک فایل موقت بنویسید و با os.replace جایگزینش کنید.
  • یک commit برای هر ردیف در SQLite. هر commit با دیسک همگام می‌شود. یک دسته را با executemany در یک تراکنش بپیچید.
  • ساختن SQL با f-string. نقل‌قولی که آپاستروف دارد دستور را خراب می‌کند. از نگه‌دارنده‌ها استفاده کنید.
  • نگه داشتن همه‌چیز در حافظه در یک خزش بزرگ. برای هر صفحه یا هر دسته بنویسید تا خرابی فقط یک صفحه را از بین ببرد، نه کل اجرا را.

راهنمای انتخاب

نیازپیشنهاد
فایلی که همکارتان در Excel باز می‌کندCSV با utf-8-sig، یا XLSX از طریق pandas
رکوردهای تودرتو برای برنامه‌ای دیگرJSON (یک آرایه)، به‌صورت اتمی نوشته‌شده
لاگی که فقط رو به رشد است و هر اجرا را ثبت می‌کندJSON Lines
اجراهای تکراری بدون رکورد تکراریSQLite با کلید اصلی و upsert
تاریخچه: هر قلم نخستین و آخرین بار کی دیده شدSQLite با first_seen و last_seen
چند ماشین که هم‌زمان می‌نویسندPostgreSQL با همان upsert مبتنی بر ON CONFLICT
تحلیل موردیهر یک از گزینه‌های بالا، بارگذاری‌شده در pandas

پرسش‌های متداول

کدام قالب برای ذخیره داده‌های اسکرپینگ مناسب‌تر است؟

بستگی دارد چه کسی آن را می‌خواند: CSV برای صفحه‌گسترده، JSON Lines برای رکوردهای تودرتو و لاگ‌های رو به رشد، SQLite برای اسکرپرهای زمان‌بندی‌شده‌ای که نباید ردیف تکراری بسازند. بسیاری از پروژه‌ها SQLite را منبع اصلی داده نگه می‌دارند و برای افراد خروجی CSV می‌گیرند.

چرا فایل CSV اسکرپ‌شده من در Excel به هم ریخته نمایش داده می‌شود؟

وقتی فایل CSV نشانه ترتیب بایت (BOM) ندارد، Excel در Windows اغلب آن را به‌جای UTF-8 با صفحه‌کد قدیمی سیستم رمزگشایی می‌کند، و هر نویسه چندبایتی به دو یا سه نویسه نادرست تبدیل می‌شود. فایل را با encoding="utf-8-sig" بنویسید، یا آن را از پنجره وارد کردن داده در Excel وارد کنید و UTF-8 را برگزینید.

چگونه داده را بدون تکرار سرستون به یک CSV موجود اضافه کنم؟

فایل را در حالت "a" باز کنید و سرستون را فقط وقتی بنویسید که فایل تازه یا خالی است، همان‌طور که append_csv در بالا انجام می‌دهد. افزودن رکوردهای تکراری را حذف نمی‌کند؛ برای این کار فایل را با pandas بارگذاری کنید و drop_duplicates را فراخوانی کنید، یا داده را با یک کلید در SQLite ذخیره کنید.

تفاوت JSON و JSON Lines چیست؟

فایل JSON یک مقدار واحد دارد، معمولاً آرایه‌ای از رکوردها، پس باید یک‌جا خوانده و نوشته شود. JSON Lines در هر خط یک مقدار JSON دارد، پس می‌توانید یک رکورد را با یک نوشتن ساده اضافه کنید و فایل بزرگ را خط به خط بخوانید.

آیا SQLite از پس یک پروژه اسکرپینگ بزرگ برمی‌آید؟

روی یک ماشین، معمولاً بله؛ محدودیت حجم بسیار بالاتر از چیزی است که یک اسکرپر تولید می‌کند. محدودیتی که اهمیت دارد هم‌زمانی است: در هر فایل فقط یک نویسنده در هر لحظه. اگر چند ماشین هم‌زمان بنویسند، به PostgreSQL نیاز دارید.

هنگام اجرای دوباره اسکرپر چگونه از ردیف‌های تکراری جلوگیری کنم؟

به هر رکورد یک کلید پایدار بدهید، آن را در SQLite کلید اصلی کنید و با ON CONFLICT(key) DO UPDATE درج کنید. در فایل‌های تخت، پس از بارگذاری با drop_duplicates(subset="key") تکراری‌ها را حذف کنید.

خلاصه

ذخیره‌سازی تعیین می‌کند که اسکرپر پس از نخستین اجرا همچنان مفید بماند یا نه. CSV با utf-8-sig و newline="" قالب تحویل برای صفحه‌گسترده است و JSON Lines ساده‌ترین راه امن برای افزودن رکوردهای ساختاریافته. SQLite با یک کلید پایدار و upsert، اجراهای تکراری را به یک جدول واحد و قابل پرس‌وجو با تاریخچه نخستین و آخرین مشاهده تبدیل می‌کند؛ وقتی چند ماشین هم‌زمان می‌نویسند، PostgreSQL جای آن را می‌گیرد. اگر اجراهای شما به جایی برسد که یک نشانی IP دیگر برای جمع‌آوری محترمانه داده کافی نباشد، پلن‌های پروکسی Proxynet را ببینید.

پرسش از ChatGPTپرسش از Claude