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 همراستا کنید.
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.