لماذا تحتاج Scrapy proxy middleware في الإنتاج؟
إذا سبق لك تشغيل زاحف Scrapy على موقع يحدّ من معدل الطلبات، فأنت تعرف النتيجة: حظر كامل بعد بضع مئات من الطلبات. المشكلة ليست في سرعتك، بل في أن كل طلب يخرج من نفس IP. Scrapy residential proxies تحل هذه المشكلة بتوزيع الطلبات عبر آلاف عناوين IP السكنية الحقيقية، لكن التكامل يتطلب أكثر من مجرد تمرير رابط بروكسي في settings.py.
هذا الدليل يأخذك من بنية Scrapy الداخلية إلى middleware مخصص كامل، مروراً بمعالجة الأعطال والنشر والمراقبة. كل ما تحتاجه كمهندس scraping يعمل في بيئة إنتاج حقيقية.
بنية Scrapy الداخلية: أين تناسب البروكسي؟
Scrapy يعالج كل طلب عبر سلسلة من downloader middlewares قبل إرساله وبعد استلام الاستجابة. هذه الـ middlewares هي المكان الطبيعي لحقن البروكسي لأنها تتحكم في كائن Request قبل وصوله إلى downloader.
ترتيب الـ middlewares الافتراضي مهم:
- HttpProxyMiddleware (ترتيب 560) — يقرأ
http_proxyمن البيئة أو منrequest.meta['proxy'] - RetryMiddleware (ترتيب 550) — يعيد المحاولة عند الفشل
- RedirectMiddleware (ترتيب 600) — يتبع إعادات التوجيه
القاعدة الذهبية: middleware البروكسي يجب أن يعمل قبل RetryMiddleware حتى يتمكن من تغيير البروكسي بين المحاولات. رقم الترتيب الأقل يعمل أولاً.
يمكنك تمرير البروكسي بعدة طرق:
request.meta['proxy']— الطريقة القياسية في Scrapy- متغيرات البيئة
HTTP_PROXY— لكنها لا تدعم الدوران - Middleware مخصص — المرن والأقوى
الطريقة الثالثة هي ما سنبنيه الآن.
بناء RotatingProxyMiddleware مخصص من الصفر
الهدف: middleware يدير pool من البروكسي السكني، يوزّع الطلبات عشوائياً، يدعم الجلسات اللاصقة (sticky sessions)، ويُزيل البروكسي المعطّل مؤقتاً.
import random
import time
import logging
from scrapy import signals
from scrapy.downloadermiddlewares.httpproxy import HttpProxyMiddleware
from scrapy.exceptions import NotConfigured
logger = logging.getLogger(__name__)
class ProxyHatRotatingMiddleware(HttpProxyMiddleware):
"""
Scrapy proxy middleware that rotates residential proxies
from ProxyHat on every request or maintains sticky sessions.
"""
def __init__(self, proxy_username, proxy_password, proxy_host, proxy_port):
super().__init__()
self.username = proxy_username
self.password = proxy_password
self.host = proxy_host
self.port = proxy_port
self.banned_proxies = {} # proxy_key: ban_until_timestamp
self.stats = {} # proxy_key: {success: int, fail: int}
@classmethod
def from_crawler(cls, crawler):
username = crawler.settings.get('PROXYHAT_USERNAME')
password = crawler.settings.get('PROXYHAT_PASSWORD')
host = crawler.settings.get('PROXYHAT_HOST', 'gate.proxyhat.com')
port = crawler.settings.get('PROXYHAT_PORT', 8080)
if not username or not password:
raise NotConfigured(
'Set PROXYHAT_USERNAME and PROXYHAT_PASSWORD in settings'
)
middleware = cls(username, password, host, port)
crawler.signals.connect(
middleware.spider_closed, signal=signals.spider_closed
)
return middleware
def _build_proxy_url(self, country=None, city=None, session_id=None):
"""Build ProxyHat proxy URL with optional geo-targeting and session."""
parts = [self.username]
if country:
parts.append(f'country-{country}')
if city:
parts.append(f'city-{city}')
if session_id:
parts.append(f'session-{session_id}')
user_part = '-'.join(parts)
return f'http://{user_part}:{self.password}@{self.host}:{self.port}'
def _get_proxy(self, request):
"""Select a proxy for the request, respecting sticky sessions."""
# Sticky session: reuse same proxy for requests with same meta key
session_id = request.meta.get('proxy_session')
country = request.meta.get('proxy_country')
city = request.meta.get('proxy_city')
if session_id:
return self._build_proxy_url(
country=country, city=city, session_id=session_id
)
# Per-request rotation with geo-targeting
return self._build_proxy_url(country=country, city=city)
def process_request(self, request, spider):
"""Assign a proxy to every outgoing request."""
proxy_url = self._get_proxy(request)
request.meta['proxy'] = proxy_url
request.meta['proxy_had_proxy'] = True
logger.debug(f'Using proxy for {request.url}')
def process_response(self, request, response, spider):
"""Detect bans and record success stats."""
if response.status in (403, 429, 503):
logger.warning(
f'Possible ban detected: {response.status} '
f'for {request.url}'
)
return self._handle_ban(request, response, spider)
# Record success
proxy_key = request.meta.get('proxy', 'none')
self.stats.setdefault(proxy_key, {'success': 0, 'fail': 0})
self.stats[proxy_key]['success'] += 1
return response
def process_exception(self, request, exception, spider):
"""Handle connection errors by retrying with a different proxy."""
logger.warning(
f'Proxy error {type(exception).__name__} for {request.url}'
)
proxy_key = request.meta.get('proxy', 'none')
self.stats.setdefault(proxy_key, {'success': 0, 'fail': 0})
self.stats[proxy_key]['fail'] += 1
# Mark proxy as temporarily banned (60 seconds cooldown)
self.banned_proxies[proxy_key] = time.time() + 60
# Don't retry here — let RetryMiddleware handle it
return None
def _handle_ban(self, request, response, spider):
"""On ban, mark proxy and retry with a fresh one."""
proxy_key = request.meta.get('proxy', 'none')
self.banned_proxies[proxy_key] = time.time() + 120
self.stats.setdefault(proxy_key, {'success': 0, 'fail': 0})
self.stats[proxy_key]['fail'] += 1
# Force a new proxy on retry by clearing session
if 'proxy_session' in request.meta:
del request.meta['proxy_session']
return response # Let RetryMiddleware evaluate
def spider_closed(self, spider):
"""Log proxy stats when spider finishes."""
logger.info('=== Proxy Stats Summary ===')
for proxy, counts in self.stats.items():
total = counts['success'] + counts['fail']
rate = counts['success'] / total * 100 if total else 0
logger.info(
f'{proxy}: {rate:.1f}% success '
f'({counts["success"]}/{total})'
)
الآن في settings.py:
# settings.py
PROXYHAT_USERNAME = 'your_username'
PROXYHAT_PASSWORD = 'your_password'
PROXYHAT_HOST = 'gate.proxyhat.com'
PROXYHAT_PORT = 8080
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyHatRotatingMiddleware': 540,
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': None,
}
RETRY_TIMES = 3
RETRY_HTTP_CODES = [429, 500, 502, 503, 504]
ولاستخدامه في الزاحف:
import scrapy
import uuid
class ProductSpider(scrapy.Spider):
name = 'products'
def start_requests(self):
urls = ['https://example.com/products?page=1',
'https://example.com/products?page=2']
for url in urls:
yield scrapy.Request(
url,
callback=self.parse,
meta={
'proxy_country': 'US', # geo-target to US IPs
'proxy_session': str(uuid.uuid4()), # sticky session
}
)
def parse(self, response):
for product in response.css('.product-item'):
yield {
'name': product.css('h2::text').get(),
'price': product.css('.price::text').get(),
}
scrapy-rotating-proxies مقابل بناء middleware مخصص
مكتبة scrapy-rotating-proxies هي حل مجتمعي شائع، لكن هل تناسب حالة استخدام البروكسي السكني؟ لنقارن:
| المعيار | scrapy-rotating-proxies | Middleware مخصص (ما بنيناه) |
|---|---|---|
| نوع البروكسي | قائمة IPs ثابتة فقط | بروكسي سكني متحرك (gateway) |
| الدوران | round-robin / عشوائي | لكل طلب + جلسات لاصقة |
| الاستهداف الجغرافي | غير مدعوم | بلد + مدينة |
| كشف الحظر | أساسي (إزالة IP من القائمة) | تتبع معدل النجاح لكل IP |
| إعادة المحاولة | مدمجة | تكامل مع RetryMiddleware |
| الصيانة | محدودة (آخر تحديث 2020) | أنت تتحكم بالكامل |
الخلاصة: إذا كنت تستخدم بروكسي سكني عبر gateway (مثل ProxyHat)، فالـ middleware المخصص هو الخيار الصحيح. scrapy-rotating-proxies مصمم لقوائم IP الثابتة ولا يدعم الاستهداف الجغرافي أو الجلسات اللاصقة عبر gateway.
معالجة أعطال البروكسي: RetryMiddleware مع دوران IP
عندما يفشل طلب بسبب بروكسي معطّل، لا فائدة من إعادة المحاولة بنفس البروكسي. الحل: إزالة request.meta['proxy'] قبل إعادة المحاولة، مما يسمح لـ ProxyHatRotatingMiddleware بتعيين بروكسي جديد.
from scrapy.downloadermiddlewares.retry import RetryMiddleware
from scrapy.utils.response import response_status_message
import logging
logger = logging.getLogger(__name__)
class ProxyAwareRetryMiddleware(RetryMiddleware):
"""Retry middleware that clears proxy on failure to force rotation."""
def __init__(self, settings):
super().__init__(settings)
self.max_proxy_retries = settings.getint(
'MAX_PROXY_RETRIES', 3
)
def process_response(self, request, response, spider):
# Track proxy retry count
retry_count = request.meta.get('proxy_retry_count', 0)
if response.status in self.retry_http_codes:
retry_count += 1
if retry_count > self.max_proxy_retries:
logger.error(
f'Max proxy retries exceeded for {request.url}'
)
return response
logger.info(
f'Retrying {request.url} with new proxy '
f'(attempt {retry_count})'
)
request = request.replace(dont_filter=True)
request.meta['proxy_retry_count'] = retry_count
# Clear session to get a fresh proxy
request.meta.pop('proxy_session', None)
request.meta['proxy_country'] = request.meta.get(
'proxy_country'
)
return request
return response
def process_exception(self, request, exception, spider):
retry_count = request.meta.get('proxy_retry_count', 0)
retry_count += 1
if retry_count > self.max_proxy_retries:
logger.error(
f'Max proxy retries exceeded due to '
f'{type(exception).__name__} for {request.url}'
)
return None
request = request.replace(dont_filter=True)
request.meta['proxy_retry_count'] = retry_count
request.meta.pop('proxy_session', None)
return request
في settings.py:
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyHatRotatingMiddleware': 540,
'myproject.middlewares.ProxyAwareRetryMiddleware': 550,
'scrapy.downloadermiddlewares.retry.RetryMiddleware': None,
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': None,
}
RETRY_TIMES = 5
MAX_PROXY_RETRIES = 3
المواقع المعتمدة على JavaScript: scrapy-splash و scrapy-playwright
العديد من المواقع الحديثة تعرض المحتوى عبر JavaScript. Scrapy وحده لا ينفّذ JS، لكن يمكنك دمج البروكسي مع محركات المتصفح.
الخيار 1: scrapy-playwright مع بروكسي
scrapy-playwright يتيح تشغيل متصفح حقيقي داخل Scrapy. تمرير البروكسي مباشرة:
# settings.py
PLAYWRIGHT_BROWSER_TYPE = 'chromium'
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyHatRotatingMiddleware': 540,
'scrapy_playwright.middleware.ScrapyPlaywrightDownloadHandler': 885,
}
# In your spider:
class JSSpider(scrapy.Spider):
name = 'js_heavy'
def start_requests(self):
yield scrapy.Request(
'https://example.com/dynamic-data',
meta={
'playwright': True,
'playwright_include_page': True,
'proxy_country': 'DE',
'proxy_session': str(uuid.uuid4()),
}
)
async def parse(self, response):
page = response.meta.get('playwright_page')
if page:
# Wait for dynamic content to load
await page.wait_for_selector('.product-grid')
html = await page.content()
await page.close()
# Parse the rendered HTML
selector = scrapy.Selector(text=html)
for item in selector.css('.product'):
yield {'name': item.css('h3::text').get()}
الخيار 2: scrapy-splash
إذا كنت تفضل خدمة منفصلة للعرض، scrapy-splash يعمل مع Splash Docker container. تمرير البروكسي عبر Splash arguments:
SPLASH_URL = 'http://splash:8050'
DOWNLOADER_MIDDLEWARES = {
'scrapy_splash.SplashDeduplicateArgsMiddleware': 723,
'myproject.middlewares.ProxyHatRotatingMiddleware': 540,
}
# In spider:
from scrapy_splash import SplashRequest
def start_requests(self):
yield SplashRequest(
'https://example.com',
self.parse,
args={'wait': 2, 'proxy': 'http://user-country-US:pass@gate.proxyhat.com:8080'},
endpoint='render.html'
)
ملاحظة مهمة: عند استخدام scrapy-playwright أو scrapy-splash، البروكسي يمر عبر طبقتين — middleware و محرك المتصفح. تأكد من أن middleware لا يضيف بروكسي مزدوج على طلبات Playwright (تحقق من request.meta.get('playwright')).
النشر: من التطوير إلى الإنتاج
الخيار 1: Docker + Cron (الأبسط)
للزواحف المجدولة، حاوية Docker مع cron كافية:
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Run spider on schedule via cron
CMD ["scrapy", "crawl", "products", "-s", "LOG_LEVEL=INFO"]
ثم في docker-compose.yml:
services:
scraper:
build: .
environment:
- PROXYHAT_USERNAME=${PROXYHAT_USERNAME}
- PROXYHAT_PASSWORD=${PROXYHAT_PASSWORD}
volumes:
- ./output:/app/output
deploy:
replicas: 3 # Run 3 concurrent instances
restart: unless-stopped
الخيار 2: Scrapyd
Scrapyd يوفر API لنشر وتشغيل الزواحف عن بعد. مناسب عندما تدير عدة زواحف وتحتاج جدولة مركزية.
الخيار 3: ScrapeOps أو خدمات مشابهة
خدمات مثل ScrapeOps تضيف طبقة مراقبة وجدولة. مفيدة إذا كنت لا تريد إدارة البنية التحتية بنفسك.
| المعيار | Docker + Cron | Scrapyd | ScrapeOps |
|---|---|---|---|
| التعقيد | منخفض | متوسط | منخفض |
| المرونة | عالية | عالية | محدودة |
| المراقبة | يدوية | Scrapyd API | لوحة تحكم مدمجة |
| التكلفة | الخادم فقط | الخادم فقط | اشتراك شهري |
| التوسع | يدوي (replicas) | يدوي | تلقائي |
المراقبة: معدل النجاح لكل IP وكشف الحظر
في بيئة الإنتاج، تحتاج مراقبة نشطة. Middleware الذي بنيناه يسجّل إحصائيات، لكن يمكنك تعزيزه بـ إشعارات فورية.
إضافة StatsCollector مخصص
# middlewares.py — add to ProxyHatRotatingMiddleware
from scrapy.statscollectors import StatsCollector
class ProxyStatsCollector(StatsCollector):
"""Custom stats collector that tracks per-IP success rates."""
def open_spider(self, spider):
super().open_spider(spider)
self._proxy_stats = {}
def record_proxy_result(self, proxy_key, success=True):
if proxy_key not in self._proxy_stats:
self._proxy_stats[proxy_key] = {'success': 0, 'fail': 0}
key = 'success' if success else 'fail'
self._proxy_stats[proxy_key][key] += 1
def get_proxy_stats(self):
return self._proxy_stats.copy()
مؤشرات المراقبة الأساسية
- معدل النجاح الإجمالي: نسبة الطلبات الناجحة (200) مقابل المرفوضة (403/429)
- زمن الاستجابة المتوسط: البروكسي البطيء يبطئ الزاحف بالكامل
- عدد المحاولات المتكررة: ارتفاع مفاجئ يشير إلى حظر جماعي
- معدل النجاح حسب البلد: بعض المواقع تحظر نطاقات جغرافية محددة
نصيحة عملية: ابدأ بـ CONCURRENT_REQUESTS = 16 و DOWNLOAD_DELAY = 1 مع البروكسي السكني. زيادة التزامن تدريجياً مع مراقبة معدل النجاح. إذا انخفض عن 85%، قلّل التزامن.
أفضل الممارسات لاستخدام البروكسي السكني مع Scrapy
- استخدم جلسات لاصقة عند الحاجة فقط. تسجيل الدخول وإكمال النماذج يحتاج نفس IP. أما جمع البيانات العامة فلا يحتاج جلسات.
- حدد البلد المناسب. إذا كان الموقع يعرض أسعاراً مختلفة حسب المنطقة، استخدم
proxy_countryلضمان一致性 البيانات. - احترم robots.txt وشروط الخدمة. البروكسي لا يمنحك الحق في تجاهل القوانين.
- راقب معدل النجاح. إذا انخفض عن 80%، غيّر استراتيجية الدوران أو قلّل التزامن.
- لا تشارك كلمات المرور. استخدم بيانات اعتماد مختلفة لكل زاحف.
- اختبر بروكسي واحد أولاً. قبل تشغيل الزاحف الكامل، تحقق من أن
curlيعمل:
curl -x http://USERNAME:PASSWORD@gate.proxyhat.com:8080 https://httpbin.org/ip
الخلاصة: النقاط الرئيسية
- Scrapy proxy middleware هو المكان الصحيح لإدارة البروكسي — وليس متغيرات البيئة أو settings بسيطة.
- بناء middleware مخصص يعطيك تحكماً كاملاً في Scrapy proxy rotation، الاستهداف الجغرافي، والجلسات اللاصقة.
scrapy-rotating-proxiesمناسب فقط لقوائم IP الثابتة — لا للبروكسي السكني عبر gateway.- معالجة أعطال البروكسي تتطلب RetryMiddleware مخصص يزيل البروكسي الفاشل ويُعيّن واحداً جديداً.
- البروكسي السكني مثل ProxyHat يوفر دوران IP تلقائي مع استهداف جغرافي — لا تحتاج لإدارة قائمة IPs.
- المراقبة النشطة (معدل النجاح، زمن الاستجابة) ضرورية في الإنتاج.
- للمواقع المعتمدة على JS، استخدم scrapy-playwright مع البروكسي.
جاهز لبدء الزحف مع بروكسي سكني موثوق؟ استكشف خطط ProxyHat أو تحقق من المواقع المتاحة.






