ProxynetProxynet

پاک‌سازی داده‌های اسکرپینگ با pandas در Python

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

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

Acar Diveroli
نویسنده: Acar Diveroli
یک NaN بزرگ میان خط‌های راهنمای نوع داده و نواری آبی با خط‌کشی جدول؛ کارت‌ها قیمتی متنی را به 51.77 تبدیل می‌کنند

اسکرپر شما شب کارش را تمام کرده و فایل CSV در نگاه اول درست به نظر می‌رسد. بعد می‌خواهید میانگین قیمت‌ها را بگیرید و pandas نمی‌پذیرد، چون هر قیمت رشته‌ای است مثل £51.77. یک کتاب دو بار آمده است، چون هم در یکی از صفحه‌های کاتالوگ بوده و هم در صفحه یک دسته. یک عنوان به شکل Shakespeare’s خوانده می‌شود، هفت کتاب در دسته‌ای به نام "Default" نشسته‌اند و یک ردیف اصلاً توضیح ندارد. هیچ‌کدام از این‌ها باگ اسکرپر نیست. داده خام وب همین شکلی است و پیش از آنکه کسی از آن نمودار بکشد، به یک دور پاک‌سازی نیاز دارد.

این راهنما آن دور پاک‌سازی را با pandas روی یک مجموعه داده واقعی انجام می‌دهد که از books.toscrape.com جمع شده است؛ سایتی آزمایشی که برای تمرین وب اسکرپینگ ساخته شده. ترمیم متن و یونیکد، مقادیر گمشده، تبدیل قیمت‌های متنی به عدد، عددهای دیگری که درون متن پنهان‌اند، تاریخ‌ها، برچسب‌های دسته، ردیف‌های تکراری، داده‌های پرت، بررسی‌های اعتبارسنجی و ذخیره نتیجه را پوشش می‌دهیم. اسکریپت کامل در 28 سپتامبر 2026 با pandas 3.0.6، Requests 2.34.2، ⁦beautifulsoup4 4.15.0⁩، pyarrow 25.0.1 و Python 3.13 اجرا شد.

داده اسکرپینگ «تمیز» یعنی چه؟

داده تمیز برای هر چیز واقعی یک ردیف دارد، برای هر ستون یک نوع و برای هر مقدار یک شکل نوشتاری. ستون قیمت عدد نگه می‌دارد، نه متنی با نماد ارز. کتابی که دو بار اسکرپ شده فقط یک بار می‌آید. توضیحی که وجود ندارد به‌عنوان گمشده ثبت می‌شود، نه به شکل رشته‌ای خالی که پر به نظر می‌رسد. دسته‌ای به نام "Default" نامی می‌گیرد که به تحلیلگر بگوید هیچ اطلاعاتی در آن نیست.

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

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

صفحه‌های وب برای آدم‌ها نوشته می‌شوند و اسکرپر همان بخش‌هایی را می‌خواند که برای چشم ساخته شده‌اند. بیشتر این آشفتگی از چند منبع می‌آید:

  • همه چیز متن است. HTML نوع داده ندارد. £51.77، In stock (22 available) و Three (یک کلاس CSS برای امتیاز ستاره‌ای) همه به شکل رشته می‌رسند.
  • حدس رمزگذاری اشتباه از آب درمی‌آید. books.toscrape.com هدر Content-Type: text/html را بدون charset می‌فرستد. در این حالت Requests از قاعده قدیمی HTTP/1.1 پیروی می‌کند و صفحه را با ⁦ISO-8859-1⁩ رمزگشایی می‌کند (مستندات Requests درباره رمزگذاری)، و نماد پوند ⁦UTF-8⁩ به £ تبدیل می‌شود. سازوکار آن در خطاهای رمزگذاری یونیکد در پایتون توضیح داده شده است.
  • یک کالا چند مسیر دارد. یک محصول در کاتالوگ، در دسته خودش و گاهی در فهرست «جدیدها» یا «حراج» آمده است. اگر بیش از یکی از این‌ها را بپیمایید، آن را دو بار جمع می‌کنید. صفحه‌بندی که میان اجراها جابه‌جا می‌شود هم همین کار را می‌کند.
  • قالب‌ها با هم فرق دارند. از هر هزار صفحه یکی بلوکی را که بقیه دارند ندارد و انتخابگر شما None برمی‌گرداند.
  • داده خود سایت بی‌نقص نیست. دسته‌های جای‌نگهدار، فاصله‌های سرگردان و دنباله‌های "...more" بخشی از منبع‌اند، نه اشتباه شما.

