مقدمه
در این مقاله یاد میگیرید چگونه دادههای ناساختاریافته HTML را با استفاده از Scrapy به دادههای تمیز و قابل استفاده تبدیل کنید. تمرکز ما روی Item و ItemLoader است: چطور ساختار مدلشده بسازیم، پردازش ورودی/خروجی را جدا کنیم، خطاها را دیباگ کنیم و در نهایت خروجیٔ قابل اطمینان (JSON/CSV) تولید کنیم. در پایان نمونههای عملی شامل BookItem، BookLoader و اسپایدرهای مرتبط را خواهید دید.
چرا از Item به جای dict استفاده کنیم؟
در پروژههای ساده ممکن است همیشه خروجیها را با dict بازگردانید، اما Item مزایای زیر را دارد:
- تعریف صریح فیلدها و مقدار پیشفرض که یکنواختی داده را تضمین میکند.
- جدا کردن منطق تمیزسازی از اسپایدر و افزایش قابلیت نگهداری.
- هماهنگی بهتر با pipelineها و خروجیگیرها.
نمونهٔ سادهٔ تعریف یک آیتم کتاب:
import scrapy
class BookItem(scrapy.Item):
title = scrapy.Field(default="n/a")
price = scrapy.Field(default="0.00")
availability = scrapy.Field(default="")
rating = scrapy.Field(default="Zero")
توضیح: این کلاس ورودیها را محدود نمیکند اما یک ساختار ثابت برای پروژه فراهم میآورد؛ اگر فیلدی یافت نشود مقدار default استفاده میشود.
ادغام Item در اسپایدرها
اسپایدر مسئول درخواست HTTP و یافتن سلکتورهاست، اما تبدیل و تمیزکاری دادهها را بهتر است به ItemLoader بسپاریم. نمونهٔ اسپایدر برای books.toscrape.com:
import scrapy
from item_loaders.items import BookItem
from item_loaders.loaders.book_loader import BookLoader
class BooksSpider(scrapy.Spider):
name = "books"
start_urls = ["http://books.toscrape.com"]
def parse(self, response):
for book in response.css("article.product_pod"):
loader = BookLoader(item=BookItem(), selector=book)
loader.add_css("title", "h3 a::attr(title)")
loader.add_css("price", ".price_color::text")
loader.add_css("availability", ".instock.availability::text")
loader.add_css("rating", "p.star-rating::attr(class)")
yield loader.load_item()
نکات کلیدی:
- name برای اجرای اسپایدر استفاده میشود.
- selector به لودر میگوید کدام بلاک HTML را مبنا قرار دهد.
- با add_css یا add_xpath استخراج را انجام میدهیم؛ تبدیلها داخل لودر انجام خواهد شد.
راهاندازی یک ItemLoader پایه
یک لودر مخصوص هر آیتم منطق تمیزسازی را نگه میدارد. مثال BookLoader:
# loaders/book_loader.py
from itemloaders import ItemLoader
from itemloaders.processors import TakeFirst, MapCompose, Join
class BookLoader(ItemLoader):
default_output_processor = TakeFirst()
price_in = MapCompose(str.strip, lambda x: x.replace("£", "").replace("$", ""), float)
availability_in = MapCompose(str.strip)
rating_in = MapCompose(str.strip, lambda x: x.replace("star-rating ", ""))
توضیح پردازندهها:
- TakeFirst() (خروجی): اگر چند مقدار استخراج شد، اولین مقدار غیرخالی را برمیگرداند.
- MapCompose(...) (ورودی): لیستی از توابع را به ترتیب روی هر مقدار اعمال میکند (مثلاً پاکسازی فاصلهها، حذف نمادهای ارزی، تبدیل به float).
- Join برای ترکیب لیستها به رشتهٔ واحد مفید است (مثلاً تگها).
ورودی و خروجی پردازندهها و ساخت پردازندهٔ سفارشی
روال معمول برای هر فیلد: دادهٔ خام → input processors → (جمعآوری) → output processors → مقدار نهایی. مثال قیمت:
# price_in: ورودیها به ترتیب پردازش میشوند
price_in = MapCompose(
str.strip, # حذف فضاها و newline
lambda x: x.replace("£", "").replace("$", ""), # حذف نماد ارز
float # تبدیل به عدد اعشاری
)
نکتهٔ عملی: توابع داخل MapCompose باید توقع نوع ورودی را مشخص کنند (مثلاً اگر بعضی وقتها مقدار None میآید، ابتدا چک None اضافه کنید) تا از خطای زمان اجرا جلوگیری شود.
XPath یا CSS؟
عملکرد هر دو مشابه است؛ براساس ساختار صفحه انتخاب کنید. مثالها:
# XPath
loader.add_xpath('text', ".//span[@class='text']/text()")
# CSS
loader.add_css('title', 'h3 a::attr(title)')
در صفحات ناسازگار گاهی XPath انعطاف بیشتری میدهد؛ اما CSS خواناتر است و برای بسیاری موارد کافی است.
دادههای تودرتو و استفاده از چند لودر
برای ساختارهای تودرتو مثل کتابها و نقدها بهتر است برای هر زیرشیء یک آیتم و لودر بسازید و سپس آنها را در آیتم اصلی قرار دهید:
# در اسپایدر: استخراج نقدها و افزودن به BookLoader
reviews = []
for review in book.css('.reviews .review'):
review_loader = ReviewLoader(selector=review)
review_loader.add_css('reviewer', '.reviewer::text')
review_loader.add_css('comment', '.comment::text')
reviews.append(review_loader.load_item())
book_loader.add_value('reviews', reviews)
مزیت: هر لودر منطق خاص خودش را دارد و قابل تست و نگهداری است.
دیباگ کردن لودرها
وقتی لودر پیچیده شد، از pdb.set_trace() و متدهای لودر برای مشاهدهٔ مقادیر میانی استفاده کنید:
import pdb
# ... پس از افزودن دادهها
pdb.set_trace()
print('Intermediate values:', loader.get_collected_values('text'))
این روش کمک میکند ببینید چه دادههایی قبل از اعمال پردازندهها جمعآوری شدهاند و آنالیز سریعتری برای خطاها فراهم میآورد.
نکات عملکرد، امنیت و بهترینروشها
- تفکیک منطق: تمیزسازی را همیشه در لودر/پایپلاین بگذارید، نه در اسپایدر.
- احترام به محدودیتها: به robots.txt و قوانین سایت احترام بگذارید و از throttle (DOWNLOAD_DELAY) و محدود کردن همزمانی (CONCURRENT_REQUESTS) استفاده کنید.
- هدرها و session: برای صفحات حساس از هدرهای مناسب و مدیریت کوکیها استفاده کنید، ولی اعتبارنامهها را در سورس کد ذخیره نکنید.
- پایداری: برای خطاهای موقت از مکانیزم retry استفاده کنید و requestهای بزرگ را با pagination و استریم کردن خروجی مدیریت کنید.
- آزمونپذیری: برای هر لودر توابع پردازنده را جدا و با unit test پوشش دهید.
- امنیت داده: هنگام ذخیرهٔ خروجی به فرمتهایی مانند CSV/JSON، به تزریق کاراکترها و آسیبهای احتمالی توجه کنید (مثلاً کنترل مقدارهای غیرمنتظره در فیلدها).
اجرای اسپایدر و نمونهٔ خروجی
برای اجرای اسپایدر و گرفتن خروجی JSON:
scrapy crawl books -o books.json
scrapy crawl quotes -o quotes.json
نمونهٔ خروجی (نمایش نمونهٔ سادهشده):
[
{"title": "A Light in the Attic", "price": 51.77, "availability": "In stock", "rating": "Three"},
{"title": "Tipping the Velvet", "price": 53.74, "availability": "In stock", "rating": "One"}
]
جمعبندی
استفاده از Item و ItemLoader در اسکریپینگ با Scrapy کد شما را تمیزتر، قابل تستتر و مقاومتر میکند. با قرار دادن منطق تمیزسازی در لودرها، پیادهسازی pipelineها و جداسازی مسئولیتها میتوانید به سرعت پروژههای مقیاسپذیر و قابل اطمینانی بسازید. از پردازندههایی مثل MapCompose و TakeFirst بهدرستی استفاده کنید، دیباگها را هدفمند انجام دهید و همیشه به مسائل مربوط به نرخ درخواست، امنیت و نگهداری توجه نمایید.





