← بازگشت به وبلاگ

منطق تلاش مجدد پارسر: چگونه درخواست‌های تکراری تا ۴۰٪ ترافیک را مصرف می‌کنند و چگونه این مشکل را حل کنیم

درخواست‌های تکراری می‌توانند تا ۴۰٪ ترافیک پروکسی را مصرف کنند. نشان می‌دهیم چگونه خسارات واقعی را محاسبه کرده و از طریق بک‌آف صحیح، مدار شکن و چرخش هوشمند پروکسی‌ها آن‌ها را کاهش دهیم.

📅۵ مهر ۱۴۰۵

اگر هزینه پروکسی سریع‌تر از حجم داده‌های جمع‌آوری‌شده افزایش یابد، مشکل تقریباً همیشه در منطق retry است. پارسر به‌طور خاموش درخواست‌های ناموفق را چندین بار تکرار می‌کند، ترافیک را برای تایم‌اوت‌ها و کپچاها مصرف می‌کند، و توسعه‌دهنده حتی این هزینه‌ها را در لاگ‌ها نمی‌بیند. بیایید بررسی کنیم که چگونه خسارات واقعی را محاسبه کرده و آنها را بدون از دست دادن کیفیت داده‌ها کاهش دهیم.

چرا منطق retry ترافیک را مصرف می‌کند

بیشتر پارسرها با منطق retry ساده نوشته شده‌اند: اگر درخواست موفق نشد — تکرار کن، و این کار را تا 3-5 بار انجام بده. مشکل این است که هر درخواست تکراری نه تنها یک درخواست HTTP جدید است، بلکه یک چرخه کامل است: handshake TCP، مذاکره TLS، بارگذاری کامل صفحه (حتی اگر فقط یک بلوک داده نیاز باشد)، و گاهی اوقات بارگذاری مجدد تصاویر یا فایل‌های JS، اگر پارسر از مرورگر headless به جای یک کلاینت HTTP ساده استفاده کند.

تکرارها به‌ویژه در هنگام کار با پروکسی‌های مسکونی هزینه‌بر هستند، جایی که ترافیک بر اساس حجم محاسبه می‌شود، نه بر اساس تعداد درخواست‌ها. یک درخواست ناموفق به صفحه محصول Wildberries با تصاویر و اسکریپت‌ها می‌تواند 300-500 کیلوبایت هزینه داشته باشد. اگر پارسر 3 بار در هنگام تایم‌اوت تکرار کند، شما برای یک درخواست ناموفق چهار بار متوالی پرداخت می‌کنید — و این بدون تضمین اینکه تلاش چهارم موفق باشد.

دلیل دوم — retry به خاطر خطاهایی که نمی‌توان با تکرار اصلاح کرد. اگر سایت به دلیل شناسایی ربات 403 را برگرداند، درخواست تکراری با همان fingerprint و با همان جلسه کوکی تقریباً به‌طور قطع همان پاسخ را دریافت خواهد کرد. پارسر ترافیک را برای تلاش‌هایی مصرف می‌کند که از نظر ریاضی نمی‌توانند با موفقیت به پایان برسند، مگر اینکه آدرس IP یا fingerprint مرورگر تغییر کند.

چقدر ترافیک واقعاً برای تکرارها مصرف می‌شود

برای درک مقیاس مشکل، یک مثال ساده می‌زنیم. پارسر کارت‌های محصولات را از یک بازار جمع‌آوری می‌کند، اندازه متوسط پاسخ — 250 کیلوبایت (HTML + JSON API + بخشی از استاتیک). در هنگام کار پایدار بدون مسدودیت‌ها درصد درخواست‌های ناموفق در سطح 5-8% باقی می‌ماند. اما در هنگام پارسینگ تهاجمی از طریق پروکسی‌های ارزان دیتا سنتر، این شاخص می‌تواند به 25-35% افزایش یابد، زیرا ارائه‌دهنده هدف به سرعت الگو را شناسایی کرده و شروع به بازگرداندن کپچاها یا مسدودیت‌های موقتی بر اساس IP می‌کند.

بیایید با اعداد محاسبه کنیم. فرض کنید نیاز به جمع‌آوری 100,000 کارت محصول دارید:

درصد درخواست‌های ناموفق تکرارها برای 1 درخواست (به طور متوسط) ترافیک نهایی اضافه‌مصرف
5% 0.15 28.75 گیگابایت +15%
15% 0.45 36.25 گیگابایت +45%
30% 0.90 47.5 گیگابایت +90%