یک دور پاک‌سازی چگونه کار می‌کند؟

ترتیب مهم است، چون هر گام به گام قبلی تکیه دارد:

  1. داده خام را نگه دارید. دقیقاً همان چیزی را که اسکرپر جمع کرده در books_raw.csv ذخیره کنید. پاک‌سازی روی یک نسخه کار می‌کند، پس می‌توانید آن را با قواعد تازه دوباره اجرا کنید بدون آنکه دوباره اسکرپ کنید.
  2. متن را یکدست کنید. فاصله‌های دو سر را بردارید، رشته‌های خالی را به مقدار گمشده تبدیل کنید، رمزگذاری خراب را ترمیم کنید و نرمال‌سازی یونیکد را اعمال کنید.
  3. مقادیر گمشده را بشمارید. آن‌ها را در یک گزارش ثبت کنید و ستون نشانگر اضافه کنید؛ با حدس پرشان نکنید.
  4. قیمت‌ها را تبدیل کنید. نماد ارز را به ستون جداگانه ببرید و باقی را به float تبدیل کنید.
  5. عددهای دیگر را بیرون بکشید. موجودی انبار از In stock (22 available)، امتیاز از روی کلمه‌ها، تعداد نقدها از متن.
  6. تاریخ‌ها را تجزیه کنید و به مهر زمانی دارای منطقه زمانی تبدیل کنید.
  7. دسته‌ها را یکدست کنید تا هر مفهوم فقط یک برچسب داشته باشد.
  8. ردیف‌های تکراری را حذف کنید، بر اساس یک کلید پایدار و پس از درست شدن نوع‌ها.
  9. داده‌های پرت را علامت بزنید، با قاعده‌ای ساده که بتوانید توضیحش دهید.
  10. اعتبارسنجی و ذخیره کنید. اگر یک بررسی شکست خورد متوقف شوید؛ وگرنه CSV و Parquet را بنویسید.

حذف ردیف‌های تکراری عمداً در انتها می‌آید. دو ردیف که فقط در یک فاصله انتهایی یا یک آپاستروف بد رمزگشایی‌شده فرق دارند یک کتاب‌اند، و drop_duplicates این را فقط پس از گام‌های 2 تا 7 می‌بیند.

مقدار خام و تمیز کنار هم

این‌ها مقدارهای واقعی از اسکرپ و چیزی است که اسکریپت از آن‌ها می‌سازد:

ستونمقدار خاممشکلمقدار تمیزابزار pandas
price£51.77متن، رمزگذاری خراب51.77 (float64) + currency = £str.extract، pd.to_numeric
availabilityIn stock (22 available)عدد درون جملهin_stock = 22 (Int64)str.extract
ratingThreeکلمه به‌جای عدد3 (Int64)map
titleShakespeare’s Globeموجی‌باکه (mojibake)Shakespeare’s Globeencode/decode، unicodedata
categoryDefaultبرچسب جای‌نگهدارUncategorized (category)replace، astype("category")
description… and their affair begins. ...moreتکه‌های صفحه درون متن… and their affair begins.str.removesuffix
scraped_at2026-09-28T14:51:11+00:00رشتهdatetime64[us, UTC]pd.to_datetime
upcیک کد در دو ردیفکتاب تکرارییک ردیف، تازه‌ترین نسخهdrop_duplicates

