خانه/مقالات/همزمان‌سازی درخواست‌ها با OkHttp و Apache HttpClient برای وب اسکریپینگ
پروکسی و چرخش IP
Playwright
برگشت به صفحه مقاله ها
همزمان‌سازی درخواست‌ها با OkHttp و Apache HttpClient برای وب اسکریپینگ

همزمان‌سازی درخواست‌ها با OkHttp و Apache HttpClient برای وب اسکریپینگ

این مقاله روش‌های عملی برای ارسال درخواست‌های همزمان با OkHttp و Apache HttpClient را برای وب اسکریپینگ توضیح می‌دهد؛ شامل نمونه‌های کد جاوا، توضیح خط‌به‌خط، نکات مدیریت خطا، تنظیم Thread pool و مثال ادغام با پراکسی (مانند ScrapeOps). پس از خواندن این راهنما می‌دانید چگونه همزمانی را امن، پایدار و قابل اندازه‌گیری پیاده‌سازی کنید.
امیر حسین حسینیان
امیر حسین حسینیان
1404-09-14

مقدمه

در این مقاله یاد می‌گیرید چگونه با استفاده از OkHttp و Apache HttpClient درخواست‌های HTTP را به‌صورت همزمان (concurrent) ارسال کنید تا سرعت فرآیند وب اسکریپینگ افزایش یابد. مخاطب این راهنما توسعه‌دهنده‌ای است که با مفاهیم پایه آشناست و می‌خواهد مثال‌های عملی، توضیحات خط‌به‌خط و نکات مربوط به پایداری، امنیت و کارایی را ببیند. در پایان؛ نحوهٔ ساخت Thread pool، استفاده از java.util.concurrent، مدیریت پاسخ‌ها، خطاها، retry و یک نمونهٔ ساده برای اتصال به یک پراکسی (مثلاً سرویس‌های aggregator مانند ScrapeOps) را بلد خواهید بود.

چرا همزمان‌سازی (Concurrency) مهم است؟

در وب اسکریپینگ، تاخیر شبکه و زمانی که صرف پردازش صفحه می‌شود غالباً عامل محدودکنندهٔ سرعت است. با باز کردن چند اتصال همزمان می‌توانید از زمان‌های انتظار (I/O wait) استفاده بهتری ببرید و نرخ برداشت داده‌ها را افزایش دهید. با این حال همزمان‌سازی مسئولیت‌های تازه‌ای مثل مدیریت نرخ درخواست، اشتراک‌گذاری منابع و جلوگیری از بلاک شدن (rate limiting / blocking) به همراه دارد.

مبانی: Thread pool و Callable

ایدهٔ کلی این است که یک ExecutorService (معمولاً یک fixed thread pool) بسازید و هر درخواست HTTP را داخل یک Callable یا Runnable قرار دهید. سپس با فراخوانی invokeAll یا ارسال تک‌به‌تک tasks به executor، آنها را اجرا کنید. استفاده از fixed thread pool کمک می‌کند تعداد همزمانی کنترل شود و از باز کردن بی‌رویهٔ اتصال‌ها جلوگیری شود.

نمونه: OkHttp با java.util.concurrent

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.jsoup.Jsoup;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class ConcurrentThreads {
    public static void main(String[] args) throws Exception {
        OkHttpClient client = new OkHttpClient();

        String[] requestUris = new String[] {
            "http://quotes.toscrape.com/page/1/",
            "http://quotes.toscrape.com/page/2/",
            "http://quotes.toscrape.com/page/3/",
            "http://quotes.toscrape.com/page/4/",
            "http://quotes.toscrape.com/page/5/"
        };

        List outputData = new ArrayList<>();
        int numberOfThreads = 5; // تعداد تردها

        ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
        List> tasks = new ArrayList<>();

        for (String requestUri : requestUris) {
            Callable task = () -> {
                Request request = new Request.Builder()
                        .url(requestUri)
                        .build();

                try (Response response = client.newCall(request).execute()) {
                    String html = response.body().string();
                    String title = Jsoup.parse(html).title();
                    synchronized (outputData) {
                        outputData.add(title);
                    }
                }
                return null;
            };
            tasks.add(task);
        }

        executor.invokeAll(tasks);
        outputData.forEach(System.out::println);
        executor.shutdown();
    }
}

توضیح (ورودی، خروجی، نقش توابع):

  1. ورودی‌ها: آرایه‌ای از URLها (requestUris) و یک OkHttpClient.

  2. خروجی: لیستی از عناوین صفحات (outputData).

  3. نقش: هر Callable یک درخواست می‌سازد، آن را اجرا می‌کند، بدنهٔ HTML را می‌خواند و با Jsoup عنوان صفحه را استخراج و به لیست نهایی اضافه می‌کند.

نکات خط‌به‌خط (چکیده):

  • ساخت OkHttpClient: کلاینت قابل‌استفاده در چند ترد است و برای re-use توصیه می‌شود.
  • هر ترد یک درخواست را همزمان اجرا می‌کند. استفاده از try-with-resources برای Response باعث می‌شود منابع به‌درستی آزاد شوند.
  • برای نوشتن امن به outputData از synchronized یا ساختار موازی امن مانند ConcurrentLinkedQueue استفاده کنید.

نمونه: Apache HttpClient (Async) با java.util.concurrent

