

در این مقاله گامبهگام یاد میگیریم چگونه میانافزار (middleware) دلخواه برای Scrapy بنویسیم و آن را برای سناریوهای واقعیِ وب اسکریپینگ مثل پروکسی و مکانیزم retry بهکار بگیریم. فرض میکنم خواننده یک توسعهدهنده پایتون در سطح متوسط است؛ پس تمرکز بر روی جزئیات فنی، مثالهای عملی و بهترین روشها خواهد بود. در پایان شما میدانید چگونه میانافزار دانلودر بسازید، آن را در settings فعال کنید، و رفتار درخواستها را کنترل کنید.
در Scrapy میانافزارها بین سهجزو اصلی قرار میگیرند: Engine، Downloader و Spider. بسته به محل قرارگیری، دو نوع میانافزار وجود دارد:
چند نمونه از چیزهایی که با میانافزار میتوان پیادهسازی کرد:
مراحل اولیه: نصب و ساخت پروژه. این دستورات را در ترمینال اجرا کنید:
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 ساده استفاده کنید.
یک میانافزار دانلودر چند متد معمول دارد که برای تغییر جریان درخواست/پاسخ استفاده میشوند:
نمونهٔ ساده (نمایشی):
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
نکات فنی و توضیح:
معمولاً 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) تا شانس بلاک شدن را کاهش دهید.
نمونهٔ قرار دادن تنظیمات پروکسی و فعالسازی میانافزار:
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 در جای مناسب قرار دارد.
دو راه سریع برای تست:
scrapy fetch https://lumtest.com/echo.json
در لاگ باید ببینید که درخواست از پروکسی عبور کرده و هدر Proxy-Authorization ارسال شده است.
scrapy crawl test_spider
در کنسول به دنبال پیغامهای debug که در process_request لاگ کردهایم باشید و بررسی کنید که پاسخها وضعیت مورد انتظار را دارند.
هدف: کدهای وضعیت ضعیف (مثل 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}")
توضیح عملکرد:
میانافزارها ابزار قدرتمندی برای سفارشیسازی رفتار Scrapy در سطح درخواست/پاسخ هستند. با نوشتن میانافزار پروکسی میتوانید ترافیک را از طریق شبکههای مسکونی بفرستید و با میانافزار retry کنترل پایداری درخواستها را افزایش دهید. رعایت بهترین روشها (مدولار بودن، لاگ خوب، مدیریت کلیدها و احترام به نرخها) باعث میشود پروژهٔ شما قابل توسعه و قابل اطمینان باشد. حالا وقت آن است که نمونههایی که دیدید را در پروژهٔ خود پیادهسازی و متناسب با نیازتان توسعه دهید.