موجی‌باکه (mojibake) نام متن درهم‌ریخته‌ای است که وقتی بایت‌ها با رمزگذاری اشتباه رمزگشایی شوند پدید می‌آید، مثل ’ به‌جای ’.

در pandas 3.0 چه چیزی تغییر کرد؟

pandas 3.0.0 در 21 ژانویه 2026 منتشر شد و نسخه فعلی 3.0.6 است. سه تغییر از یادداشت‌های انتشار pandas 3.0 مستقیماً بر کد پاک‌سازی اثر می‌گذارند:

  • رشته‌ها نوع مخصوص خود را دارند. ستون‌های متنی حالا به‌جای object به‌صورت str شناخته می‌شوند و مقادیر گمشده در آن‌ها مثل دیگر نوع‌های پیش‌فرض NaN است. به همین دلیل اسکریپت ستون‌های متنی را با select_dtypes(include=["object", "string"]) انتخاب می‌کند: هم در pandas 2 کار می‌کند و هم در 3.
  • Copy-on-Write تنها حالت است. انتساب زنجیره‌ای مثل df["price"][0] = 10 دیگر df را تغییر نمی‌دهد؛ pandas 3.0.6 فقط هشدار ChainedAssignmentError چاپ می‌کند. از df.loc[0, "price"] = 10 استفاده کنید. هشدار قدیمی SettingWithCopyWarning حذف شده و فراخوانی‌های احتیاطی .copy() که برای ساکت کردن آن نوشته می‌شدند دیگر لازم نیستند.
  • تاریخ‌های تجزیه‌شده به‌طور پیش‌فرض میکروثانیه‌ای‌اند. pd.to_datetime روی رشته‌ها حالا به‌جای نانوثانیه datetime64[us] برمی‌گرداند؛ همان چیزی که در خروجی پایین می‌بینید.

pandas 3 همچنین به Python 3.11 یا جدیدتر نیاز دارد. pd.to_numeric فقط errors="raise" یا errors="coerce" را می‌پذیرد (مرجع to_numeric)؛ قطعه‌کدهای قدیمی که errors="ignore" می‌دهند شکست می‌خورند.

اسکریپت کامل: اسکرپ books.toscrape.com و پاک‌سازی آن

کتابخانه‌ها را در یک محیط مجازی نصب کنید. pyarrow برای فایل Parquet لازم است:

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

اسکرپر دو صفحه از کاتالوگ و دو صفحه دسته (Poetry و Classics) را می‌خواند، روی هم 78 کتاب، با یک ثانیه مکث میان درخواست‌ها و تلاش مجدد خودکار برای پاسخ‌های 429 و ⁦5xx⁩. تجزیه HTML همان روش آموزش BeautifulSoup ما را دنبال می‌کند. اجرای دوم books_raw.csv را می‌خواند و فقط پاک‌سازی را تکرار می‌کند.

python
"""Scrape books.toscrape.com, then clean the result with pandas."""
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",                             # main catalogue
    BASE + "catalogue/category/books/poetry_23/index.html",     # one category
    BASE + "catalogue/category/books/classics_6/index.html",    # another one
]
MAX_PAGES = 2          # list pages per start URL
DELAY = 1.0            # seconds between requests
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   # decoded with Requests' guess, on purpose: see step 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. Strip whitespace in every text column; empty strings become missing
    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: repair mojibake, then normalise to NFKC
    def fix_text(s):
        if pd.isna(s):
            return s
        try:
            s = s.encode("latin-1").decode("utf-8")   # "£" -> "£", "é" -> "é"
        except (UnicodeEncodeError, UnicodeDecodeError):
            pass                                        # already correct
        return unicodedata.normalize("NFKC", s)

    for col in ["title", "category", "description", "price"]:
        df[col] = df[col].map(fix_text)

    # 3. Missing values: count them, flag them, don't invent them
    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. Price strings to numbers
    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. Other numbers hidden in text
    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. Dates
    df["scraped_at"] = pd.to_datetime(df["scraped_at"], utc=True, errors="coerce")

    # 7. Categories: one spelling per value, junk labels merged
    df["category"] = (df["category"]
                      .replace({"Default": "Uncategorized",
                                "Add a comment": "Uncategorized"})
                      .astype("category"))

    # 8. Duplicates: same UPC = same book; keep the newest copy
    report["duplicate_rows"] = int(df.duplicated("upc").sum())
    df = (df.sort_values("scraped_at")
            .drop_duplicates("upc", keep="last")
            .reset_index(drop=True))

    # 9. Outliers: flag, don't delete
    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())

