

در این مقاله یاد میگیرید چگونه با استفاده از OkHttp و Apache HttpClient درخواستهای HTTP را بهصورت همزمان (concurrent) ارسال کنید تا سرعت فرآیند وب اسکریپینگ افزایش یابد. مخاطب این راهنما توسعهدهندهای است که با مفاهیم پایه آشناست و میخواهد مثالهای عملی، توضیحات خطبهخط و نکات مربوط به پایداری، امنیت و کارایی را ببیند. در پایان؛ نحوهٔ ساخت Thread pool، استفاده از java.util.concurrent، مدیریت پاسخها، خطاها، retry و یک نمونهٔ ساده برای اتصال به یک پراکسی (مثلاً سرویسهای aggregator مانند ScrapeOps) را بلد خواهید بود.
در وب اسکریپینگ، تاخیر شبکه و زمانی که صرف پردازش صفحه میشود غالباً عامل محدودکنندهٔ سرعت است. با باز کردن چند اتصال همزمان میتوانید از زمانهای انتظار (I/O wait) استفاده بهتری ببرید و نرخ برداشت دادهها را افزایش دهید. با این حال همزمانسازی مسئولیتهای تازهای مثل مدیریت نرخ درخواست، اشتراکگذاری منابع و جلوگیری از بلاک شدن (rate limiting / blocking) به همراه دارد.
ایدهٔ کلی این است که یک ExecutorService (معمولاً یک fixed thread pool) بسازید و هر درخواست HTTP را داخل یک Callable یا Runnable قرار دهید. سپس با فراخوانی invokeAll یا ارسال تکبهتک tasks به executor، آنها را اجرا کنید. استفاده از fixed thread pool کمک میکند تعداد همزمانی کنترل شود و از باز کردن بیرویهٔ اتصالها جلوگیری شود.
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();
}
}
توضیح (ورودی، خروجی، نقش توابع):
ورودیها: آرایهای از URLها (requestUris) و یک OkHttpClient.
خروجی: لیستی از عناوین صفحات (outputData).
نقش: هر Callable یک درخواست میسازد، آن را اجرا میکند، بدنهٔ HTML را میخواند و با Jsoup عنوان صفحه را استخراج و به لیست نهایی اضافه میکند.
نکات خطبهخط (چکیده):
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 کنترل میشود تا تعداد اتصالات همزمان محدود باشد.
// مثال سادهٔ 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 را در کد سختنویسی نکنید. از متغیرهای محیطی یا سرویسهای مدیریت اسرار استفاده کنید و محدودیتهای دسترسی را اعمال کنید.
هیچ مقدار واحدی برای همهٔ موارد وجود ندارد. بهطور کلی:
اگر بیشتر I/O-bound هستید (انتظار شبکه طولانی): تعداد تردها را بیشتر از تعداد هستههای CPU انتخاب کنید (مثلاً 2–10x هستهها بسته به latency).
اگر CPU-bound پردازش HTML سنگین است، تعداد تردها را نزدیک به تعداد هستهها نگه دارید.
همیشه با اعداد کوچک شروع کنید، مانیتور کنید و کمکم افزایش دهید تا به نقطهٔ بهینه برسید.
با ترکیب OkHttp یا Apache HttpClient و java.util.concurrent میتوانید یک اسکریپر سریع و قابل کنترل بسازید. اصول کلیدی عبارتاند از: استفاده مجدد از کلاینت، تعیین timeoutها، مدیریت خطا و retry، همگامسازی دسترسی به دادهٔ خروجی و انتخاب مناسب اندازهٔ thread pool. برای مقیاسپذیری بیشتر، میتوانید از پراکسیهای aggregator یا سرویسهای مدیریت پراکسی استفاده کنید اما کلید موفقیت، اندازهگیری و تنظیم پارامترها بر اساس مانیتورینگ واقعی است.