همان‌طور که مشاهده می‌شود، با درصد خطا 30% و استراتژی retry «تکرار تا 3 بار» ترافیک واقعی تقریباً دو برابر حداقل نظری 25 گیگابایت می‌شود. همین 20+ گیگابایت اضافی — خسارات مستقیم بودجه بر روی پروکسی است، که می‌توان آن را کاهش داد اگر منطق تکرارها را بازنگری کنیم.

اشتباهات رایج در منطق retry پارسرها

قبل از اصلاح منطق retry، باید الگوهای ضد الگوی رایج را شناسایی کنید که در 90% پارسرهای خودنویس وجود دارد:

  • تکرار بدون بررسی کد خطا. تکرار در هر وضعیت غیرعادی — 403، 429، 500، تایم‌اوت، قطع اتصال — آغاز می‌شود، در حالی که استراتژی پردازش برای آنها باید متفاوت باشد.
  • تأخیر ثابت بین تکرارها. به عنوان مثال، 2 ثانیه بین تلاش‌ها بدون توجه به اینکه آیا این اولین تلاش است یا پنجم — این یا برای سایت خیلی تهاجمی است یا برای حجم‌های بزرگ خیلی کند است.
  • تکرار با همان IP و همان جلسه. اگر سایت درخواست را به دلیل fingerprint مسدود کرده باشد، تکرار با پارامترهای یکسان نتیجه را تغییر نمی‌دهد، اما ترافیک را مصرف می‌کند.
  • عدم وجود حد بالای تلاش‌ها. برخی از پارسرها بر روی URL‌های «مرده» قفل می‌شوند و ده‌ها تکرار انجام می‌دهند قبل از اینکه تسلیم شوند.
  • عدم تمایز بین خطاهای موقتی و دائمی. 404 (صفحه وجود ندارد) و 503 (سرور موقتا در دسترس نیست) نیاز به منطق متفاوتی دارند — اما اغلب به صورت یکسان پردازش می‌شوند.

backoff نمایی با کد به زبان Python

یک راه حل ساده اما مؤثر — تأخیر نمایی با جیتتر (تغییر تصادفی)، که تعداد تکرارهای بی‌معنی را کاهش می‌دهد و بار را در زمان توزیع می‌کند. به جای تأخیر ثابت بین تلاش‌ها، تأخیر به‌طور نمایی افزایش می‌یابد، که به سایت زمان می‌دهد تا «خنک» شود بعد از مسدودیت، و به خود پارسر — اجازه می‌دهد تا ترافیک را برای درخواست‌هایی که تقریباً به‌طور قطع شکست خواهند خورد، مصرف نکند.

import time
import random
import requests

def fetch_with_backoff(url, proxies, max_retries=4, base_delay=1.0):
    retryable_codes = {429, 500, 502, 503, 504}
    non_retryable_codes = {404, 410}

    for attempt in range(max_retries + 1):
        try:
            response = requests.get(url, proxies=proxies, timeout=10)

            if response.status_code == 200:
                return response

            if response.status_code in non_retryable_codes:
                # هیچ دلیلی برای تکرار وجود ندارد — صفحه به‌طور فیزیکی وجود ندارد
                return None

            if response.status_code not in retryable_codes:
                return None

        except (requests.exceptions.Timeout,
                requests.exceptions.ConnectionError):
            pass  # خطای موقتی شبکه — می‌توان تکرار کرد

        if attempt == max_retries:
            return None

        # تأخیر نمایی با جیتتر
        delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
        time.sleep(delay)

    return None

ایده کلیدی این کد — تقسیم خطاها به سه دسته: آنهایی که نمی‌توان با تکرار اصلاح کرد (404، 410)، آنهایی که می‌توان با تأخیر تکرار کرد (429، 500-504، تایم‌اوت‌ها)، و همه چیز دیگر که به‌طور خودکار به عنوان شکست در نظر گرفته می‌شود بدون هزینه برای تلاش‌های اضافی. این تقسیم‌بندی به خودی خود ترافیک اضافی را 20-30% نسبت به «تکرار همه چیز» ساده کاهش می‌دهد.

نکته: Retry-After را در پردازش اضافه کنید — بسیاری از سایت‌ها خودشان به شما می‌گویند که چند ثانیه قبل از تکرار صبر کنید. نادیده گرفتن این هدر — دلیل رایج مسدودیت‌های اضافی و ترافیک است.

