خانه/مقالات/اسکریپ با Java Apache HttpClient: راهکارهای Retry قابل‌اطمینان
سلنیوم
پروکسی و چرخش IP
برگشت به صفحه مقاله ها
اسکریپ با Java Apache HttpClient: راهکارهای Retry قابل‌اطمینان

اسکریپ با Java Apache HttpClient: راهکارهای Retry قابل‌اطمینان

در این راهنما یاد می‌گیرید چگونه با Apache HttpClient در Java درخواست‌های ناموفق را با دو رویکرد: استفاده از retry4j و نوشتن wrapper سفارشی، به‌صورت قابل‌اعتماد ری‌تری کنید؛ همراه با نمونه‌کد، روش تشخیص صفحه‌های بلاک (با Jsoup) و نکات عملی در مورد performance، امنیت و بهترین‌روش‌ها.
امیر حسین حسینیان
امیر حسین حسینیان
1404-09-13

مقدمه: در این مقاله به توسعه‌دهندگان Java نشان می‌دهیم چگونه با استفاده از Apache HttpClient درخواست‌های HTTP را هنگام خطا دوباره ارسال (Retry) کنید تا سیستم اسکریپ شما پایدارتر شود. دو رویکرد رایج را بررسی می‌کنیم: استفاده از کتابخانهٔ retry4j و نوشتن یک wrapper سفارشی برای منطق ری‌تری. در پایانِ هر بخش، نکات عملی، محدودیت‌ها و نمونهٔ کد قابل‌اجرا ارائه می‌شود.

Retry با استفاده از retry4j

ایدهٔ کلی: از یک کتابخانهٔ آماده برای مدیریت تعداد تلاش‌ها، تاخیر بین تلاش‌ها و backoff استفاده می‌کنیم تا به جای مدیریت دستی خطاها، رفتار Retry به‌صورت قابل‌پیکربندی و قابل‌نظارت اجرا شود.

مراحل کلی:

  • ایجاد و راه‌اندازی CloseableHttpAsyncClient.
  • بسته‌بندی منطق ارسال درخواست داخل یک Callable.
  • تعریف RetryConfig (حداکثر تلاش، تاخیر، backoff، شرایط تکرار).
  • اجرای CallExecutor روی Callable و مدیریت listeners برای لاگ و cleanup.

نمونهٔ سادهٔ بازنویسی‌شده:

import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.time.temporal.ChronoUnit;
import com.evanlennick.retry4j.CallExecutor;
import com.evanlennick.retry4j.CallExecutorBuilder;
import com.evanlennick.retry4j.config.RetryConfig;
import com.evanlennick.retry4j.config.RetryConfigBuilder;

public class RetryExample {
    public static List badStatusCodes = Arrays.asList(429,500,502,503,504);

    public static void main(String[] args) throws Exception {
        CloseableHttpAsyncClient client = HttpAsyncClients.createDefault();
        client.start();

        Callable<Void> makeRequest = () -> {
            var request = SimpleRequestBuilder.get("https://quotes.toscrape.com").build();
            SimpleHttpResponse response = client.execute(request, null).get();
            int status = response.getCode();
            if (badStatusCodes.contains(status)) {
                throw new Exception("Bad status code: " + status);
            }
            System.out.println("Response body: " + response.getBodyText());
            return null;
        };

        RetryConfig config = new RetryConfigBuilder()
            .retryOnAnyException()
            .withMaxNumberOfTries(5)
            .withDelayBetweenTries(10, ChronoUnit.SECONDS)
            .withExponentialBackoff()
            .build();

        CallExecutor callExecutor = new CallExecutorBuilder()
            .config(config)
            .build();

        callExecutor.execute(makeRequest);
        client.close();
    }
}