اجرای اول حدود دو دقیقه طول کشید که بیشترش همان مکث مؤدبانه میان درخواست‌ها بود. خط گزارشی که چاپ کرد:

text
{'raw_rows': 78, 'missing_before': {'description': 1}, 'duplicate_rows': 5, 'price_outliers': 0, 'clean_rows': 73}

پنج کتاب Poetry هم در صفحه‌های کاتالوگ بودند و هم در صفحه Poetry، پس 78 ردیف به 73 رسید. یک عنوان از Classics یعنی Alice in Wonderland در صفحه خود بلوک توضیح ندارد. هفت کتاب دسته "Default" داشتند. قیمت‌ها از 12.84 تا 58.63 بودند، پس قاعده داده پرت چیزی را علامت نزد؛ که برای این سایت پاسخ درستی است. خروجی dtypes برای متن str، برای قیمت float64، برای شمارش‌ها Int64، برای دسته category و برای مهر زمانی datetime64[us, UTC] را نشان می‌دهد.

هر گام چه می‌کند و چرا

متن و یونیکد

str.strip() فاصله‌ها و شکست‌های خطی را که چیدمان HTML دور مقدارها جا می‌گذارد برمی‌دارد. تبدیل "" به مقدار گمشده مهم‌تر از آن است که به نظر می‌رسد: isna() رشته‌های خالی را نمی‌شمارد، پس بدون این کار یک فیلد خالی پر حساب می‌شود.

ترمیم رمزگذاری حدس اشتباه را برمی‌گرداند. s.encode("latin-1").decode("utf-8") نویسه‌هایی را که Requests ساخته به بایت‌های اصلی برمی‌گرداند و آن بایت‌ها را با ⁦UTF-8⁩ رمزگشایی می‌کند. رشته‌ای که از قبل درست بوده یا بدون تغییر از این مرحله می‌گذرد (ASCII ساده) یا در یکی از این دو فراخوانی خطا می‌دهد و دست‌نخورده می‌ماند. راه‌حل تمیزتر در مبدأ است: response.content را به BeautifulSoup بدهید یا پیش از خواندن response.text مقدار response.encoding = "utf-8" را تنظیم کنید. با این حال، دانستن این ترمیم برای فایل‌هایی که قبلاً ذخیره کرده‌اید ارزشمند است. پس از آن، unicodedata.normalize("NFKC", s) نویسه‌های سازگاری مثل لیگاتورها و فاصله‌های نشکن را به شکل ساده‌شان برمی‌گرداند (مستندات unicodedata)، تا دو شکل نوشتاری یک عنوان برابر شمرده شوند.

مقادیر گمشده

اسکریپت مقادیر گمشده هر ستون را می‌شمارد و ستون نشانگر has_description را اضافه می‌کند. در جای خالی «بدون توضیح» نمی‌نویسد، چون این رشته بعدها شبیه متن واقعی به نظر می‌رسد. برای عددها، پر کردن جاهای خالی با میانگین یا میانه کار مرحله تحلیل است، جایی که تحلیلگر می‌تواند آن را روی نمودار بگوید، نه کار مرحله پاک‌سازی.

