خانه/مقالات/راهنمای سریع وب اسکریپینگ: Retry در Node.js
برنامه نویسی
وب اسکریپینگ
Axios
برگشت به مقاله‌ها

راهنمای سریع وب اسکریپینگ: Retry در Node.js

راهنمای سریع وب اسکریپینگ: Retry در Node.js
در این مقاله دو روش متداول برای Retry در وب اسکریپینگ با Node.js بررسی شده: استفاده از کتابخانه <strong>retry</strong> و ساخت wrapper اختصاصی. مثال‌های عملی برای Got، node‑fetch و Axios همراه با نکات backoff، تشخیص صفحه بن و بهترین‌روش‌های امنیتی و عملکردی ارائه شده‌اند.
آسان اسکریپ آسان اسکریپ
1404-10-04

مقدمه

در این راهنما می‌خواهیم یاد بگیریم چطور هنگام وب اسکریپینگ با Node.js درخواست‌هایی که شکست می‌خورند را به‌صورت مرتب Retry کنیم تا سیستم پایدارتر و مقاوم‌تری داشته باشیم. بعد از خواندن این مقاله دو رویکرد مرسوم را می‌دانید:

  • استفاده از کتابخانه retry برای تعریف رفتار Retry
  • ساختن یک wrapper اختصاصی با کنترل دقیق‌تر روی خطاها، backoff و اعتبارسنجی پاسخ

مثال‌های عملی برای کتابخانه‌های رایج مانند Got، node-fetch، Axios، SuperAgent و request-promise آورده شده و نکات عملکردی و امنیتی نیز پوشش داده می‌شود.

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

ایده کلی: retry یک ماشین وضعیت برای تلاش مجدد فراهم می‌کند؛ شما گزینه‌ها (تعداد تلاش‌ها، backoff، timeout حداقل/حداکثر، تصادفی‌سازی) را تعریف می‌کنید و سپس هر بار که درخواست شکست خورد، بسته به خطا تصمیم می‌گیرد که دوباره تلاش کند یا نه.

نمونه با got:

import got from 'got'
import retry from 'retry'

const retryOptions = {
  retries: 5,
  factor: 2,
  minTimeout: 1000,
  maxTimeout: 10000,
  randomize: true,
  // لیست کدهای HTTP که معمولاً می‌خواهیم برای آن‌ها Retry کنیم
  statusCode: [429, 500, 502, 503, 504]
}

const operation = retry.operation(retryOptions)
const url = 'http://quotes.toscrape.com/'

operation.attempt(async (currentAttempt) => {
  try {
    const response = await got.get(url)
    console.log(response.body) // خروجی موفق
  } catch (err) {
    // operation.retry تصمیم می‌گیرد آیا باید دوباره تلاش کند یا نه
    if (operation.retry(err)) {
      console.log(`Retry attempt: ${currentAttempt}`)
      return
    }
    console.error(`Maximum retries reached. Error: ${err}`)
  }
})

توضیح (ورودی/خروجی/نقش‌ها):

  • ورودی: retryOptions برای کنترل رفتار Retry و url برای درخواست.
  • خروجی: در صورت موفقیت response.body چاپ می‌شود؛ در غیر این صورت پس از اتمام تلاش‌ها خطا لاگ می‌شود.
  • نقش هر تابع: retry.operation یک عملیات Retry می‌سازد، operation.attempt محتوای تلاش را اجرا می‌کند و operation.retry(err) بررسی می‌کند آیا خطا قابل Retry هست یا نه.

فرمول زمان‌بندی Retry (exponential backoff با امکان randomize):

Math.min(random * minTimeout * Math.pow(factor, attempt), maxTimeout)

نکات عملی:

  • معمولاً 4xx غیر از 429 نباید دوباره امتحان شوند (خطای سمت کاربر)؛ 5xx و 429 را Retry کنید.
  • استفاده از randomize (jitter) برای جلوگیری از thundering herd توصیه می‌شود.

نمونه برای node-fetch (با همان retry)

import fetch from 'node-fetch'
import retry from 'retry'

const options = { retries: 5, factor: 2, minTimeout: 1000, maxTimeout: 10000, randomize: true }
const op = retry.operation(options)
const url = 'http://quotes.toscrape.com/'

op.attempt(async (attempt) => {
  try {
    const res = await fetch(url)
    const text = await res.text()
    console.log(text)
  } catch (err) {
    if (op.retry(err)) {
      console.log('Retrying...', attempt)
      return
    }
    console.error('Retries exhausted', err)
  }
})

همان منطق برای دیگر کتابخانه‌ها (Axios، SuperAgent، request‑promise) نیز قابل اعمال است. در برخی از آن‌ها (مثلاً Axios) می‌توانید نسخه‌های آماده_retry را هم بیابید اما استفاده از یک کتابخانه عمومی مثل retry کنترل بیشتری به شما می‌دهد.

روش دوم — ساختن wrapper اختصاصی برای Retry

ایده کلی: اگر نیاز به کنترول دقیق‌تری (مثل اعتبارسنجی محتوای HTML، دسته‌بندی خطاها، اعمال سیاست‌های متفاوت بر اساس endpoint یا متد) دارید، یک تابع wrapper بسازید که به شکل عمومی قابل استفاده باشد.