توضیح کد:

  • ورودی: آدرس هدف داخل SimpleRequestBuilder.get و تنظیمات retry در RetryConfig.
  • خروجی: در صورت موفقیت، محتوای پاسخ چاپ می‌شود؛ در غیر این صورت exception باعث تکرار طبق پیکربندی می‌شود.
  • در RetryConfig از retryOnAnyException() استفاده شده که هر Exception را برای تکرار پذیرا می‌کند؛ در عمل ممکن است بخواهید محدودتر شوید (مثلاً فقط IOException یا موارد خاص).
  • از withExponentialBackoff() همراه با محدود کردن حداکثر تلاش استفاده کنید تا از Flooding سرویس جلوگیری شود.

نوشتن Wrapper سفارشی برای Retry

ایدهٔ کلی: وقتی کنترل کامل می‌خواهید (مثلاً شرایط ناموفق سفارشی، بررسی محتوای HTML، یا رفتارهای متفاوت بر اساس نوع خطا)، نوشتن منطق Retry دستی مناسب‌تر است.

مزایا و معایب:

  • مزایا: انعطاف‌پذیری کامل، امکان اعتبارسنجی پاسخ (مثلاً وجود صفحهٔ بلاک)، و تصمیم‌گیری‌های ویژه.
  • معایب: پیاده‌سازی و نگهداری بیشتر، خطر خطا در منطق backoff یا شرایط خروج.

نمونهٔ ساده:

import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.core5.http.ConnectionClosedException;
import org.apache.hc.core5.http.ConnectionRequestTimeoutException;

public class CustomRetry {
    final public static int NUM_RETRIES = 3;

    public static void main(String[] args) throws Exception {
        CloseableHttpAsyncClient client = HttpAsyncClients.createDefault();
        client.start();
        SimpleHttpResponse response = null;

        for (int i=0; i<NUM_RETRIES; i++) {
            try {
                var request = SimpleRequestBuilder.get("https://quotes.toscrape.com").build();
                response = client.execute(request, null).get();
                int status = response.getCode();
                if (status == 200 || status == 404) {
                    break; // موفق یا صفحه پیدا نشد؛ نیازی به تکرار نیست
                }
            } catch (Exception e) {
                boolean connectionError = e instanceof ConnectionClosedException
                    || e instanceof ConnectionRequestTimeoutException;
                if (connectionError) {
                    // لاگ کنید و اجازه دهید حلقه تکرار شود
                } else {
                    // در موارد دیگر ممکن است بخواهید exception را بازپرتاب کنید
                    throw e;
                }
            } finally {
                System.out.println("Total tries: " + (i+1));
            }
        }

        if (response != null && response.getCode() == 200) {
            System.out.println("Response body: " + response.getBodyText());
        } else {
            System.out.println("No valid response after maximum number of tries");
        }
        client.close();
    }
}

نکات اجرایی:

  • در حلقهٔ retry صریحاً زمان‌بندی تاخیر بین تلاش‌ها (مثلاً Thread.sleep یا timers غیرهمزمان) و اعمال Backoff/Jitter را مدیریت کنید.
  • برای خطاهای غیرقابل‌انتظار، بهتر است رفتار تهاجمی‌تر (مثلاً rethrow) انتخاب شود تا حلقهٔ retry بیهوده ادامه نداشته باشد.
  • همیشه سعی کنید client.close() را در بلوک نهایی یا listener فراخوانی کنید تا منابع آزاد شوند.

اعتبارسنجی محتوای HTML و تشخیص صفحهٔ بلاک

فقط بررسی کد وضعیت (status code) کافی نیست: گاهی سرویس پاسخ 200 می‌دهد اما صفحهٔ پاسخ حاوی پیام بلاک یا CAPTCHA است. در اینجا از Jsoup برای بررسی عنوان صفحه یا وجود الگوهای مشخص استفاده می‌کنیم.

// پس از دریافت response
String body = response.getBodyText();
String pageTitle = org.jsoup.Jsoup.parse(body).title();
boolean valid = response.getCode() == 200 && !pageTitle.contains("Robot or human?");
if (!valid) {
    // علامت‌دهی به منطق retry برای تکرار درخواست
}

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