عددها، امتیازها و تاریخ‌ها

pd.to_numeric(..., errors="coerce") هر چیزی را که نتواند تجزیه کند به‌جای توقف به NaN تبدیل می‌کند. برای یک دور پاک‌سازی این پیش‌فرض درستی است، به شرطی که یک بررسی اعتبارسنجی پس از آن این NaNها را بشمارد. نوع nullable یعنی Int64 (با I بزرگ) عددهای صحیح را حتی وقتی مقداری گم است صحیح نگه می‌دارد؛ int64 معمولی نمی‌تواند NaN داشته باشد و بی‌صدا به float تبدیل می‌شود. تاریخ‌ها با utc=True تجزیه می‌شوند تا مهرهای زمانی اجراها و ماشین‌های مختلف درست با هم مقایسه شوند.

دسته‌ها و ردیف‌های تکراری

astype("category") هر برچسب را یک بار ذخیره می‌کند و برای هر ردیف یک کد کوچک نگه می‌دارد. جدا از صرفه‌جویی در حافظه، فهرست برچسب‌ها را صریح می‌کند: df["category"].cat.categories با یک نگاه نشان می‌دهد که آیا "Poetry" و "poetry " هر دو باقی مانده‌اند یا نه.

برای ردیف‌های تکراری، کلیدی انتخاب کنید که خود چیز را شناسایی کند، نه ردیف را. اینجا آن کلید UPC است که در صفحه هر کتاب چاپ شده. در این سایت URL هم کار می‌کند، اما در فروشگاه‌های واقعی یک محصول اغلب چند URL دارد (پارامترهای ردیابی، مسیرهای دسته). مرتب کردن بر اساس scraped_at پیش از drop_duplicates(keep="last") تازه‌ترین نسخه را نگه می‌دارد؛ همان چیزی که وقتی قیمت‌ها میان اجراها تغییر می‌کنند می‌خواهید.

داده‌های پرت و اعتبارسنجی

قاعده دامنه میان‌چارکی (IQR) مقدارهایی را علامت می‌زند که بیش از 1.5 برابر IQR پایین‌تر از چارک اول یا بالاتر از چارک سوم باشند. توضیح آن ساده است و توزیع نرمال را فرض نمی‌گیرد. اسکریپت به‌جای حذف ردیف‌ها ستون price_outlier را اضافه می‌کند: قیمت 0.99 در یک فروشگاه واقعی ممکن است یک حراج واقعی باشد یا خطای تجزیه‌ای که "0.99" را از "10.99" خوانده است. پیش از حذف هر چیزی، یک نفر باید آن را ببیند.