import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
import org.jsoup.Jsoup;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

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

        String[] requestUris = new String[] {
            "http://quotes.toscrape.com/page/1/",
            "http://quotes.toscrape.com/page/2/",
            "http://quotes.toscrape.com/page/3/",
            "http://quotes.toscrape.com/page/4/",
            "http://quotes.toscrape.com/page/5/"
        };

        List outputData = new ArrayList<>();
        int numberOfThreads = 5;

        ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
        List> tasks = new ArrayList<>();

        for (String requestUri : requestUris) {
            SimpleHttpRequest request = SimpleRequestBuilder.get(requestUri).build();
            Callable task = () -> {
                SimpleHttpResponse response = client.execute(request, null).get();
                String html = response.getBodyText();
                String title = Jsoup.parse(html).title();
                synchronized (outputData) {
                    outputData.add(title);
                }
                return null;
            };
            tasks.add(task);
        }

        executor.invokeAll(tasks);
        outputData.forEach(System.out::println);
        executor.shutdown();
        client.close();
    }
}

توضیحات مهم:

  • CloseableHttpAsyncClient یک کلاینت غیرهمزمان است؛ اما در این الگو از آن به‌صورت blocking با Future.get() استفاده شده تا منطق ساده بماند. در پروژه‌های بزرگ بهتر است از callbackها یا ترکیب کامل async استفاده کنید.

  • باز هم همزمانی با ExecutorService کنترل می‌شود تا تعداد اتصالات همزمان محدود باشد.

افزودن پراکسی و ScrapeOps (نمونهٔ عملی)

// مثال سادهٔ OkHttp با استفاده از یک URL پراکسی که شامل API_KEY است
public class ScrapeOpsProxyConcurrentThreads {
    final public static String SCRAPEOPS_API_KEY = "your_api_key";

    public static void main(String[] args) throws Exception {
        OkHttpClient client = new OkHttpClient();
        String[] requestUris = new String[] { /* ... */ };
        List outputData = new ArrayList<>();
        int numberOfThreads = 5;
        ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
        List> tasks = new ArrayList<>();

        for (String requestUri : requestUris) {
            String proxyUrl = String.format("https://proxy.scrapeops.io/v1?api_key=%s&url=%s", SCRAPEOPS_API_KEY, requestUri);
            Callable task = () -> {
                Request request = new Request.Builder().url(proxyUrl).build();
                try (Response response = client.newCall(request).execute()) {
                    String html = response.body().string();
                    String title = Jsoup.parse(html).title();
                    synchronized (outputData) { outputData.add(title); }
                }
                return null;
            };
            tasks.add(task);
        }

        executor.invokeAll(tasks);
        outputData.forEach(System.out::println);
        executor.shutdown();
    }
}

نکتهٔ امنیتی: مقدار SCRAPEOPS_API_KEY را در کد سخت‌نویسی نکنید. از متغیرهای محیطی یا سرویس‌های مدیریت اسرار استفاده کنید و محدودیت‌های دسترسی را اعمال کنید.

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

  • استفاده مجدد از client: هر دو کلاینت OkHttp و Apache برای reuse طراحی شده‌اند؛ از ایجاد کلاینت جدید برای هر درخواست اجتناب کنید.
  • Timeouts: حتما timeout مناسب برای connect/read و write تعیین کنید تا تردها برای همیشه منتظر نمانند.
  • مدیریت خطا و Retry: خطاهای شبکه، پاسخ‌های 5xx و خطاهای مربوط به پراکسی باید با سیاست retry کنترل شوند. از backoff نمایی و محدودیت تعداد تلاش استفاده کنید.
  • سازگاری با سرور مقصد: افزایش همزمانی باعث فشار روی سرور می‌شود؛ قوانین روبات‌ها و نرخ مجاز را رعایت کنید تا IP بلاک نشود.
  • همگام‌سازی داده‌های مشترک: برای نوشتن نتایج از ساختارهای thread-safe مانند ConcurrentLinkedQueue یا سینک استفاده کنید.
  • پایش و مانیتورینگ: نرخ موفقیت، میانگین زمان پاسخ و خطاها را مانیتور کنید تا پارامترهای همزمانی را بهینه کنید.

اندازهٔ مناسب Thread Pool

هیچ مقدار واحدی برای همهٔ موارد وجود ندارد. به‌طور کلی:

  1. اگر بیشتر I/O-bound هستید (انتظار شبکه طولانی): تعداد تردها را بیشتر از تعداد هسته‌های CPU انتخاب کنید (مثلاً 2–10x هسته‌ها بسته به latency).

  2. اگر CPU-bound پردازش HTML سنگین است، تعداد تردها را نزدیک به تعداد هسته‌ها نگه دارید.

  3. همیشه با اعداد کوچک شروع کنید، مانیتور کنید و کم‌کم افزایش دهید تا به نقطهٔ بهینه برسید.

جمع‌بندی

با ترکیب OkHttp یا Apache HttpClient و java.util.concurrent می‌توانید یک اسکریپر سریع و قابل کنترل بسازید. اصول کلیدی عبارت‌اند از: استفاده مجدد از کلاینت، تعیین timeoutها، مدیریت خطا و retry، همگام‌سازی دسترسی به دادهٔ خروجی و انتخاب مناسب اندازهٔ thread pool. برای مقیاس‌پذیری بیشتر، می‌توانید از پراکسی‌های aggregator یا سرویس‌های مدیریت پراکسی استفاده کنید اما کلید موفقیت، اندازه‌گیری و تنظیم پارامترها بر اساس مانیتورینگ واقعی است.

مقاله‌های مرتبط
بهینه‌سازی درخواست‌ها و جلوگیری از بلاک‌شدن
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 و توضیحات فنی برای پیاده‌سازی عملی.