خانه/مقالات/اسکریپینگ با hRequests: دو راهکار ری‌ترای
برنامه نویسی
استخراج داده
برگشت به مقاله‌ها

اسکریپینگ با hRequests: دو راهکار ری‌ترای

اسکریپینگ با hRequests: دو راهکار ری‌ترای
این مقاله دو روش عملی برای ری‌ترای درخواست‌ها در وب اسکریپینگ با hRequests را آموزش می‌دهد: استفاده از کتابخانهٔ آمادهٔ retry و پیاده‌سازی یک wrapper اختصاصی با بک‌آف و تشخیص صفحات بن. همراه با مثال‌های پایتون، توضیحات خط‌به‌خط و بهترین‌شیوه‌های مربوط به تایم‌اوت، جیتِر، لاگ و ایمن‌سازی درخواست‌ها ارائه شده است.
آسان اسکریپ آسان اسکریپ
1405-05-06

مقدمه

در این مقاله به‌صورت عملی دربارهٔ پایدارسازی درخواست‌های HTTP هنگام وب اسکریپینگ با hRequests صحبت می‌کنیم. هدف این راهنما این است که در پایان، دو روش مرسوم برای ری‌ترای درخواست‌ها را بدانید: استفاده از کتابخانهٔ آمادهٔ retry و نوشتن یک wrapper اختصاصی. علاوه بر کد نمونه، به نکات عملکردی، امنیتی و بهترین‌شیوه‌ها هم می‌پردازیم تا سیستم اسکریپینگتان پایدارتر و کمتر مستعد خطا شود.

روش اول: استفاده از کتابخانهٔ retry

ایده کلی: به‌جای نوشتن منطق ری‌ترای دستی، از یک کتابخانهٔ تست‌شده مثل retry استفاده می‌کنیم تا تعداد تلاش‌ها، تأخیر اولیه و ضریب بک‌آف را به‌راحتی مدیریت کنیم.

import hrequests
from retry.api import retry_call

failed_statuses = [429, 500, 502, 503, 504]

def make_request(url):
    # درخواست با timeout برای جلوگیری از بلاک شدن نامحدود
    response = hrequests.get(url, timeout=10)
    # اگر سرور پاسخ خطا داد، استثنا پرتاب می‌کنیم تا retry فعال شود
    if response.status_code in failed_statuses:
        print("bad status code - retrying...", response.status_code)
        raise Exception(response.status_code)
    return response

# retry_call می‌تواند پارامترهای backoff، delay و tries را بگیرد
response = retry_call(make_request, fargs=("http://quotes.toscrape.com/",), tries=5, delay=1, backoff=2, exceptions=Exception)
print(response.status_code)
print(response.text[:200])

توضیح پارامترها و جریان:

  • make_request(url): ورودی یک URL است؛ خروجی یک شیٔ پاسخ hrequests یا پرتاب استثنا در صورت وضعیت نامطلوب.
  • failed_statuses: لیستی از کدهای HTTP که باید باعث ری‌ترای شوند (مثلاً 429 مربوط به rate limit).
  • retry_call(..., tries, delay, backoff, exceptions): این تابع تلاش‌ها را مدیریت می‌کند — tries حداکثر دفعات تلاش، delay تاخیر اولیه، و backoff عامل افزایش فاصله بین تلاش‌ها.

نکات عملی:

  • اگر می‌خواهید به هدر Retry-After سرور احترام بگذارید، داخل make_request آن را بخوانید و در صورت وجود از مقدار آن برای تأخیر استفاده کنید (به‌جای یا همراه با مکانیزم backoff).
  • استثناهای خاص‌تری به‌جای Exception تعریف کنید تا فقط خطاهای قابل بازگشت باعث ری‌ترای شوند (مثل اتصال یا 5xx). این کار از ری‌ترای روی خطاهای منطقی جلوگیری می‌کند.

روش دوم: نوشتن wrapper اختصاصی ری‌ترای

ایده کلی: وقتی به کنترل دقیق‌تری نیاز دارید — مثلاً مدیریت انواع مختلف خطاها، اعتبارسنجی محتوای HTML یا رفتار خاص برای 404 — یک تابع wrapper سفارشی بنویسید.

import hrequests
import time
import random

# یک نمونه تابع تشخیص بن (ban) بر پایهٔ تگ title
def is_ban(response):
    return response is not None and 'Robot or human?' in response.text