circuit breaker: چه زمانی باید متوقف شویم

backoff نمایی در سطح یک درخواست کمک می‌کند، اما از وضعیتی که کل دامنه یا یک گره پروکسی خاص به‌طور موقتی برای صدها URL متوالی در دسترس نیست، محافظت نمی‌کند. در اینجا به یک الگوی circuit breaker نیاز داریم — "قطع‌کننده خودکار"، که نسبت خطاها را در دوره اخیر ردیابی می‌کند و در صورت عبور از آستانه، تلاش‌ها را به‌طور موقت متوقف می‌کند، به جای اینکه به ضربه زدن به در بسته ادامه دهد.

class CircuitBreaker:
    def __init__(self, failure_threshold=0.5, window_size=50, cooldown=60):
        self.failure_threshold = failure_threshold
        self.window_size = window_size
        self.cooldown = cooldown
        self.results = []
        self.open_until = 0

    def is_open(self):
        return time.time() < self.open_until

    def record(self, success: bool):
        self.results.append(success)
        if len(self.results) > self.window_size:
            self.results.pop(0)

        if len(self.results) == self.window_size:
            failure_rate = 1 - sum(self.results) / self.window_size
            if failure_rate > self.failure_threshold:
                self.open_until = time.time() + self.cooldown
                self.results.clear()

منطق ساده است: اگر از آخرین 50 درخواست، بیشتر از نیمی از آنها شکست خورده باشد، پارسر تلاش‌ها را برای این دامنه یا پروکسی به مدت 60 ثانیه متوقف می‌کند. در این مدت می‌توان IP را تغییر داد، سرعت درخواست‌ها را کاهش داد یا به یک مجموعه پروکسی دیگر سوئیچ کرد. این به‌ویژه در هنگام کار با سایت‌های هدف که به‌طور موقتی دامنه IP را بعد از عبور از حد درخواست‌ها مسدود می‌کنند، مهم است — ادامه ضربه زدن به در بسته به معنای سوزاندن ترافیک بی‌فایده است.

چرخش هوشمند پروکسی در هنگام تکرارها

یکی از مؤثرترین اقدامات در برابر تکرارهای اضافی — عدم تکرار درخواست با همان IP است که قبلاً رد شده است. منطق ساده است: اگر خطا به مسدودیت IP مربوط باشد (403، 429، ریدایرکت به کپچا)، تغییر پروکسی قبل از تکرار به‌طور چشمگیری شانس موفقیت را افزایش می‌دهد و تعداد تلاش‌ها را کاهش می‌دهد.

برای پارسینگ بازارهایی مانند Wildberries، Ozon یا Avito، ترکیب زیر به خوبی کار می‌کند: درخواست‌های عادی از طریق پروکسی‌های دیتا سنتر انجام می‌شود — آنها سریع‌تر و ارزان‌تر هستند، و به محض اینکه تشخیص مسدودیت چندین بار متوالی فعال شود، پارسر به پروکسی‌های مسکونی تغییر می‌کند، که کمتر تحت فیلترهای سیستم‌های ضد ربات قرار می‌گیرند. این رویکرد ترکیبی مصرف کلی ترافیک را کاهش می‌دهد، زیرا IP‌های مسکونی گران فقط در جایی که واقعاً نیاز است استفاده می‌شوند، نه برای همه درخواست‌ها به‌طور متوالی.

نوع خطا استراتژی تکرار آیا تغییر IP لازم است؟
تایم‌اوت اتصال Backoff، 1-2 تکرار خیر
403 / کپچا چرخش فوری بله، ضروری است
429 (محدودیت نرخ) Backoff بر اساس Retry-After ترجیحی
500-503 Backoff، 2-3 تکرار خیر
404 / 410 بدون تکرار —

برای پارسرهایی که ترافیک موبایل را شبیه‌سازی می‌کنند (به عنوان مثال، جمع‌آوری داده‌ها از نسخه‌های موبایل برنامه‌های بازار یا شبکه‌های اجتماعی)، استفاده از پروکسی‌های موبایل منطقی است — آنها کمتر باعث مشکوک شدن سیستم‌های حفاظتی می‌شوند، زیرا اپراتورهای ارتباطی یک IP را به هزاران کاربر واقعی به‌طور همزمان اختصاص می‌دهند و مسدودیت نقطه‌ای برای سایت هدف غیرمعقول می‌شود.

نظارت بر معیارهای retry

