مقدمه
وبدیتا معمولاً آشفته، ناتمام و پر از استثناهاست. در این مقاله عملی و فنی برای یک توسعهدهنده پایتون سطح متوسط، روشهایی را برای ساختن اسپایدرهای Scrapy مقاوم و قابلاعتماد توضیح میدهیم. پس از مطالعه، شما خواهید توانست دادهها را با Items سازماندهی کنید، با Item Loaders پیشپردازش انجام دهید و با Item Pipelines پردازش و اعتبارسنجی پس از استخراج را انجام دهید؛ همچنین با استراتژیهای برخورد با حالات مرزی (edge cases) آشنا میشوید.
استراتژیها برای مواجهه با Edge Cases
قبل از طراحی ساختار داده و کد اسپایدر، بهتر است چند الگوی معمول برای مواجهه با دادههای نامنظم را در نظر بگیریم:
- Try/Except — بخشهایی از پارسینگ را در بلوکهای try/except قرار دهید تا در صورت خطا به پارسینگ جایگزین برگردید یا فیلد را با مقدار پیشفرض پر کنید.
- Conditional parsing — ابتدا DOM را بررسی کنید و بسته به وجود یا عدم وجود عنصر، مسیر پارسینگ متفاوتی انتخاب کنید (مثلاً قیمت موجود/حراج/ناموجود).
- Item Loaders — تمیزسازی اولیه و تبدیلات ساده را هنگام استخراج انجام دهید (حذف نمادهای ارز، تبدیل URL نسبی به مطلق و غیره).
- Item Pipelines — پردازشهای بعد از استخراج مثل تبدیل نوع، محاسبات ارزی، حذف موارد بدون قیمت و حذف تکراریها را در اینجا انجام دهید.
- تمیزکاری در مرحله تحلیل داده — اگر ترجیح میدهید تمام مقادیر ممکن را استخراج کنید و بعداً در جریان تحلیل پاکسازی انجام دهید، آن را مستند کنید چون پردازش در مراحل بعدی هزینه و پیچیدگی تحلیل را افزایش میدهد.
هر روش مزایا و معایب خود را دارد؛ آشنایی با هر چهار رویکرد بالا به شما انتخاب بهینه را در پروژههای مختلف میدهد. در ادامه تمرکز ما روی ترکیب Items + Item Loaders + Item Pipelines است چون بیشترین کنترل و تفکیک مسئولیت را فراهم میکنند.
سازماندهی داده با Scrapy Items
بهجای بازگرداندن دیکشنریهای آزاد، بهتر است ساختار دادهای مشخصی تعریف کنید. Items نقش اسکیمای سادهای را دارند که فیلدها را مشخص میکنند و خوانایی و ایمنی کد را افزایش میدهند.
مثال: فایل items.py که سه فیلد پایه دارد:
import scrapy
class ChocolateProduct(scrapy.Item):
name = scrapy.Field()
price = scrapy.Field()
url = scrapy.Field()توضیح: ورودی این کلاس هیچ پردازشی انجام نمیدهد؛ فقط فیلدها را تعریف میکند. خروجی هر اسپایدر از نوع این Item خواهد بود که سپس توسط Item Loaders یا Pipelines خوانده میشود.
پیشپردازش با Item Loaders
Item Loaders مکانیزمی تمیز برای جمعآوری، پاکسازی و نمایش اولیه دادهها هنگام استخراج فراهم میکنند. شما میتوانید برای هر فیلد processors تعریف کنید که هنگام اضافه شدن مقدار اجرا شوند.
نمونهای از itemsloaders.py که علامت پوند را حذف و URL نسبی را مطلق میکند:
from itemloaders.processors import TakeFirst, MapCompose
from scrapy.loader import ItemLoader
class ChocolateProductLoader(ItemLoader):
default_output_processor = TakeFirst()
# ورودی قیمت: تقسیم روی '£' و گرفتن آخرین بخش
price_in = MapCompose(lambda x: x.split("£")[-1])
# ورودی url: تبدیل نسبی به مطلق
url_in = MapCompose(lambda x: 'https://www.chocolate.co.uk' + x)توضیح پردازشگرها:
- TakeFirst خروجی فهرست را به اولین مقدار مفید تبدیل میکند (یا None).
- MapCompose یک یا چند تابع را روی هر مقدار ورودی اعمال میکند؛ خروجی میتواند تغییر نوع، حذف نویسهها یا الحاق مقدار باشد.
ادغام Item Loader با اسپایدر — قطعهای از کد parse که از Loader استفاده میکند:
import scrapy
from chocolatescraper.itemloaders import ChocolateProductLoader
from chocolatescraper.items import ChocolateProduct
class ChocolateSpider(scrapy.Spider):
name = 'chocolatespider'
start_urls = ['https://www.chocolate.co.uk/collections/all']
def parse(self, response):
products = response.css('div.product-item')
for product in products:
loader = ChocolateProductLoader(item=ChocolateProduct(), selector=product)
loader.add_css('name', 'a.product-item-meta__title::text')
loader.add_css('price', 'span.price', re='\s*(?:Sale price)?(.*)')
loader.add_css('url', 'div.product-item-meta a::attr(href)')
yield loader.load_item()
next_page = response.css('[rel="next"] ::attr(href)').get()
if next_page:
yield response.follow('https://www.chocolate.co.uk' + next_page, callback=self.parse)نکته درباره ورودیها/خروجیها:
- ورودی: هر سلکتور محصول (selector) و مقادیر خام استخراجشده.
- خروجی: یک экземпляر از ChocolateProduct که مقادیر فیلدها بعد از اجرا شدن _in processors آماده هستند.
پردازش پس از استخراج با Item Pipelines
پس از اینکه Item از اسپایدر خارج شد، وارد صف Pipeline میشود. هر کلاس Pipeline متد process_item را پیادهسازی میکند و میتواند آیتم را تغییر دهد یا با DropItem آن را حذف کند.
اولین Pipeline: تبدیل قیمت به float و اعمال نرخ تبدیل:
from itemadapter import ItemAdapter
from scrapy.exceptions import DropItem
class PriceToUSDPipeline:
gbpToUsdRate = 1.3
def process_item(self, item, spider):
adapter = ItemAdapter(item)
# اگر قیمت وجود دارد آن را به float تبدیل کن و نرخ تبدیل را اعمال کن
if adapter.get('price'):
float_price = float(adapter['price'])
adapter['price'] = float_price * self.gbpToUsdRate
return item
else:
# حذف آیتمهایی که قیمت ندارند (مثلاً محصول ناموجود)
raise DropItem(f"Missing price in {item}")توضیح عملکرد:
- ورودی: آیتمی که فیلد price به صورت رشته دارد (مثلاً "8.50").
- کاری که انجام میدهد: تبدیل به عدد شناور و ضرب در نرخ تبدیل.
- خروجی: آیتم تغییرشده یا DropItem.
دومین Pipeline: حذف موارد تکراری بر اساس نام:
from itemadapter import ItemAdapter
from scrapy.exceptions import DropItem
class DuplicatesPipeline:
def __init__(self):
self.names_seen = set()
def process_item(self, item, spider):
adapter = ItemAdapter(item)
name = adapter.get('name')
if name in self.names_seen:
raise DropItem(f"Duplicate item found: {item!r}")
else:
self.names_seen.add(name)
return itemنکتهها درباره حذف تکراریها:
- روش مبتنی بر مجموعه در حافظه برای یک اجرای محلی ساده خوب است اما در مقیاس توزیعشده یا اجرای طولانیمدت باید از ذخیره بیرونی (مثلاً Redis، دیتابیس یا فایل) استفاده کنید.
- استفاده از شناسه یا fingerprint به جای نام متن میتواند پایایی را بالا ببرد.
فعالسازی Pipelines در settings.py:
ITEM_PIPELINES = {
'chocolatescraper.pipelines.PriceToUSDPipeline': 100,
'chocolatescraper.pipelines.DuplicatesPipeline': 200,
}اعداد ترتیب اجرای Pipelineها را تعیین میکنند (از کوچک به بزرگ).
تست، عملکرد و نکات عملی
بعد از راهاندازی، خروجی دیباگ باید آیتمها را نشان دهد و ببینید که قیمتها به دلار تبدیل شده و موارد تکراری حذف شدهاند. نمونه لاگ:
DEBUG: Scraped from <200 https://www.chocolate.co.uk/collections/all>
{'name': 'Blonde Chocolate Honeycomb', 'price': 11.63, 'url': 'https://www.chocolate.co.uk/products/blonde-chocolate-honeycomb'}نکات مهم در تولید و امنیت:
- Rate limiting: با DOWNLOAD_DELAY و محدود کردن همزمانی از بار زیاد روی سرور جلوگیری کنید.
- Retry و خطای شبکه: از RetryMiddleware یا منطق ریتری در درخواستها برای شبکههای ناپایدار استفاده کنید.
- User-Agent و پراکسی: برای جلوگیری از بلاک شدن، چرخش User-Agent و استفاده از پراکسیهای مناسب مهم است (اما قوانین سایت و شرایط استفاده را رعایت کنید).
- قابلاعتماد بودن نرخ تبدیل: نرخ تبدیل را هاردکد نکنید؛ بهتر است از سرویس خارجی یا فایل پیکربندی خوانده یا با dependency injection تزریق کنید تا در تستها قابل کنترل باشد.
- پردازش موازی و حافظه: اگر تعداد آیتمها زیاد است، از پایپلاینهایی استفاده کنید که I/O را دستهای (batch) انجام دهند یا از ذخیرهسازی تدریجی استفاده کنید تا حافظه پر نشود.
- قانونی و اخلاقی: همیشه robots.txt و شرایط استفاده سایت را بررسی کنید و از scraping برای اهدافی که مجاز نیستند خودداری کنید.
بهبودها و سناریوهای واقعی
چند ایده برای پروژههای بعدی یا مقیاسبندی:
- استفاده از fingerprint یا شناسه یکتا برای تشخیص تکراریها در چند اجرای مختلف یا در کلستر.
- ذخیرهسازی موقتی وضعیت (مثلاً تا کجا رفتهایم) در دیتابیس یا در صف پیام برای ادامهی کراول بعد از وقفه.
- برای سایتهای جاوااسکریپتی، استفاده از رندر سرور-ساید یا ابزارهایی مثل Headless browser (فقط در صورت نیاز) و سپس همان الگوهای Items/Loaders/Pipelines را اعمال کنید.
جمعبندی
با سازماندهی دادهها با Items، پاکسازی هنگام استخراج با Item Loaders و پردازش نهایی با Item Pipelines میتوانید اسپایدرهای Scrapy بسازید که هم خواناتر و هم مقاومتر در برابر حالات مرزی هستند. توصیه نهایی: پردازشها را تفکیک کنید، تست بنویسید و هنگام مقیاسبندی از ذخیرهسازی خارجی برای وضعیت و حذف تکراریها استفاده کنید. در قسمت بعدی (ذخیرهسازی) خواهید دید چگونه خروجی را به فایل، دیتابیس یا S3 بفرستید.





