خانه/مقالات/میان‌افزار سفارشی Scrapy برای وب اسکریپینگ
برنامه نویسی
پروکسی و چرخش IP
ضد بلاک (Anti-bot)
برگشت به صفحه مقاله ها
میان‌افزار سفارشی Scrapy برای وب اسکریپینگ

میان‌افزار سفارشی Scrapy برای وب اسکریپینگ

این مقاله یک راهنمای عملی برای نوشتن میان‌افزارهای سفارشی در Scrapy است؛ شامل پیاده‌سازی پروکسی با احراز هویت base64، مثال تغییر User-Agent، و یک میان‌افزار ری‌تری با توضیح کامل متدها و نکات عملی برای تست و بهینه‌سازی.
امیر حسین حسینیان
امیر حسین حسینیان
1404-11-25

مقدمه

در این مقاله گام‌به‌گام یاد می‌گیریم چگونه میان‌افزار (middleware) دلخواه برای Scrapy بنویسیم و آن را برای سناریوهای واقعیِ وب اسکریپینگ مثل پروکسی و مکانیزم retry به‌کار بگیریم. فرض می‌کنم خواننده یک توسعه‌دهنده پایتون در سطح متوسط است؛ پس تمرکز بر روی جزئیات فنی، مثال‌های عملی و بهترین روش‌ها خواهد بود. در پایان شما می‌دانید چگونه میان‌افزار دانلودر بسازید، آن را در settings فعال کنید، و رفتار درخواست‌ها را کنترل کنید.

معماری میان‌افزارهای Scrapy

در Scrapy میان‌افزارها بین سه‌جزو اصلی قرار می‌گیرند: Engine، Downloader و Spider. بسته به محل قرارگیری، دو نوع میان‌افزار وجود دارد:

  • Downloader middleware: بین Engine و Downloader قرار می‌گیرد و روی درخواست‌ها و پاسخ‌ها عمل می‌کند. مناسب برای پروکسی، هدرهای سفارشی، و مدیریت timeouts.
  • Spider middleware: بین Engine و Spider قرار می‌گیرد و می‌تواند پاسخ‌ها یا آیتم‌ها را تغییر، حذف یا فیلتر کند؛ مناسب برای پیش‌پردازش پاسخ‌ها قبل از استخراج.

موارد کاربردی رایج

چند نمونه از چیزهایی که با میان‌افزار می‌توان پیاده‌سازی کرد:

  • Rotating proxies — توزیع درخواست‌ها روی آدرس‌های مختلف.
  • مدیریت بلاک شدن IP — تشخیص بلوک و چرخش پروکسی.
  • Handling timeouts — تشخیص تأخیر طولانی و قطع یا retry.
  • Retry logic — ری‌تری هوشمند برای کدهای خطای خاص.
  • Custom headers — اضافه یا تعویض هدرها (مانند User-Agent).
  • Authentication — افزودن کوکی یا توکن‌های API به درخواست‌ها.
  • Rate limiting — واکنش به 429 و اعمال تاخیر یا backoff.

راه‌اندازی پروژه Scrapy

مراحل اولیه: نصب و ساخت پروژه. این دستورات را در ترمینال اجرا کنید:

python --version
pip install scrapy
scrapy startproject my_custom_middleware

ساختار نهایی فایل‌ها که می‌خواهیم به‌دست بیاوریم می‌تواند شبیه این باشد:

.
├── my_custom_middleware
│   ├── __init__.py
│   ├── items.py
│   ├── middlewares
│   │   ├── __init__.py
│   │   ├── proxy_middleware.py
│   │   └── retry_middleware.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       └── test_spider.py
└── scrapy.cfg

درون پوشه middlewares فایل‌های proxy_middleware.py و retry_middleware.py را می‌نویسیم. برای تست سریع می‌توانید از scrapy fetch یا یک spider ساده استفاده کنید.

مبانی یک Downloader Middleware

یک میان‌افزار دانلودر چند متد معمول دارد که برای تغییر جریان درخواست/پاسخ استفاده می‌شوند:

  • process_request(request, spider) — قبل از ارسال درخواست اجرا می‌شود. می‌تواند درخواست را تغییر دهد یا مقدار دیگری برگرداند (مانند پاسخ آماده).
  • process_response(request, response, spider) — پس از دریافت پاسخ اجرا می‌شود. باید یا یک پاسخ (معمولاً همان response) یا یک درخواست جدید برای ری‌تری بازگرداند.
  • process_exception(request, exception, spider) — هنگام وقوع استثنا در مرحله دانلود اجرا می‌شود؛ می‌تواند ری‌تری یا لاگ انجام دهد.

نمونهٔ ساده (نمایشی):

class BasicDownloaderMiddleware:
    def process_request(self, request, spider):
        # ورودی: request و spider
        # خروجی: None یا یک Response/Request متفاوت
        request.headers[b'X-Custom-Header'] = b'MyCustomValue'
        spider.logger.info(f"Modified request: {request.url}")
        return None

    def process_response(self, request, response, spider):
        # ورودی: request، response
        # خروجی: باید یک Response یا Request برگردانیم
        spider.logger.info(f"Received response {response.status} for {request.url}")
        return response

    def process_exception(self, request, exception, spider):
        # ورودی: request، exception
        spider.logger.error(f"Exception {exception} occurred for {request.url}")
        return None