بدون معیارها، بهینه‌سازی منطق retry به حدس و گمان تبدیل می‌شود. حداقل مجموعه‌ای از شاخص‌ها که باید در هر درخواست ثبت شوند:

  • نرخ تکرار — درصد درخواست‌هایی که حداقل یک تکرار نیاز داشتند.
  • موفقیت پس از تکرار — چه درصدی از تکرارها در نهایت با موفقیت به پایان رسیدند (اگر این شاخص پایین باشد، تکرارها فقط ترافیک را می‌سوزانند).
  • ترافیک برای نتیجه موفق — حجم کل داده‌های منتقل‌شده تقسیم بر تعداد رکوردهای موفق جمع‌آوری‌شده. این یک معیار کلیدی برای کارایی است.
  • توزیع خطاها بر اساس کدها — کمک می‌کند تا بفهمیم کجا اصلی‌ترین نشت ترافیک رخ می‌دهد: تایم‌اوت‌ها، 403، 429 یا چیز دیگری.
  • نرخ تکرار بر اساس گره‌های پروکسی خاص — اگر یک IP نرخ تکرار 80% داشته باشد و بقیه 10%، مشکل محلی است و با تغییر یک گره خاص حل می‌شود، نه کل منطق.

حتی یک جدول ساده در Google Sheets یا لاگ در CSV با این پنج معیار، که هر ساعت به‌روزرسانی می‌شود، داده‌های کافی برای مشاهده ناهنجاری‌ها و اصلاح به‌موقع استراتژی فراهم می‌کند — به عنوان مثال، کاهش فرکانس درخواست‌ها به یک بخش خاص از سایت یا افزایش درصد IP‌های مسکونی در مجموعه.

چک‌لیست بهینه‌سازی ترافیک بر روی retry

  1. کدهای خطا را به retryable و non-retryable تقسیم کنید — 404/410 را تکرار نکنید.
  2. backoff نمایی با جیتتر را به جای تأخیر ثابت پیاده‌سازی کنید.
  3. به هدر Retry-After احترام بگذارید، اگر سایت آن را ارسال می‌کند.
  4. قبل از تکرار IP را در صورت 403 و مشکوک شدن به شناسایی ربات تغییر دهید.
  5. یک محدودیت سخت برای تعداد تلاش‌ها تعیین کنید (معمولاً 3-4 کافی است).
  6. برای دامنه‌ها و گره‌های پروکسی با نرخ شکست بالا، circuit breaker را پیاده‌سازی کنید.
  7. نرخ تکرار و ترافیک برای نتیجه موفق را ثبت کنید — بدون معیارها، بهینه‌سازی غیرممکن است.
  8. مجموعه پروکسی را تقسیم کنید: دیتا سنترهای ارزان برای بخش‌های پایدار، IP‌های مسکونی یا موبایل — برای بخش‌های مشکل‌دار.

نتیجه‌گیری

منطق retry یک جزئیات کوچک در پارسر نیست، بلکه یکی از عوامل اصلی است که بر هزینه جمع‌آوری داده‌ها تأثیر می‌گذارد. استراتژی ساده «تکرار همه چیز» می‌تواند ترافیک واقعی را 40-90% نسبت به حداقل نظری افزایش دهد، در حالی که بخش عمده‌ای از تکرارها به همان شکستی که تلاش اول داشت، ختم می‌شود. تقسیم‌بندی خطاها بر اساس نوع، backoff نمایی، circuit breaker و چرخش هوشمند IP به کاهش این خسارات به‌طور قابل توجهی بدون کاهش کامل بودن داده‌های جمع‌آوری‌شده کمک می‌کند.

اگر پارسر شما با سایت‌هایی کار می‌کند که به‌طور تهاجمی ربات‌ها را شناسایی می‌کنند — بازارها، شبکه‌های اجتماعی، پلتفرم‌های تبلیغاتی — باید چند نوع پروکسی را بسته به وظیفه ترکیب کنید. برای عملیات پایه، پروکسی‌های دیتا سنتر مناسب هستند، و در جایی که نیاز به حداکثر مقاومت در برابر مسدودیت‌ها وجود دارد، پروکسی‌های مسکونی با آدرس‌های IP واقعی کاربران عادی. این رویکرد ترکیبی به همراه منطق retry هوشمند کاهش قابل توجهی در مصرف ترافیک با همان حجم داده‌های جمع‌آوری‌شده ارائه می‌دهد.