پارسر در Scrapy به دلیل تایماوت سقوط میکند، پروکسیپول به سرعت بیشتری از حد انتظار مصرف میشود و لاگها با پاسخهای 407 و 403 پر شدهاند — آیا این تصویر آشنا نیست؟ در 90% موارد مشکل در خود پروکسیها نیست، بلکه در نحوه نوشتن DownloaderMiddleware است. ما پنج اشتباه رایج در میدلور را بررسی میکنیم که ترافیک گرانقیمت را به درخواستهای بیفایده تبدیل میکند و نشان میدهیم چگونه میتوان آنها را با کد اصلاح کرد.
اشتباه 1: چرخش ابتدایی بدون در نظر گرفتن وضعیت پروکسی
رایجترین ساختاری که در آموزشها میتوان یافت — random.choice(PROXY_LIST) درون process_request است. مشکل این است که چنین چرخشی نمیداند کدام پروکسی به تازگی مسدود شده و کدام هنوز فعال است. در نتیجه، پارسر به ارسال درخواستها از طریق IP مسدود شده ادامه میدهد، 403/429 دریافت میکند، تلاش مجدد میکند — و دوباره همان آدرس را انتخاب میکند، زیرا انتخاب تصادفی است و «گرههای بد» را حذف نمیکند.
رویکرد صحیح — پیگیری وضعیت هر پروکسی: تعداد درخواستهای موفق، تعداد خطاها، زمان آخرین استفاده. در اینجا یک نسخه کارآمد حداقلی آورده شده است:
import random
import time
class ProxyPool:
def __init__(self, proxies):
self.proxies = {p: {"fails": 0, "last_used": 0, "banned_until": 0} for p in proxies}
def get_proxy(self):
now = time.time()
available = [
p for p, state in self.proxies.items()
if state["banned_until"] < now
]
if not available:
# اگر همه مسدود شدهاند، قدیمیترین مسدودیت را ریست میکنیم
available = list(self.proxies.keys())
return random.choice(available)
def mark_fail(self, proxy, cooldown=300):
self.proxies[proxy]["fails"] += 1
self.proxies[proxy]["banned_until"] = time.time() + cooldown
def mark_success(self, proxy):
self.proxies[proxy]["fails"] = 0
self.proxies[proxy]["last_used"] = time.time()
این پول، IPهای مسدود شده را در زمان «خنکسازی» (cooldown) حذف میکند و آنها را بعداً به چرخه بازمیگرداند. این به طور قابل توجهی مصرف ترافیک را کاهش میدهد، زیرا شما به طور مکرر به یک گره مسدود شده ضربه نمیزنید.
اشتباه 2: پردازش نادرست تلاش مجدد و کدهای وضعیت
دومین اشتباه رایج — استفاده از RetryMiddleware استاندارد از Scrapy بدون تغییر. به طور پیشفرض، این میدلور درخواست را بر روی همان پروکسی که آن را رد کرده، تلاش مجدد میکند، مگر اینکه شما به وضوح تغییر پروکسی را در process_exception اضافه کرده باشید. در نتیجه، تصویر کلاسیک را داریم: 3 تلاش مجدد، 3 مسدودیت، درخواست همچنان سقوط میکند و ترافیک از قبل مصرف شده است.
نکته دوم — همه کدهای وضعیت نیاز به تلاش مجدد یکسان ندارند. 429 (بسیاری از درخواستها) نیاز به وقفه و تغییر IP دارد، 403 معمولاً به معنای مسدودیت پروکسی خاص است (نیاز به تعویض فوری)، و 5xx — این معمولاً یک مشکل موقتی در سمت سرور است، میتوان بر روی همان پروکسی تلاش مجدد کرد. اگر همه چیز را در یک دسته قرار دهید، میدلور یا به طور بیش از حد پروکسی را میسوزاند، یا خیلی طولانی منتظر میماند در جایی که باید بلافاصله IP را تغییر دهد.
class SmartRetryMiddleware:
def __init__(self, pool):
self.pool = pool
def process_response(self, request, response, spider):
proxy = request.meta.get("proxy")
if response.status in (403, 407):
if proxy:
self.pool.mark_fail(proxy, cooldown=600)
new_request = request.copy()
new_request.meta["proxy"] = self.pool.get_proxy()
new_request.dont_filter = True
return new_request
if response.status == 429:
if proxy:
self.pool.mark_fail(proxy, cooldown=120)
new_request = request.copy()
new_request.meta["proxy"] = self.pool.get_proxy()
new_request.dont_filter = True
return new_request
if proxy:
self.pool.mark_success(proxy)
return response
در اینجا مهم است که خنکسازی را بر اساس نوع خطا تقسیم کنید: مسدودیت سخت (403/407) — وقفه طولانی، محدودیت فرکانس (429) — وقفه کوتاه. این به صرفهجویی در دهها درصد ترافیک در طول پروسههای طولانی کمک میکند.
اشتباه 3: عدم وجود جلسات چسبنده برای سایتهای دارای احراز هویت
اگر پارسر با سایتی کار کند که دارای ورود، سبد خرید، صفحهبندی با حفظ وضعیت یا کپچا با بررسی IP باشد — تغییر پروکسی برای هر درخواست جلسه را خراب میکند. سایت میبیند که درخواست شماره 1 از یک IP آمده، و درخواست شماره 2 (در چارچوب همان جلسه کوکی) از IP دیگری آمده و این بلافاصله حفاظت از ربات را فعال میکند، حتی اگر هر دو IP «پاک» باشند.
راه حل — پیوند دادن یک پروکسی به یک جلسه منطقی (برای مثال، به یک حساب خاص یا به زنجیرهای از درخواستها در یک دامنه) برای مدت زمان ثابت، و نه تغییر آن برای هر درخواست. این به عنوان جلسه چسبنده شناخته میشود.
class StickySessionMiddleware:
def __init__(self, pool, ttl=600):
self.pool = pool
self.ttl = ttl
self.sessions = {} # session_id -> (proxy, expires_at)
def process_request(self, request, spider):
session_id = request.meta.get("session_id")
if not session_id:
return
now = time.time()
session = self.sessions.get(session_id)
if session and session[1] > now:
request.meta["proxy"] = session[0]
else:
proxy = self.pool.get_proxy()
self.sessions[session_id] = (proxy, now + self.ttl)
request.meta["proxy"] = proxy
برای کارهایی که نیاز به ثبات IP در طول جلسه دارند — احراز هویت، کار با حساب کاربری، فرمهای چند مرحلهای — بهترین گزینهها پروکسیهای مسکونی با پشتیبانی از جلسات هستند: آنها اجازه میدهند که یک IP خروجی یکسان برای چند دقیقه یا ساعت حفظ شود و سپس به طور کنترلشده تغییر یابد، نه به طور تصادفی برای هر درخواست.
اشتباه 4: انتقال نادرست احراز هویت پروکسی
چهارمین اشتباه — فنی است، اما تقریباً در هر پروژه دومی وجود دارد. توسعهدهندگان نام کاربری و رمز عبور پروکسی را مستقیماً در URL به شکل http://user:pass@ip:port از طریق request.meta["proxy"] منتقل میکنند. این در اکثر موارد کار میکند، اما در هنگام استفاده از پروکسی از طریق تونل HTTPS یا در کار با برخی ارائهدهندگان، این روش احراز هویت به درستی توسط HttpProxyMiddleware استاندارد پردازش نمیشود و درخواستها با 407 Proxy Authentication Required سقوط میکنند، حتی اگر اعتبارنامهها صحیح باشند.
روش مطمئنتر — انتقال هدر Proxy-Authorization به وضوح، کدگذاری شده در base64:
import base64
class ProxyAuthMiddleware:
def process_request(self, request, spider):
proxy = request.meta.get("proxy")
if not proxy:
return
# پروکسی بدون اعتبارنامه در URL
request.meta["proxy"] = proxy
user = request.meta.get("proxy_user")
password = request.meta.get("proxy_pass")
if user and password:
credentials = f"{user}:{password}"
encoded = base64.b64encode(credentials.encode()).decode()
request.headers["Proxy-Authorization"] = f"Basic {encoded}"
این روش در هنگام مقیاسگذاری به صدها درخواست موازی پایدارتر عمل میکند و به نوع خاصی از نسخه کتابخانه بستگی ندارد که چگونه URL را با اعتبارنامههای جاسازی شده تجزیه میکند. این موضوع به ویژه در کار با پروکسیهای موبایل که احراز هویت اغلب به IP whitelist یا بررسی دقیق هدرها وابسته است، بسیار حیاتی است.
اشتباه 5: عدم نظارت و ثبت مسدودیتها
آخرین و شاید پرهزینهترین اشتباه — عدم ثبت اینکه کدام پروکسیها مسدود میشوند، چقدر و در کدام دامنهها. بدون این دادهها، نمیتوان فهمید که چه چیزی ترافیک را هدر میدهد: آیا پول پروکسی در یک سایت خاص تمام شده است یا مشکل در خود پارسر است (درخواستهای بیش از حد، عدم تأخیر، هدرهای مشکوک).
حداقل مجموعهای از معیارهایی که باید در میدلور ثبت شود:
- تعداد درخواستها برای هر پروکسی در طول جلسه پارسینگ
- تعداد و کدهای خطا (403، 407، 429، 5xx) برای هر پروکسی
- زمان عمر پروکسی تا اولین مسدودیت
- دامنههایی که در آنها مسدودیتها بیشتر اتفاق میافتد
import logging
import json
logger = logging.getLogger("proxy_stats")
class ProxyStatsMiddleware:
def __init__(self):
self.stats = {}
def process_response(self, request, response, spider):
proxy = request.meta.get("proxy", "unknown")
domain = request.url.split("/")[2]
key = f"{proxy}|{domain}"
entry = self.stats.setdefault(key, {"requests": 0, "errors": 0})
entry["requests"] += 1
if response.status in (403, 407, 429):
entry["errors"] += 1
if entry["requests"] % 50 == 0:
logger.info(json.dumps(self.stats))
return response
بدون چنین آماری، هر گونه تلاش برای «بهینهسازی» میدلور به حدس و گمان تبدیل میشود. با این آمار، شما به وضوح میبینید: اگر به عنوان مثال، یک دامنه خاص 80% از پروکسیپول را در 10 درخواست اول مسدود کند — مشکل در پروکسی نیست، بلکه در الگوی درخواستها است (عدم چرخش User-Agent، فرکانس بسیار بالا، عدم تأخیر بین درخواستها).
مثال کارآمد میدلور به طور کامل
همه چیز را در settings.py جمعآوری میکنیم — ترتیب میدلور حیاتی است، زیرا به ترتیب اعمال بررسیها بستگی دارد:
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.ProxyAuthMiddleware": 350,
"myproject.middlewares.StickySessionMiddleware": 400,
"myproject.middlewares.SmartRetryMiddleware": 550,
"myproject.middlewares.ProxyStatsMiddleware": 900,
}
RETRY_ENABLED = False # تلاش مجدد استاندارد را غیرفعال میکند، از خودمان استفاده میکنیم
DOWNLOAD_TIMEOUT = 15
CONCURRENT_REQUESTS_PER_DOMAIN = 8
به RETRY_ENABLED = False توجه کنید — این اصل است، در غیر این صورت مکانیزم تلاش مجدد داخلی Scrapy با منطق تغییر پروکسی شما تداخل خواهد داشت و درخواستها شروع به تکرار یا تلاش مجدد دو بار خواهند کرد. همچنین مهم است که CONCURRENT_REQUESTS_PER_DOMAIN را محدود کنید — همزمانی بسیار بالا برای یک دامنه حتی با چرخش پروکسی برای سیستمهای ضد ربات مشکوک به نظر میرسد.
کدام نوع پروکسی را برای Scrapy انتخاب کنیم
میدلور نیمی از مشکل را حل میکند، نیمه دیگر — انتخاب صحیح خود پروکسیپول برای وظیفه. در زیر مقایسهای برای سناریوهای رایج پارسینگ آورده شده است.
| نوع پروکسی | کی استفاده شود | مزایا | معایب |
|---|---|---|---|
| پروکسیهای دیتاسنتر | پارسینگ انبوه صفحات باز بدون حفاظت ضد ربات سخت | سرعت بالا، هزینه پایین ترافیک | به راحتی شناسایی میشوند، اغلب در لیست سیاه هستند |
| پروکسیهای مسکونی | پارسینگ بازارها، سایتهای با حفاظت JS، احراز هویت | IPهای واقعی، درصد پایین مسدودیت، پشتیبانی از جلسات چسبنده | سرعت پایینتر نسبت به دیتاسنتر |
| پروکسیهای موبایل | پارسینگ نسخههای موبایل سایتها و API با حفاظت ضد ربات سخت | اعتماد حداکثری از سوی سایتهای هدف | بالاترین هزینه ترافیک |
قاعده عملی: برای پارسینگ صفحات ایستا بدون حفاظت قوی، پروکسیهای دیتاسنتر ارزان به همراه میدلور مناسب از این مقاله مناسب هستند. اگر سایت از Cloudflare، PerimeterX، DataDome یا سیستمهای مشابه استفاده کند — IPهای دیتاسنتر تقریباً بلافاصله مسدود میشوند و در اینجا بهتر است بلافاصله به پول مسکونی بروید و زمان را در دیباگ میدلور صرفهجویی کنید.
چک لیست قبل از راهاندازی پارسر در تولید
- میدلور پروکسیهای مسدود شده را در زمان خنکسازی حذف میکند، نه اینکه به طور تصادفی آنها را انتخاب کند
- منطق تلاش مجدد انواع خطاها را (403/407 در مقابل 429 در مقابل 5xx) با خنکسازی متفاوت تشخیص میدهد
- برای وظایف جلسهای از پیوند چسبنده پروکسی استفاده میشود، نه تغییر برای هر درخواست
- احراز هویت پروکسی از طریق هدر Proxy-Authorization منتقل میشود، نه فقط از طریق URL
- آمار مسدودیتها بر اساس دامنهها و پروکسیها برای تشخیص مشکلات ثبت میشود
- تلاش مجدد استاندارد Scrapy غیرفعال شده است تا با منطق سفارشی تداخل نداشته باشد
- همزمانی با مقادیر معقول محدود شده است، نه اینکه به حداکثر تنظیم شود
نتیجهگیری
بیشتر مشکلات مربوط به مصرف ترافیک پروکسی در Scrapy با خرید پول IP بیشتر حل نمیشود، بلکه با اصلاح منطق میدلور: تقسیم خطاها بر اساس نوع، جلسات چسبنده برای سناریوهای پیچیده، احراز هویت صحیح و نظارت مداوم بر مسدودیتها. کد این مقاله میتواند به عنوان پایهای برای پروژه خاص شما استفاده شود — ساختار برای پارسرهای کوچک و نصبهای توزیع شده Scrapy-Cluster کارآمد باقی میماند.
اگر میدلور به درستی تنظیم شده است و با این حال مسدودیتها همچنان بیش از حد اتفاق میافتند — احتمالاً مشکل در کیفیت خود پول IP است. برای پارسینگ سایتهای با حفاظت پیشرفته ضد ربات، بهتر است از پروکسیهای مسکونی با پشتیبانی از جلسات استفاده کنید — آنها به طور قابل توجهی درصد مسدودیت را نسبت به آدرسهای دیتاسنتر با همان منطق میدلور کاهش میدهند.