توضیح کوتاه خط‌به‌خط: در process_request هدر سفارشی اضافه می‌شود و مقدار None برگردانده می‌شود تا ادامهٔ پردازش طبیعی انجام شود. در process_response وضعیت پاسخ لاگ می‌شود و همان پاسخ بازگردانده می‌شود.

طراحی میان‌افزار پروکسی

ایده: همهٔ درخواست‌ها را از طریق پروکسی عبور دهیم و از طریق هدر Proxy-Authorization احراز هویت کنیم. سه مقدار ضروری در settings خواهیم داشت: PROXY_URL، PROXY_USER و PROXY_PASS. رشتهٔ "username:password" را با base64 میکنیم و در هدر قرار می‌دهیم.

میان‌افزار پروکسی: پیاده‌سازی

کدی تمیز و خوانا برای proxy_middleware.py:

import base64
from scrapy.exceptions import NotConfigured

class ProxyMiddleware:
    def __init__(self, proxy_url):
        # proxy_url: آدرس کامل پروکسی مثل http://residential-proxy.scrapeops.io:8181
        self.proxy_url = proxy_url

    @classmethod
    def from_crawler(cls, crawler):
        # خواندن تنظیمات از crawler.settings و غیرفعال کردن middleware درصورت نبودن پروکسی
        proxy_url = crawler.settings.get("PROXY_URL")
        if not proxy_url:
            raise NotConfigured("PROXY_URL is not set in settings.")
        return cls(proxy_url)

    def process_request(self, request, spider):
        # ورودی: request، spider
        # خروجی: None (ادامهٔ مسیر) یا می‌توان Request/Response جایگزین بازگرداند
        spider.logger.debug(f"Processing request: {request.url}")
        request.meta['proxy'] = self.proxy_url

        proxy_user = spider.settings.get("PROXY_USER")
        proxy_pass = spider.settings.get("PROXY_PASS")
        if proxy_user and proxy_pass:
            proxy_auth = f"{proxy_user}:{proxy_pass}"
            encoded_auth = base64.b64encode(proxy_auth.encode()).decode()
            request.headers['Proxy-Authorization'] = f"Basic {encoded_auth}"
            spider.logger.debug(f"Using proxy authentication: {proxy_user}")

        spider.logger.debug(f"Proxy set to {self.proxy_url}")
        return None

نکات فنی و توضیح:

  • ورودی‌ها: proxy_url از تنظیمات گرفته می‌شود؛ process_request، request و spider را دریافت می‌کند.
  • خروجی‌ها: معمولاً None بازگردانده می‌شود تا Scrapy ادامه دهد؛ در موارد خاص می‌توان یک Response یا Request جدید بازگرداند.
  • چرا base64؟ هدر Proxy-Authorization معمولاً با قالب Basic و مقدار base64 برای username:password ارسال می‌شود.
  • مدیریت خطا: اگر PROXY_URL تعریف نشده باشد، با NotConfigured میان‌افزار غیرفعال می‌شود تا از خطای زمان اجرا جلوگیری گردد.

افزونه: تغییر User-Agent و هدرها

معمولاً Scrapy مقدار پیش‌فرض User-Agent را اعلام می‌کند و بهتر است در تولید از یک User-Agent معتبر استفاده کنید:

USER_AGENT = (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
)

# داخل process_request پس از تنظیم Proxy-Authorization:
# request.headers['User-Agent'] = USER_AGENT

توجه: می‌توانید USER_AGENT را از تنظیمات بخوانید یا به طور داینامیک آن را بچرخانید (rotating user-agents) تا شانس بلاک شدن را کاهش دهید.

تنظیمات (settings.py)

نمونهٔ قرار دادن تنظیمات پروکسی و فعال‌سازی میان‌افزار:

PROXY_URL = "http://residential-proxy.scrapeops.io:8181"
PROXY_USER = "scrapeops"
PROXY_PASS = "your-super-secret-api-key"

DOWNLOADER_MIDDLEWARES = {
    "my_custom_middleware.middlewares.proxy_middleware.ProxyMiddleware": 543,
    "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 750,
}

ترتیب عددی تعیین‌کنندهٔ اولویت میان‌افزارهاست؛ مقدار کمتر اجرا شدن زودتر را مشخص می‌کند. مطمئن شوید HttpProxyMiddleware در جای مناسب قرار دارد.

آزمون میان‌افزار

دو راه سریع برای تست:

  • استفاده از fetch در ترمینال:
scrapy fetch https://lumtest.com/echo.json

در لاگ باید ببینید که درخواست از پروکسی عبور کرده و هدر Proxy-Authorization ارسال شده است.

  • اجرای spider:
scrapy crawl test_spider

در کنسول به دنبال پیغام‌های debug که در process_request لاگ کرده‌ایم باشید و بررسی کنید که پاسخ‌ها وضعیت مورد انتظار را دارند.