validate() آخرین خط دفاع است. وقتی یک قیمت خام را به N/A و یک امتیاز را به Six تغییر دادیم، اجرا به‌جای نوشتن یک فایل خراب با پیام Validation failed: prices that did not parse, ratings missing or outside 1-5 متوقف شد. این بررسی‌ها را به قواعد واقعی داده خود نزدیک نگه دارید؛ بررسی‌ای که هرگز شکست نمی‌خورد از چیزی محافظت نمی‌کند.

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

  • پایش قیمت: قیمت‌های عددی همراه با مهر زمانی همان چیزی است که تاریخچه قیمت و هشدار قیمت لازم دارند. پایش قیمت و رصد قیمت رقبا را ببینید.
  • تحقیقات بازار: سهم دسته‌ها، توزیع امتیازها و سطح موجودی در کل یک کاتالوگ. تحقیقات بازار را ببینید.
  • داده‌کاوی و مدل‌سازی: مدل‌ها به ورودی‌های نوع‌دار و بدون تکرار نیاز دارند. زمینه را در داده‌کاوی بخوانید.
  • پایپ‌لاین‌ها: این اسکریپت گام «تبدیل» یک کار ETL کوچک است؛ الگوی کامل در ETL چیست؟ آمده است.
  • ذخیره‌سازی: وقتی ستون‌ها نوع‌دار شدند، انتخاب میان CSV، JSON و SQLite در ذخیره داده‌های اسکرپینگ در CSV، JSON و SQLite بررسی شده است.
  • جمع‌آوری در مقیاس بزرگ: پروژه‌های اسکرپینگ بزرگ‌تر و زیرساخت پیرامون آن‌ها در صفحه جمع‌آوری داده ما آمده است.

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

  • پاک‌سازی تنها نسخه. بازنویسی فایل خام یعنی هر تغییر قاعده به یک اسکرپ تازه نیاز دارد.
  • حذف ردیف‌های تکراری در ابتدا. drop_duplicates مقدارهای دقیق را مقایسه می‌کند، پس اگر آن را پیش از برداشتن فاصله‌ها اجرا کنید "Olio" و "Olio " هر دو باقی می‌مانند.
  • astype(float) روی قیمت‌های متنی. روی اولین £ شکست می‌خورد. نویسه‌های غیرعددی را بردارید، سپس pd.to_numeric را با errors="coerce" به کار ببرید و NaNهای حاصل را بشمارید.
  • پر کردن جاهای خالی با صفر. موجودی گمشده به معنای موجودی صفر نیست. از Int64 استفاده کنید و مقدار را گمشده بگذارید.
  • انتساب زنجیره‌ای. در pandas 3، df[df.price > 50]["flag"] = True چیزی را تغییر نمی‌دهد و فقط هشدار می‌دهد. از df.loc[df.price > 50, "flag"] = True استفاده کنید.
  • ذخیره برای Excel بدون BOM. Excel در Windows یک CSV ساده ⁦UTF-8⁩ را با یک صفحه‌کد قدیمی می‌خواند و £ دوباره خراب می‌شود؛ encoding="utf-8-sig" جلوی این را می‌گیرد.
  • اسکرپ بیشتر به‌جای پاک‌سازی بهتر. اگر داده به این دلیل گم شده که درخواست‌ها شکست خورده‌اند، جمع‌آوری را درست کنید: به robots.txt احترام بگذارید، در برابر ⁦HTTP 429⁩ آهسته‌تر پیش بروید و هر جا API رسمی هست آن را ترجیح دهید.

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

نیازتوصیه
عددهایی که به شکل متن ذخیره شده‌اندنمادها را بردارید، pd.to_numeric(errors="coerce")، سپس NaNها را بشمارید
عدد صحیح با جای خالیInt64 از نوع nullable، نه int64 یا float
متن دارای £، ’ یا éرمزگشایی را در مبدأ درست کنید؛ فایل‌های ذخیره‌شده را با رفت‌وبرگشت latin-1 ترمیم کنید
یک کالا از چند صفحهdrop_duplicates روی یک شناسه پایدار، با نگه‌داشتن تازه‌ترین نسخه
چند قیمت عجیببا قاعده IQR علامت بزنید و بازبینی کنید؛ خودکار حذف نکنید
برچسب‌های تکرارشونده مثل دسته‌هاastype("category") و یک نگاشت صریح برای برچسب‌های جای‌نگهدار
مهرهای زمانی از اجراهای مختلفpd.to_datetime(..., utc=True)
حفظ نوع‌ها میان گام‌هاParquet را کنار CSV ذخیره کنید
اسکرپ‌های بزرگ یا تکرارشوندهمحدودیت نرخ، تلاش مجدد و هر جا حجم کار لازم داشت پروکسی چرخشی از طریق pr.proxynet.io:8000

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

داده را هنگام اسکرپ پاک کنم یا بعد از آن؟

