Scrape «موفق» که قیمت را به‌صورت رشتهٔ «۱۲,۵۰۰,۰۰۰ تومان» و «12.5 میلیون» در دو فیلد مختلف ذخیره کند، برای dashboard خطرناک است. پاک‌سازی (cleaning) یعنی حذف نویز و آماده‌سازی برای parse؛ نرمال‌سازی (normalization) یعنی یک واحد و یک نوع در کل dataset. این مقاله کاربردی است — بعد از استخراج از وب و قبل از انتخاب فرمت ذخیره.

خلاصه
  • داده خام را نگه دارید (ردپای منبع) اما مصرف‌کننده فقط نسخه یکدست‌شده ببیند.
  • قیمت → integer تومان؛ موجودی → boolean؛ تاریخ → ISO در DB.
  • validation جدا از parse — رکورد بد به quarantine نه صفر ساکت.

نمونه raw از منبع

{
  "title": "  گوشی X  ",
  "price_raw": "۱۲,۵۰۰,۰۰۰ تومان",
  "alt_price": "12.5 میلیون",
  "stock": "ناموجود",
  "listed_at": "۱۴۰۴/۰۱/۰۵",
  "url": "/p/991"
}

نمونه نمایشی — خروجی parser قبل از pipeline کیفیت

پاک‌سازی

trim فاصلهٔ عنوان، حذف تگ HTML باقی‌مانده، یکسان‌سازی فاصلهٔ نیم‌فاصله در صورت نیاز، رد رکورد بدون شناسه منبع (SKU). اگر چند فیلد قیمت دارید، rule انتخاب منبع حقیقت (اولویت فیلد) تعریف کنید.

Normalization

  • قیمت: ارقام فارسی به لاتین، حذف جداکننده، تبدیل «میلیون» به ضرب — خروجی price_toman: 12500000 (طبق schema پروژه: تومان integer).
  • موجودی: «ناموجود» / «اتمام» → in_stock: false؛ نه رشتهٔ آزاد در نمودار.
  • URL: نسبی → مطلق با urljoin(base, path).
  • تاریخ: شمسی در UI → Gregorian ISO در DB برای sort و join؛ نمایش شمسی فقط در لایه presentation (jalali در قالب).

Type conversion و null

رشتهٔ خالی = null. قیمت غیرقابل parse → quarantine با دلیل، نه 0 که فریب‌دهنده است. boolean از «بله/خیر» فارسی با map ثابت.

Deduplication

کلید یکتا مثلاً source_id یا hash URL canonical. همان محصول با دو URL → merge با آخرین scraped_at. dedupe روی title خام قبل از normalize اشتباه رایج است.

Validation

schema ساده: price_toman > 0 اگر in_stock؛ عنوان حداقل ۳ کاراکتر؛ URL با scheme معتبر. شکست validation → لاگ + نمونه برای QA؛ اگر نرخ شکست بالا رفت شاید selector خراب شده — با circuit breaker هم‌راستا کنید.

خام→پاک→یکدست→اعتبارسنجی→ذخیره
نمونه نمایشی — خط لوله کیفیت
python">
import re

FA_DIGITS = str.maketrans("۰۱۲۳۴۵۶۷۸۹", "0123456789")

def parse_price_toman(text: str) -> int | None:
    if not text or "ناموجود" in text:
        return None
    t = text.translate(FA_DIGITS)
    if "میلیون" in t:
        m = re.search(r"([\d.]+)\s*میلیون", t)
        return int(float(m.group(1)) * 1_000_000) if m else None
    digits = re.sub(r"[^\d]", "", t)
    return int(digits) if digits else None

بعد از normalize

{
  "title": "گوشی X",
  "price_toman": 12500000,
  "in_stock": false,
  "listed_at": "2025-03-25",
  "url": "https://example.com/p/991",
  "scraped_at": "2026-03-19T12:00:00Z"
}

نمونه نمایشی — آماده upsert

نقش AI

برای دسته‌بندی متن آزاد یا استخراج از پاراگراف بدون ساختار ثابت، مدل می‌تواند کمک کند — بعد از rule-based و با نمونهٔ human review. جزئیات در AI در وب‌اسکرپینگ.

اشتباه رایج

فقط raw در DB بدون نسخه parser — بعد از fix قیمت، دادهٔ تاریخی غیرقابل مقایسه می‌ماند.

در عمل چه انتخابی کنیم؟

ماژول جدا و unit-test برای normalize (ورودی‌های فارسی/انگلیسی). raw در archive یا JSONB برای debug؛ محصول و BI روی جدول normalized. صفحه‌بندی منبع: Pagination.