مثال واقعی: میان‌افزار Retry

هدف: کدهای وضعیت ضعیف (مثل 500، 503 یا 429) و استثناهای دانلود را تشخیص داده و با سقف مشخص ری‌تری انجام دهیم.

from scrapy.exceptions import IgnoreRequest
from scrapy.utils.response import response_status_message

class RetryMiddleware:
    def __init__(self, retry_times, retry_http_codes):
        # retry_times: حداکثر دفعات ری‌تری
        # retry_http_codes: مجموعه کدهای HTTP که باید ری‌تری شوند
        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):
        spider.logger.info(f"Modifying request: {request.url}")
        return None

    def process_response(self, request, response, spider):
        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}")

توضیح عملکرد:

  • در from_crawler محدودهٔ ری‌تری و کدهای مورد نظر از تنظیمات خوانده می‌شوند.
  • اگر پاسخ شامل کد از پیش‌تعریف‌شده باشد، _retry یک کپی از Request با متای به‌روز شده بازمی‌گرداند.
  • در صورت رسیدن به سقف تلاش‌ها، میان‌افزار با IgnoreRequest درخواست را نادیده می‌گیرد.

بهترین روش‌ها و هشدارهای عملی

  • مدولار بنویسید: یک میان‌افزار یک مسئولیت داشته باشد (مثلاً فقط پروکسی یا فقط retry).
  • لاگ‌گزاری مناسب: از سطوح مختلف لاگ (debug/info/warning/error) استفاده کنید تا هنگام مشکل‌زدایی سریع علت را پیدا کنید.
  • امنیت: کلیدها و رمزها را در کنترل نسخه ذخیره نکنید؛ از متغیرهای محیطی یا vault استفاده کنید.
  • پایایی و عملکرد: افزوده شدن میان‌افزار می‌تواند latency اضافه کند؛ تست بار انجام دهید و از همزمانی مناسب استفاده کنید.
  • احترام به قوانین: قبل از اسکریپ کردن حتماً robots.txt و شرایط استفاده سایت را بررسی کنید و نرخ درخواست را کنترل کنید تا سرویس هدف آسیب نبیند.
  • استفاده از backoff: در مواجهه با 429 یا 503 از الگوریتم‌های exponential backoff استفاده کنید تا احتمال بلاک شدن کاهش یابد.

جمع‌بندی

میان‌افزارها ابزار قدرتمندی برای سفارشی‌سازی رفتار Scrapy در سطح درخواست/پاسخ هستند. با نوشتن میان‌افزار پروکسی می‌توانید ترافیک را از طریق شبکه‌های مسکونی بفرستید و با میان‌افزار retry کنترل پایداری درخواست‌ها را افزایش دهید. رعایت بهترین روش‌ها (مدولار بودن، لاگ خوب، مدیریت کلیدها و احترام به نرخ‌ها) باعث می‌شود پروژهٔ شما قابل توسعه و قابل اطمینان باشد. حالا وقت آن است که نمونه‌هایی که دیدید را در پروژهٔ خود پیاده‌سازی و متناسب با نیازتان توسعه دهید.

مقاله‌های مرتبط
ابزارها و فریم‌ورک‌ها (Scrapy, Puppeteer و …)
1404-12-01
نظارت اسپایدرهای Scrapy در وب اسکریپینگ
این راهنما چهار روش نظارت روی اسپایدرهای Scrapy را بررسی می‌کند: لاگ‌ها و آمار داخلی، ابزارهای اختصاصی مانیتورینگ، Spidermon برای تست‌های اعتبارسنجی و ابزارهای عمومی لاگینگ. با مثال‌های پایتون و تنظیمات عملی، توصیه‌های استقرار و یک چک‌لیست عملی برای تولید ارائه شده است.
ابزارها و فریم‌ورک‌ها (Scrapy, Puppeteer و …)
1404-11-28
مانیتورینگ Scrapy با Spidermon برای وب اسکریپینگ
این مقاله راهنمایی عملی برای راه‌اندازی Spidermon در پروژه‌های Scrapy ارائه می‌دهد: از نصب و تنظیمات پایه تا نوشتن Monitorها، MonitorSuiteها، اعتبارسنجی آیتم‌ها و ارسال نوتیفیکیشن (مثلاً اسلک). با مثال‌های کد پایتون و توضیحات خط به خط، می‌توانید ظرف چند دقیقه یک مانیتورینگ قابل اتکا برای وب اسکریپینگ خود بسازید.
ابزارها و فریم‌ورک‌ها (Scrapy, Puppeteer و …)
1404-11-27
Scrapyd؛ راهنمای عملی اسکریپ با Scrapy
این مقاله یک راهنمای عملی برای نصب، استقرار و مدیریت اسپایدرهای Scrapy با Scrapyd ارائه می‌دهد؛ شامل مثال‌های کد برای deploy، استفاده از API و کتابخانهٔ python-scrapyd-api، ادغام با ScrapeOps و نکات امنیتی و بهترین‌روش‌ها برای اجرا در محیط تولید.