مثال عمومی یک wrapper با backoff و jitter:

// retryWrapper یک تابع عمومی است که یک تابع async (مثلاً fetch/axios) را می‌گیرد
async function retryWrapper(fn, { retries = 3, minDelay = 500, factor = 2, maxDelay = 10000 } = {}) {
  for (let i = 0; i < retries; i++) {
    try {
      const result = await fn()
      return result // بازگشت نتیجه موفق
    } catch (err) {
      const isLast = i === retries - 1
      // تصمیم‌گیری برای ادامه یا شکستن بسته به نوع خطا
      const retriable = isRetriableError(err)
      if (!retriable || isLast) throw err
      // محاسبه delay با jitter
      const backoff = Math.min(maxDelay, minDelay * Math.pow(factor, i))
      const jitter = Math.random() * backoff
      await new Promise(r => setTimeout(r, backoff + jitter))
    }
  }
}

function isRetriableError(err) {
  // نمونه ساده: خطاهای شبکه یا 5xx یا 429
  if (!err.response) return true // خطای شبکه
  const status = err.response.status
  return status === 429 || (status >= 500 && status < 600)
}

// نمونه استفاده با axios:
// const data = await retryWrapper(() => axios.get(url), { retries: 4 })

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

  • fn: تابعی که درخواست HTTP را انجام می‌دهد و یک Promise برمی‌گرداند. (ورودی)
  • پارامترهای retries,minDelay,factor,maxDelay: کنترل‌کننده سیاست backoff. (ورودی)
  • خروجی: نتیجه موفق تابع fn یا پرتاب خطا پس از اتمام تلاش‌ها.

مزیت این رویکرد: قابلیت آزمون‌پذیری، تزریق سیاست‌های متفاوت، اعتبارسنجی پاسخ (برای مثال شناسایی صفحه بن) و ترکیب با معیارگذاری و circuit breaker.

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

گاهی سرور پاسخ 200 برمی‌گرداند اما محتوا نشان‌دهندهٔ صفحه بن یا کپچا است. در این حالت باید محتوا را نیز بررسی کنید و در صورت تشخیص بن، درخواست را Retry کنید یا رفتار متفاوتی اتخاذ نمایید.

// مثال ساده: بررسی تایتل بن
const response = await fetch(url)
const text = await response.text()
if (response.status === 200 && text.includes('Robot or human?')) {
  // این صفحه بن است — می‌توانیم Retry کنیم یا proxy/user-agent را تعویض کنیم
}

توجه: بررسی محتوای HTML می‌تواند به‌عنوان یک شرط غیرقابل اعتماد در برخی سایت‌ها باشد (صفحات بن می‌توانند متنوع باشند)، بنابراین بهتر است چند امضا (title، متن خاص، وجود فرم کپچا) را بررسی کنید.

بهترین‌روش‌ها، محدودیت‌ها و نکات امنیتی

  • فقط برای متدهای ایمن یا idempotent (مثل GET) تا حد امکان Retry انجام دهید؛ برای POST با احتیاط عمل کنید.
  • از backoff نمایی همراه با jitter استفاده کنید تا فشار ناگهانی روی سرور نیاید.
  • حداکثر تعداد تلاش‌ها را محدود کنید و متریک برای تعداد Retryها و موفقیت‌ها داشته باشید.
  • کدهای 4xx معمولاً خطاهای کاربر هستند؛ به‌جز 429 به طور پیش‌فرض Retry نکنید.
  • برای احراز هویت و کوکی‌ها مراقب باشید؛ اطلاعات حساس را لاگ نکنید و ارتباطات را با TLS ایمن نگه دارید.
  • اگر از پراکسی استفاده می‌کنید، مراقب تغییر آی‌پی/هدایت باشید و health-check برای پراکسی‌ها داشته باشید.
  • در محیط‌های همزمانی بالا، از محدودکننده نرخ (rate limiter) و circuit breaker استفاده کنید تا سیستم کلی تحت فشار قرار نگیرد.
  • برای تست، سناریوهای شبکه‌ای مثل timeouts، DNS failures و پاسخ‌های غیرمنتظره را شبیه‌سازی کنید.

مقایسهٔ سریع دو رویکرد

  • کتابخانه retry: سریع، استاندارد و مناسب وقتی رفتار Retry برای چندین caller یکسان است.
  • wrapper اختصاصی: مناسب وقتی که نیاز به اعتبارسنجی محتوا، سیاست‌های متغیر، لاگینگ خاص یا integration با سیستم‌های مانیتورینگ دارید.

جمع‌بندی

برای وب اسکریپینگ با Node.js، داشتن مکانیزم Retry یکی از پایه‌ای‌ترین تکنیک‌ها برای افزایش پایداری است. استفاده از کتابخانه‌هایی مثل retry راه‌اندازی را ساده می‌کند، اما اگر نیاز به کنترل دقیق دارید ساختن wrapper اختصاصی با backoff و تشخیص صفحه بن منطقی‌تر است. همیشه سیاست retry را همراه با محدودیت، jitter، طبقه‌بندی خطا و مانیتورینگ پیاده کنید تا هم اخلاقی‌تر و هم کارآمدتر اسکریپ کنید.

مطالب مرتبط

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