مقدمه
در اسکریپینگ واقعی، پروکسیها همیشه پایدار نیستند و هزینهها میتوانند متفاوت باشند. یک سیستم «پروکسی واترفال» (Proxy Waterfall) به شما اجازه میدهد درخواستها را ابتدا بدون پروکسی یا با ارزانترین گزینه ارسال کنید و در صورت خطا به تدریج به پروکسیهای قویتر و گرانتر منتقل شوید. در این مقاله یاد میگیرید چگونه یک DownloaderMiddleware در Scrapy بسازید که این منطق را پیادهسازی کند، شامل بررسی کلید API، تنظیم هدرهای احراز هویت، و قوانین بازگشتی (retry-based) برای انتخاب پروکسی.
استراتژی پیشنهادی برای Waterfall
قبل از کدنویسی باید اهداف و قوانین روشن باشند. مثال استراتژی که پیادهسازی خواهیم کرد:
- اولین تلاش: بدون پروکسی برای کاهش هزینه.
- اگر دامنه google.com باشد، همیشه از یک پروکسی مخصوص (مثلاً ScraperAPI) استفاده شود.
- در دو بازپرینی اول: از ارزانترین پروکسی (مثلاً Scrapingdog) استفاده شود.
- در بازپرینیهای سوم و چهارم: از میانرده (مثلاً ScraperAPI) استفاده شود.
- پس از آن یا به عنوان fallback: از پروکسی پیشرفتهتر (مثلاً Scrapingbee) استفاده شود.
اگر کلید API یک سرویس تنظیم نشده یا معتبر نباشد، منطق باید به صورت خودکار به گزینه بعدی سقوط کند. همچنین میتوانید براساس request.meta یا فلگهای سفارشی مثل geotargeting یا نیاز به JS rendering، انتخاب پروکسی را سفارشی کنید.
طرح کلی Middleware و توابع کلیدی
برای یک middleware ساده به این متدها نیاز داریم:
- from_crawler: برای دسترسی به تنظیمات پروژه و ایجاد نمونه middleware.
- __init__: بارگذاری کلیدهای API و پیکربندی آدرس/نامکاربری/رمز عبور پروکسیها.
- api_key_valid: چک ساده برای معتبر بودن کلید API.
- add_proxy: اعمال پروکسی روی یک Request (تنظیم request.meta['proxy'] و هدر Proxy-Authorization).
- process_request: هسته منطق واترفال که قبل از ارسال هر درخواست اجرا میشود.
مثال کامل middleware (نمونه پایتون)
import base64
class ProxyWaterfallMiddleware:
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings)
def __init__(self, settings):
# بارگذاری کلیدهای API از settings.py
self.scraperapi_api_key = settings.get('SCRAPERAPI_API_KEY')
self.scrapingbee_api_key = settings.get('SCRAPINGBEE_API_KEY')
self.scrapingdog_api_key = settings.get('SCRAPINGDOG_API_KEY')
# پیکربندی هر سرویس: host, username, password
self.scrapingbee_http_address = 'http://proxy.scrapingbee.com:8886'
self.scrapingbee_username = self.scrapingbee_api_key
self.scrapingbee_password = 'render_js=False'
self.scraperapi_http_address = 'http://proxy-server.scraperapi.com:8001'
self.scraperapi_username = 'scraperapi'
self.scraperapi_password = self.scraperapi_api_key
self.scrapingdog_http_address = 'http://proxy.scrapingdog.com:8081'
self.scrapingdog_username = 'scrapingdog'
self.scrapingdog_password = self.scrapingdog_api_key
def api_key_valid(self, api_key):
# ورودی: api_key (str یا None)
# خروجی: True اگر api_key معتبر و غیر خالی باشد
if api_key is None or api_key == '':
return False
return True
def add_proxy(self, request, username, password, host):
# ورودیها:
# - request: شیء scrapy.Request
# - username/password: credentials برای پروکسی
# - host: آدرس پروکسی (شامل پروتکل و پورت)
# خروجی: هیچ (درخواست را تغییر میدهد)
# ساخت Basic auth و اضافه کردن به هدر
user_credentials = '{user}:{passw}'.format(user=username, passw=password)
basic_authentication = 'Basic ' + base64.b64encode(user_credentials.encode()).decode()
# تنظیم پروکسی و هدر احراز هویت
request.meta['proxy'] = host
request.headers['Proxy-Authorization'] = basic_authentication
def process_request(self, request, spider):
# این تابع قبل از ارسال هر request اجرا میشود
# از تعداد retry برای تعیین لایه پروکسی استفاده میکنیم
retries = request.meta.get('retry_times', 0)
# قاعده: همیشه برای google.com از ScraperAPI استفاده کن
if 'google.com' in request.url and self.api_key_valid(self.scraperapi_api_key):
self.add_proxy(request, self.scraperapi_username, self.scraperapi_password, self.scraperapi_http_address)
return None
# اگر اولین تلاش است، بدون پروکسی بفرست
if retries == 0:
return None
# Tier 1: Scrapingdog برای دو retry اول
if self.api_key_valid(self.scrapingdog_api_key) and retries <= 2:
self.add_proxy(request, self.scrapingdog_username, self.scrapingdog_password, self.scrapingdog_http_address)
return None
# Tier 2: ScraperAPI برای retry سوم و چهارم
if self.api_key_valid(self.scraperapi_api_key) and retries <= 4:
self.add_proxy(request, self.scraperapi_username, self.scraperapi_password, self.scraperapi_http_address)
return None
# Tier 3: Scrapingbee برای بقیه موارد یا fallback
if self.api_key_valid(self.scrapingbee_api_key):
self.add_proxy(request, self.scrapingbee_username, self.scrapingbee_password, self.scrapingbee_http_address)
return None
# اگر هیچ پروکسی موجود نبود، درخواست بدون پروکسی ارسال میشود
return None
توضیح بخشهای کد (ورودی/خروجی و نکات خطبهخط)
from_crawler: یک factory method است که تنظیمات Scrapy را میگیرد و نمونه middleware را میسازد. ورودی: crawler، خروجی: نمونه کلاس.
__init__: کلیدهای API را از تنظیمات میخواند و آدرس/نامکاربری/پسورد سرویسها را تنظیم میکند. این بخش مناسب مرکزی کردن کانفیگها و اضافه کردن ویژگیهای بیشتر (مثل geotargeting) است.
api_key_valid: برای جلوگیری از ارسال درخواست به پروکسیهایی که تنظیم نشدهاند یا کلید نامعتبر دارند. بسیار مهم است که بررسی کلید را پیش از استفاده انجام دهید تا منطق fallback کار کند.
add_proxy: دریافت یک Request و اعمال پروکسی. کارها:
- ساخت رشته اعتبار با فرمت user:password
- کدنویسی Base64 و ساخت هدر Proxy-Authorization
- تنظیم request.meta['proxy'] به آدرس پروکسی
process_request: هسته منطقی واترفال. بر اساس تعداد retry و قوانین دامنه تصمیم میگیرد کدام پروکسی را اعمال کند یا هیچ پروکسیای قرار ندهد.
نکات امنیتی، عملکرد و پایداری
- نگهداری کلیدهای API در settings.py مناسب است اما بهتر است از متغیرهای محیطی (environment variables) یا سیستمی برای مخفیسازی استفاده کنید تا در مخازن کد درز نکنند.
- هر زمان که هدرها یا credential را لاگ میکنید، مراقب باشید اطلاعات حساس چاپ نشود.
- پایداری: نرخ بازخواست (retry) و concurrency را با DOWNLOAD_DELAY و CONCURRENT_REQUESTS کنترل کنید تا از مسدود شدن ناگهانی جلوگیری شود.
- محدودیت سرویسدهندهها (rate limits) و هزینهها را در نظر بگیرید؛ ارسال زیاد به پروکسیهای گران قیمت میتواند هزینهها را افزایش دهد.
- برای ارزیابی کارآمدی هر پروکسی، میتوانید metricهایی مثل نرخ موفقیت، زمان پاسخ و خطای HTTP را ثبت کرده و بر اساس آنها وزندهی کنید (همراه با یک سیستم retry هوشمند).
مدیریت خطا، retry و اعتبارسنجی پاسخ
بهتر است پس از دریافت پاسخ، آن را سریعاً اعتبارسنجی کنید (مثلاً بررسی کد HTTP، وجود CAPTCHA یا تغییر در DOM). در صورتی که پاسخ نامعتبر بود، میتوانید با افزایش request.meta['retry_times'] یا ایجاد یک Request جدید با همان URL و meta مناسب، دوباره تلاش کنید و این باعث میشود middleware دفعه بعد لایه پروکسی دیگری انتخاب کند.
چگونه Middleware را در Scrapy فعال کنیم
در settings.py مقداردهی کنید:
SCRAPERAPI_API_KEY = 'YOUR_API_KEY'
SCRAPINGBEE_API_KEY = 'YOUR_API_KEY'
SCRAPINGDOG_API_KEY = 'YOUR_API_KEY'
DOWNLOADER_MIDDLEWARES = {
'your_project.proxy_waterfall.ProxyWaterfallMiddleware': 350,
}
توضیح: عدد 350 ترتیب اجرای middleware را مشخص میکند؛ عدد مناسب بسته به بقیه middlewareهای شما انتخاب شود.
توصیههای عملی و توسعهپذیری
- برای پروژههای بزرگ، رفتار واترفال را در یک کلاس مجزا یا سرویس پیکربندی قرار دهید تا تست و نگهداری آسانتر شود.
- بسته به سرویس پروکسی ممکن است پارامترهای اضافی مثل render_js یا geotargeting نیاز داشته باشید؛ آنها را در request.meta یا در query string آدرس پروکسی اضافه کنید.
- اگر نیاز به همزمانی بالاست، مطمئن شوید که محدودیتهای تعداد اتصالات موازی به هر پروکسی را رعایت میکنید؛ برخی سرویسدهندهها throttle دارند.
- آزمایش: قبل از اجرا روی دیتاست واقعی، واترفال را با چند URL تست کنید تا ترتیب انتخاب پروکسی و رفتار fallback را ببینید.
جمعبندی
پیادهسازی یک Proxy Waterfall در Scrapy به شما کمک میکند هزینهها را بهینه کنید و همزمان پایداری اسکراپ را افزایش دهید. ایده اصلی این است که ابتدا تلاشهای ارزانتر را امتحان کنید، سپس به صورت تدریجی به پروکسیهای قویتر بروید و همیشه شرایطی برای fallback و بررسی کلید API داشته باشید. با کمی توسعه بیشتر میتوانید سیاستهای انتخاب پروکسی را بر اساس دامنه، نرخ موفقیت، یا نیازهای رندرینگ سفارشی کنید.





