مقدمه
این مقاله یک راهنمای عملی برای شخصیسازی Scrapy در پروژههای وب اسکریپینگ است. هدف این راهنما این است که پس از خواندن آن شما بدانید چه زمانی باید از extensions، middlewares و pipelines استفاده کنید، چگونه نمونههای عملی بنویسید و آنها را در پروژهتان فعال و تست کنید. مثالها به زبان Python و با کد قابلاجرا ارائه شدهاند تا بتوانید سریع شروع به اسکریپ کردن کنید.
چرا Scrapy را شخصیسازی کنیم؟
Scrapy بهصورت پیشفرض بسیار قدرتمند و غیرهمزمان است، اما هر هدف اسکریپ متفاوت است. با شخصیسازی میتوانید رفتار کلی پروژه، جریان درخواست/پاسخ و پردازش آیتمها را دقیقاً مطابق نیازتان تغییر دهید:
- افزونهها (Extensions) برای تغییر رفتار سطح پروژه و مدیریت رخدادها مناسباند.
- میانافزارها (Middlewares) برای دستکاری درخواستها و پاسخها در مسیر بین engine و spider کاربرد دارند.
- پایپلاینها (Pipelines) برای پاکسازی، اعتبارسنجی و ذخیرهسازی دادهها بعد از استخراج مناسباند.
در ادامه هر بخش را با مثال واقعی توضیح میدهیم: یک افزونه برای لاگگیری زمان اجرای اسپایدر، یک downloader middleware برای منطق retry سفارشی و یک pipeline برای ذخیره در sqlite.
Extensions — مفهوم و مثال عملی
افزونهها برای hook زدن به سیگنالهای سطح پروژه استفاده میشوند؛ مثلاً زمانی که یک اسپایدر باز یا بسته میشود یا وقتی یک آیتم اسکریپ شد. آنها برای مانیتورینگ، ارسال هشدار و جمعآوری آمار کلی بسیار مناسباند. مزیت اصلی: تغییر رفتار در یک نقطه مرکزی؛ ایراد بالقوه: باگ در افزونه میتواند تمام اسپایدرها را تحتتأثیر قرار دهد.
مثال: یک افزونه ساده که زمان شروع/پایان اسپایدر و وضعیت پاسخها را در یک فایل لاگ ذخیره میکند.
import logging
from datetime import datetime
from scrapy import signals
class CustomLoggerExtension:
def __init__(self, stats, log_file):
# ورودیها: stats (آمار داخلی Scrapy)، log_file (مسیر فایل لاگ)
self.stats = stats
self.log_file = log_file
self.start_time = None
self.end_time = None
self.logger = logging.getLogger('custom_logger')
self.logger.setLevel(logging.INFO)
handler = logging.FileHandler(self.log_file)
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
@classmethod
def from_crawler(cls, crawler):
# نقطه اتصال به runtime: تنظیمات را میخواند و سیگنالها را متصل میکند
log_file = crawler.settings.get('CUSTOM_LOG_FILE', 'custom_log.txt')
ext = cls(crawler.stats, log_file)
crawler.signals.connect(ext.spider_opened, signal=signals.spider_opened)
crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)
crawler.signals.connect(ext.response_received, signal=signals.response_received)
return ext
def spider_opened(self, spider):
# اجرا هنگام باز شدن اسپایدر — خروجی: ثبت زمان شروع
self.start_time = datetime.now()
self.logger.info(f'Spider {spider.name} opened')
def spider_closed(self, spider):
# اجرا هنگام بسته شدن اسپایدر — خروجی: ثبت زمان پایان و مدت اجرا
self.end_time = datetime.now()
runtime = self.end_time - self.start_time
self.logger.info(f'Spider {spider.name} closed. Time elapsed: {runtime}')
def response_received(self, response, request, spider):
# اجرا برای هر پاسخ دریافتی — خروجی: ثبت کد وضعیت و URL
self.logger.info(f'Response received, status: {response.status} for {response.url}')
توضیح خط به خط (خلاصه):
- __init__: منابع لازم را نگه میدارد و یک لاگر فایل تنظیم میکند.
- from_crawler: تنظیمات را میخواند و متدهای افزونه را به سیگنالهای مناسب متصل میکند.
- spider_opened / spider_closed: زمان شروع/پایان را ثبت میکنند و بهعنوان خروجی در فایل لاگ نوشته میشوند.
- response_received: برای هر پاسخ فراخوانی میشود و اطلاعات مفید برای دیباگ را لاگ میکند.
Middlewares — کنترل جریان درخواست/پاسخ
میانافزارها اجازه میدهند رفتار HTTP شما (مثل user-agent، پراکسی، کوکیها و retry) را وسط کار تغییر دهید. دو نوع اصلی وجود دارد: downloader middleware (کاری با request/response بین engine و downloader دارد) و spider middleware (قبل از رسیدن response به اسپایدر اجرا میشود).
در ادامه یک downloader middleware برای پیادهسازی منطق retry سفارشی میبینید.
from scrapy.exceptions import IgnoreRequest
from scrapy.utils.response import response_status_message
class CustomRetryMiddleware:
def __init__(self, retry_times, retry_http_codes):
# ورودیها: حداکثر دفعات retry و لیست کدهای HTTP برای retry
self.retry_times = retry_times
self.retry_http_codes = set(retry_http_codes)
@classmethod
def from_crawler(cls, crawler):
return cls(
retry_times=crawler.settings.getint('RETRY_TIMES', 3),
retry_http_codes=crawler.settings.getlist('RETRY_HTTP_CODES', [500, 502, 503, 504, 522, 524, 408])
)
def process_request(self, request, spider):
# ورودی: request؛ خروجی: معمولاً None (اجازه ادامه جریان)
spider.logger.info(f'Modifying request: {request.url}')
return None
def process_response(self, request, response, spider):
# منطق retry روی پاسخها انجام میشود
retries = request.meta.get('retry_times', 0)
if retries > 0:
spider.logger.info(f'Retry response received: {response.status} for {response.url} (Retry {retries})')
else:
spider.logger.info(f'Processing response: {response.status} for {response.url}')
if response.status in self.retry_http_codes:
spider.logger.warning(f'Retrying {response.url} due to HTTP {response.status}')
return self._retry(request, response_status_message(response.status), spider)
return response
def process_exception(self, request, exception, spider):
# در صورت استثنا، سعی میکنیم مجدداً درخواست را اجرا کنیم
spider.logger.warning(f'Exception encountered: {exception} for {request.url}')
return self._retry(request, str(exception), spider)
def _retry(self, request, reason, spider):
retries = request.meta.get('retry_times', 0) + 1
if retries <= self.retry_times:
spider.logger.info(f'Retrying {request.url} ({retries}/{self.retry_times}) due to: {reason}')
retry_req = request.copy()
retry_req.meta['retry_times'] = retries
retry_req.dont_filter = True
return retry_req
else:
spider.logger.error(f'Gave up retrying {request.url} after {self.retry_times} attempts')
raise IgnoreRequest(f'Request failed after retries: {request.url}')
نکات و بهترین روشها برای middleware:
- برای جلوگیری از loop نگهداشتن شمارنده retries در request.meta لازم است.
- برای حفظ کارآیی از پردازش سنگین در process_request خودداری کنید؛ معمولا کارهای I/O باید در لایهای دیگر انجام شود.
- در تنظیمات، اولویت (عدد) میتواند ترتیب فراخوانی میانافزارها را تغییر دهد.
Pipelines — پردازش و ذخیره دادهها
پایپلاینها محل نهایی برای کارهای مربوط به آیتمها مثل پاکسازی، اعتبارسنجی، حذف تکراریها و ذخیرهسازی هستند. ترتیب اجرای پایپلاینها با مقدار عددی در تنظیمات مشخص میشود (اعداد کوچکتر زودتر اجرا میشوند).
مثال: ذخیره آیتمها در یک دیتابیس sqlite و حذف آیتمهایی که فیلدهای ضروری را ندارند.
import sqlite3
from scrapy.exceptions import DropItem
class SQLitePipeline:
def open_spider(self, spider):
# باز شدن اتصال دیتابیس هنگام شروع اسپایدر
self.connection = sqlite3.connect('data.db')
self.cursor = self.connection.cursor()
spider.logger.info('SQLitePipeline: Database connection opened.')
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS quotes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author TEXT NOT NULL
)
''')
self.connection.commit()
def process_item(self, item, spider):
# پردازش هر آیتم: اعتبارسنجی و درج در دیتابیس
if not item.get('title') or not item.get('author'):
raise DropItem(f'Missing title or author in {item}')
self.cursor.execute('INSERT INTO quotes (title, author) VALUES (?, ?)', (item['title'], item['author']))
self.connection.commit()
spider.logger.info(f'SQLitePipeline: Item stored in database: {item}')
return item
def close_spider(self, spider):
# بستن اتصال دیتابیس
self.connection.close()
spider.logger.info('SQLitePipeline: Database connection closed.')
نکات عملی:
- با حجم داده بالا از یک دیتابیس همزمان (async) مثل PostgreSQL یا MySQL همراه با لایه async استفاده کنید تا از گلوگاه I/O جلوگیری شود.
- برای اطمینان از یکپارچگی، عملیاتهای حساس را در تراکنش قرار دهید یا از batch insert استفاده کنید.
- برای حذف تکراریها از یک شناسه یکتا استفاده کرده یا قبل از درج چک کنید.
فعالسازی اجزا و تنظیمات
برای فعال کردن افزونه، میدلویر و پایپلاین، مسیر آنها را در settings.py قرار دهید. ترتیب اجرای پایپلاینها به مقدار عددی در ITEM_PIPELINES بستگی دارد.
# بخش تنظیمات نمونه
EXTENSIONS = {
'my_custom_crawler.extensions.custom_logger.CustomLoggerExtension': 500,
}
DOWNLOADER_MIDDLEWARES = {
'my_custom_crawler.middlewares.custom_request.CustomRetryMiddleware': 501,
}
ITEM_PIPELINES = {
'my_custom_crawler.pipelines.sqlite_pipeline.SQLitePipeline': 300,
}
دستورات اولیه برای راهاندازی و تست:
scrapy startproject my_custom_crawler
scrapy crawl test_pipeline
scrapy fetch https://httpstat.us/500 # برای تست منطق retryنکات پیشرفته، مقیاسپذیری و امنیت
برای پروژههای جدی وب اسکریپینگ به این موارد توجه کنید:
- استفاده از سیگنالها (signals) برای ساخت ابزارهای رفع خطای پیشرفته؛ مثلاً گوش دادن به spider_error یا item_dropped و ارسال هشدار.
- برای صفحات جاوااسکریپتی از ابزارهایی مثل Scrapy-Splash یا راهکارهای headless استفاده کنید؛ آنها را در یک downloader middleware cache کنید تا درخواستها کاهش یابد.
- برای توزیع بار و صفبندی از Scrapy-Redis یا سازوکار مشابه استفاده کنید تا چندین ماشین بتوانند همزمان کار کنند.
- به قوانین سایتها (robots.txt) و مسائل حقوقی/حریم خصوصی احترام بگذارید. نگهداری و پردازش دادههای خصوصی نیازمند محافظت و رعایت قوانین است.
- بهینهسازی: از AutoThrottle، تنظیمات concurrency و استفاده از دیتابیسهای async برای افزایش throughput بهره بگیرید.
جمعبندی
شخصیسازی Scrapy با افزونهها، میانافزارها و پایپلاینها به شما کنترل دقیقی روی رفتار crawler میدهد. مراحل پیشنهادی برای شروع:
- ابتدا هدف نهایی (ذخیرهسازی/تحلیل) را مشخص کنید.
- با یک افزونه ساده مانیتورینگ بسازید تا وضعیت کلی را ببینید.
- میانافزارهای ضروری مثل retry، پراکسی و user-agent را پیاده کنید و تست کنید.
- پایپلاینها را برای پاکسازی و ذخیرهسازی دادهها سازماندهی کنید و برای بار زیاد از دیتابیس مناسب بهره ببرید.
با رعایت نکات بالا و تست مداوم، میتوانید Scrapy را بهصورت قابلاعتماد و مقیاسپذیر برای پروژههای وب اسکریپینگ شخصیسازی کنید.