هنگام اسکرپ حداقل کار را انجام دهید (متن را بگیرید و فاصله‌های اضافه‌اش را بردارید) و باقی را بعداً در pandas. نگه‌داشتن فایل خام به شما امکان می‌دهد قواعد پاک‌سازی را تغییر دهید و در چند ثانیه دوباره اجرا کنید، بی‌آنکه حتی یک درخواست تازه به سایت بفرستید.

چرا ستون قیمت من به‌جای £ نماد £ را نشان می‌دهد؟

سرور charset را اعلام نکرده بود، پس Requests بایت‌های ⁦UTF-8⁩ را با ⁦ISO-8859-1⁩ رمزگشایی کرد. response.content را به تجزیه‌گر خود بدهید یا response.encoding = "utf-8" را تنظیم کنید. برای داده‌ای که قبلاً ذخیره شده، s.encode("latin-1").decode("utf-8") این اشتباه را برمی‌گرداند.

آیا dropna() راه درست برخورد با مقادیر گمشده است؟

فقط وقتی ردیفِ بدون آن فیلد برای پرسش شما بی‌فایده است. dropna() بدون آرگومان هر ردیفی را که در هر ستونی مقدار گمشده داشته باشد حذف می‌کند، و این می‌تواند بیشتر یک مجموعه داده اسکرپ‌شده را به خاطر یک فیلد اختیاری از بین ببرد. برای ستون‌هایی که واقعاً لازم دارید از dropna(subset=[...]) استفاده کنید و بقیه را علامت بزنید.

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

از شناسه‌ای استفاده کنید که روی صفحه چاپ شده است، مثل UPC، SKU، ISBN یا شناسه محصول. URL فقط در سایت‌هایی کار می‌کند که هر کالا دقیقاً یک URL دارد. اگر شناسه‌ای نیست، چند فیلد یکدست‌شده را با هم ترکیب کنید، مثلاً عنوان و برند.

آیا این کد با pandas 2 کار می‌کند؟

ما آن را فقط با pandas 3.0.6 آزمودیم، اما هر فراخوانی که به کار می‌برد در ⁦pandas 2.x⁩ هم وجود دارد و انتخاب ستون‌های متنی هم نوع قدیمی object و هم نوع جدید str را پوشش می‌دهد. انتظار تفاوت‌های کوچکی در خروجی چاپ‌شده را داشته باشید، مثل object به‌جای str و مهرهای زمانی نانوثانیه‌ای به‌جای میکروثانیه‌ای. کدی که به انتساب زنجیره‌ای تکیه دارد اصلی‌ترین بخشی است که میان این دو نسخه رفتار متفاوتی دارد.

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

نه. پاک‌سازی روی دستگاه شما اجرا می‌شود و هیچ ترافیکی نمی‌فرستد. پروکسی فقط برای جمع‌آوری اهمیت دارد، آن هم فقط در حجمی که یک نشانی IP از محدودیت نرخ یک سایت فراتر برود. کد پاک‌سازی هر شبکه‌ای که صفحه‌ها از آن آمده باشند یکسان می‌ماند.

خلاصه

داده اسکرپینگ به شکل متنی می‌رسد که برای خواننده انسانی قالب‌بندی شده است. یک دور پاک‌سازی قابل اعتماد فایل خام را نگه می‌دارد، اول متن و رمزگذاری را درست می‌کند، هر ستون را به نوع واقعی تبدیل می‌کند، ردیف‌های تکراری را بر اساس یک کلید پایدار حذف می‌کند، مقدارهای عجیب را به‌جای حذف علامت می‌زند و اگر بررسی‌ای شکست بخورد پیش از ذخیره متوقف می‌شود. اسکریپت بالا همه این کارها را روی 78 ردیف واقعی، پس از دانلود صفحه‌ها، در کمتر از یک ثانیه انجام می‌دهد. وقتی جمع‌آوری شما از یک سایت تمرینی فراتر رفت، پلن‌های صفحه پروکسی ما بخش شبکه را پوشش می‌دهند.

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