def request_retry(url, num_retries=3, success_list=(200, 404), timeout=10, backoff_factor=1.5, session=None, check_ban=None):
    """
    ورودی‌ها:
      - url: آدرس برای درخواست GET
      - num_retries: حداکثر تلاش‌ها
      - success_list: کدهای HTTP که یعنی موفقیت (می‌تواند شامل 404 باشد)
      - timeout: timeout برای هر درخواست
      - backoff_factor: عامل افزایشی برای تاخیر بین تلاش‌ها
      - session: اگر از session پشتیبانی می‌شود می‌توان آن را پاس داد تا اتصال‌ها reuse شوند
      - check_ban: تابعی که در صورت تشخیص صفحه بن، True برمی‌گرداند
    خروجی:
      - شیٔ response در صورت موفقیت، یا None اگر همه تلاش‌ها ناموفق باشند
    """
    session = session or hrequests
    base_delay = 0.5
    for attempt in range(1, num_retries + 1):
        try:
            response = session.get(url, timeout=timeout)
            # اگر کد وضعیت یکی از کدهای موفقیت است، بررسی بیشتر انجام می‌دهیم
            if response.status_code in success_list:
                if check_ban and check_ban(response):
                    # در صورت تشخیص بن، می‌خواهیم ری‌ترای کنیم
                    print("ban detected, will retry...", attempt)
                    raise Exception("ban_detected")
                return response
        except hrequests.exceptions.ClientException:
            # خطاهای اتصال را اینجا هندل می‌کنیم و به تلاش بعدی می‌رویم
            print("connection error, attempt", attempt)
        # محاسبهٔ بک‌آف همراه با jitter برای جلوگیری از همگام‌سازی
        sleep = base_delay * (backoff_factor ** (attempt - 1)) + random.uniform(0, 0.5)
        time.sleep(sleep)
    return None

# مثال استفاده
response = request_retry('http://quotes.toscrape.com/', num_retries=5, check_ban=is_ban)
if response is not None and response.status_code == 200:
    print('OK', len(response.text))
else:
    print('failed after retries')

توضیح خط‌به‌خط (خلاصه):

  • ابتدا پارامترها و نقش آن‌ها را تعریف می‌کنیم؛ تابع یک مقدار response بازمی‌گرداند یا None در صورت شکست.
  • درون حلقهٔ تلاش‌ها، درخواست ارسال می‌شود و اگر کد وضعیت در success_list باشد، آنگاه اگر تابع check_ban فعال است، پاسخ برای الگوهای بن بررسی می‌شود.
  • اگر اتصال قطع شود یا بن تشخیص داده شود، با پرتاب یا گرفتن استثنا وارد بخش تأخیر و ری‌ترای می‌شویم.
  • برای کاهش اثر ترافیک همزمان از بک‌آف نمایی همراه با jitter استفاده شده است.

اعتبارسنجی محتوا و تشخیص صفحات بن

کد وضعیت همیشه کافی نیست؛ گاهی صفحهٔ HTML حاوی پیام بن یا کپچا است در حالی که وضعیت HTTP 200 بازمی‌گردد. برای کاهش خطاها:

  • الگوهای سادهٔ HTML (مثلاً عنوان صفحه یا نشانه‌های مشخص) را چک کنید.
  • در صورت نیاز از یک parser مثل BeautifulSoup برای تحلیل دقیق‌تر DOM استفاده کنید و بررسی کنید آیا المنت‌های مورد انتظار وجود دارند یا نه.
  • هنگام تشخیص بن، رفتار متفاوت (تعویض پراکسی، افزایش فاصلهٔ بین درخواست‌ها یا توقف موقت) را در نظر بگیرید.

بهترین‌روش‌ها و نکات عملی

  • همیشه از timeout برای هر درخواست استفاده کنید تا رشته‌ها/پروسس‌ها معطل نشوند.
  • از backoff نمایی به‌جای ری‌تای‌های سریع متوالی استفاده کنید؛ اضافه کردن jitter کمک می‌کند تا هم‌زمانی زیادی ایجاد نشود.
  • حداکثر تعداد تلاش‌ها را محدود کنید و از حلقهٔ بی‌نهایت خودداری کنید مگر در شرایط خاص و کنترل‌شده.
  • برای درخواست‌های نوشتنی (POST/PUT) احتیاط کنید — این‌ها باید idempotent باشند یا از مکانیزم‌هایی برای جلوگیری از تکرار اثر استفاده شود.
  • در محیط‌هایی با پراکسی یا سرویس مدیریت پراکسی، هنگام خطا پراکسی را rotate کنید تا از بن شدن سریع جلوگیری شود.
  • از لاگ‌گذاری ساختاریافته برای هر تلاش استفاده کنید (کد وضعیت، زمان، دلیل ری‌ترای) تا بعداً تحلیل خطا ساده‌تر شود.
  • در سیستم‌های توزیع‌شده یا با concurrency بالا، از مکانیزم‌هایی مثل circuit breaker یا صف‌بندی وظایف استفاده کنید تا فشار روی سرور هدف کنترل شود.

خطاها، امنیت و رعایت قوانین

هنگام ری‌ترای مراقب باشید که قوانین سایت (robots.txt) و سیاست‌های سرویس‌دهنده‌ها نقض نشوند. تلاش‌های مکرر برای دورزدن محدودیت می‌تواند منجر به بلاک دائمی یا پیامدهای قانونی شود. همچنین، اطلاعات حساس را در لاگ‌ها ثبت نکنید و هنگام استفاده از پراکسی‌ها، از منابع قابل اطمینان استفاده کنید.

جمع‌بندی

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

مطالب مرتبط

مقاله‌های مرتبط