Performance، پایداری و امنیت — بهترین روش‌ها

  • استفاده از Exponential backoff به‌همراه jitter برای جلوگیری از تجمع درخواست‌های بازپخش‌شده روی سرور.
  • حداکثر تعداد تلاش را معقول تعیین کنید و timeouts را کوچک نکنید؛ ترکیب timeout و retry بد می‌تواند منجر به افزایش بار و مصرف منابع شود.
  • برای درخواست‌های غیردارای idempotency (مثل POST) از retry خودداری کنید یا مکانیزم idempotency token تعریف کنید.
  • در محیط‌های همزمان (multi-threaded) مطمئن شوید مدیریت client و منابع thread-safe است و از اتصال‌های مجدد غیرضروری اجتناب کنید.
  • از پروکسی‌های چرخشی و مدیریت نرخ (rate limiting) برای کاهش ریسک بلاک شدن استفاده کنید، اما به مسائل قانونی و فایل robots.txt توجه داشته باشید.
  • لاگ‌گذاری معنادار (شمارش تلاش‌ها، نوع خطا، زمان‌بندی‌ها) برای تحلیل و رفع مشکل ضروری است.
  • در محیط تولید، مانیتورینگ و آلارم روی نرخ خطاها و زمان پاسخ را فعال کنید تا رفتار retry باعث اختلال در سرویس‌های دیگر نشود.

جمع‌بندی

دو رویکرد اصلی برای اضافه کردن retry به اسکریپ‌های Java با Apache HttpClient وجود دارد: استفاده از کتابخانه‌های آماده مانند retry4j برای راه‌اندازی سریع و قابل‌پیکربندی، یا نوشتن wrapper سفارشی برای کنترل دقیق‌تر روی شرایط شکست و اعتبارسنجی محتوا. هر دو روش مزایا و محدودیت‌های خود را دارند؛ انتخاب بین آن‌ها بستگی به نیازهای قابلیت اطمینان، پیچیدگی منطق و میزان کنترلی دارد که می‌خواهید روی رفتار درخواست‌ها داشته باشید. در عمل ترکیب یک کتابخانهٔ Retry با اعتبارسنجی محتوایی (مثل Jsoup) و رعایت best practiceهایی همچون backoff، jitter و مدیریت زمان‌ها بهترین نتیجه را می‌دهد.

مقاله‌های مرتبط
بهینه‌سازی درخواست‌ها و جلوگیری از بلاک‌شدن
1404-10-04
راهنمای سریع وب اسکریپینگ: Retry در Node.js
در این مقاله دو روش متداول برای Retry در وب اسکریپینگ با Node.js بررسی شده: استفاده از کتابخانه <strong>retry</strong> و ساخت wrapper اختصاصی. مثال‌های عملی برای Got، node‑fetch و Axios همراه با نکات backoff، تشخیص صفحه بن و بهترین‌روش‌های امنیتی و عملکردی ارائه شده‌اند.
بهینه‌سازی درخواست‌ها و جلوگیری از بلاک‌شدن
1404-10-02
راهنمای سریع POST در NodeJS برای وب اسکریپینگ
این راهنمای جامع نشان می‌دهد چگونه با کتابخانه‌های مختلف NodeJS (Got، SuperAgent، node-fetch، Axios، request-promise) درخواست‌های POST برای ارسال JSON و فرم بسازید، و نکات عملی‌ مثل هدرها، مدیریت خطا، retry، همزمانی و حفاظت در برابر مسدودسازی را برای وب اسکریپینگ توضیح می‌دهد.
بهینه‌سازی درخواست‌ها و جلوگیری از بلاک‌شدن
1404-10-01
راهنمای سریع کاهش هزینه وب اسکریپینگ با Node.js
این مقاله یک راهنمای عملی برای کاهش هزینه‌های وب اسکریپینگ با Node.js است: انتخاب بین HTTP requests و headless، انتخاب نوع و مدل قیمت‌گذاری پروکسی، کاهش تعداد درخواست و پهنای‌باند، استفاده از سرویس‌های ارزان‌تر و مانیتورینگ هزینه. همراه با مثال‌های Node.js و توضیحات فنی برای پیاده‌سازی عملی.