مقدمه
در این مقاله فنی یاد میگیریم چگونه درخواستهای HTTP را بهصورت همزمان (concurrent) ارسال کنیم تا سرعت وب اسکریپینگ افزایش یابد. مثالها روی اکوسیستم Node.js متمرکز هستند (با استفاده از Bottleneck و Promise.all) و برای خوانندگان پایتون معادل عملیاتی با aiohttp و asyncio آورده شده است. در پایان این مطلب با روشهای محدودسازی همزمانی، نکات خطا، retry و استفاده از پروکسی آشنا میشوید.
ایدهٔ کلی و چرا همزمانی مهم است
وقتی صفحات زیادی را اسکریپ میکنیم، ارسال همزمان چندین درخواست بهجای ارسال پشتسرهم میتواند زمان کلی کار را چندین برابر کاهش دهد. اما افزایش همزمانی بدون کنترل میتواند منجر به:
- بار بیشازحد روی سرور هدف (و در نتیجه بلاک شدن)
- مصرف بیشازحد منابع شبکه و حافظه در کلاینت
- خطاهای همزمانی که مدیریت نشدهاند
برای همین باید همزمانی را با صفها، محدودکنندهها (rate limiters) یا سمانتورها کنترل کنیم. در Node.js رایجترین ترکیب برای این کار Promise.all برای اجرا و Bottleneck برای محدودسازی است.
Node.js — الگوی ساده با Promise.all و Bottleneck
در این الگو لیستی از URLها را با Promise.all اجرا میکنیم، اما هر فراخوانی را از طریق Bottleneck زمانبندی میکنیم تا همزمانی واقعی بیش از حد نشود.
import fetch from 'node-fetch';
import cheerio from 'cheerio';
import Bottleneck from 'bottleneck';
const NUM_THREADS = 5;
const list_of_urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
'http://quotes.toscrape.com/page/3/'
];
const output = [];
const limiter = new Bottleneck({ maxConcurrent: NUM_THREADS });
async function scrapePage(url) {
try {
const res = await fetch(url, { timeout: 10000 });
const html = await res.text();
if (res.status === 200) {
const $ = cheerio.load(html);
const title = $('h1').text().trim();
output.push({ url, title });
} else {
console.warn('Non-200', res.status, url);
}
} catch (err) {
console.error('Error', url, err.message);
}
}
(async () => {
await Promise.all(
list_of_urls.map(url => limiter.schedule(() => scrapePage(url)))
);
console.log(output);
})();توضیح ورودیها/خروجیها و نقش توابع:
- list_of_urls: آرایهٔ URLهایی که میخواهیم اسکریپ کنیم (ورودی).
- scrapePage(url): تابعی که یک URL میگیرد، درخواست HTTP ارسال میکند، HTML را parse میکند و دادهٔ مورد نظر را به output اضافه میکند (خروجی جزئی).
- Bottleneck({ maxConcurrent }): محدودکنندهای که تضمین میکند حداکثر NUM_THREADS همزمان اجرا شوند.
- Promise.all: منتظر میماند همهٔ پرامیسها (که از طریق limiter.schedule زمانبندی شدهاند) تکمیل شوند.
خطبهخط مهم:
- ایمپورت ماژولها و تعریف NUM_THREADS.
- ساخت limiter با گزینهٔ maxConcurrent برای کنترل همزمانی.
- درون scrapePage، ارسال درخواست با timeout، بررسی status و استخراج داده با cheerio.
- زمانبندی اجرای هر فراخوانی با limiter.schedule و همگامسازی با Promise.all.
الگو و معایب/مزایا
- مزایا: پیادهسازی ساده، استفاده از ابزارهای استاندارد Node، کنترل واضح روی همزمانی.
- معایب: Promise.all تمام پرامیسها را سریعا در حافظه نگه میدارد — برای تعداد بسیار زیاد URL (میلیونها) نیاز به صفبندی مرحلهای دارید.
معادل پایتون با asyncio و aiohttp
اگر شما توسعهدهنده پایتون هستید، معادل عملیاتی این الگو استفاده از asyncio.Semaphore یا کتابخانههایی مثل aiolimiter و aiohttp است. مثال پایین نشان میدهد چگونه همان محدودیت همزمانی را در پایتون پیاده کنید:
import asyncio
import aiohttp
from bs4 import BeautifulSoup
NUM_THREADS = 5
list_of_urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
'http://quotes.toscrape.com/page/3/'
]
async def fetch(session, url):
try:
async with session.get(url, timeout=10) as resp:
if resp.status == 200:
text = await resp.text()
soup = BeautifulSoup(text, 'html.parser')
title = soup.find('h1').get_text(strip=True) if soup.find('h1') else ''
return {'url': url, 'title': title}
return {'url': url, 'status': resp.status}
except Exception as e:
return {'url': url, 'error': str(e)}
async def worker(sem, session, url):
async with sem:
return await fetch(session, url)
async def main():
sem = asyncio.Semaphore(NUM_THREADS)
async with aiohttp.ClientSession(headers={'User-Agent': 'my-scraper/1.0'}) as session:
tasks = [asyncio.create_task(worker(sem, session, url)) for url in list_of_urls]
results = await asyncio.gather(*tasks)
print(results)
if __name__ == '__main__':
asyncio.run(main())نکات مهم در نسخهٔ پایتون:
- از Semaphore برای محدود کردن تعداد همزمان درخواستها استفاده شده است.
- برای مدیریت خطا از ساختار try/except و بازگشت شیٔ خطا برای هر URL استفاده میکنیم تا یک شکست همهٔ کارها را متوقف نکند.
- تنظیم هدر User-Agent و timeout برای هر درخواست ضروری است.
استفاده از پروکسی و مثال عملی (ScrapeOps-style)
اگر از سرویس پروکسی استفاده میکنید، معمولاً باید URL را به URL پروکسی تبدیل کرده و سپس آن را اسکریپ کنید؛ همینجا هم میتوانید Bottleneck را برای کنترل همزمانی نگه دارید. الگوی ساخت URL پروکسی بهصورت زیر است:
function getProxyUrl(targetUrl, apiKey) {
const payload = { api_key: apiKey, url: targetUrl };
const qs = new URLSearchParams(payload).toString();
return `https://proxy.example.com/v1/?${qs}`;
}
// سپس در scrapePage به جای targetUrl از getProxyUrl(targetUrl, KEY) استفاده کنید.نکات عملی دربارهٔ پروکسی:
- تعداد threads مجاز را از پلن پروکسی خود بگیرید و NUM_THREADS را مطابق آن تنظیم کنید.
- پروکسیها ممکن است هزینه یا محدودیت تراکنش داشته باشند؛ همزمانی بالا لزوماً به معنی بهتر بودن نیست.
مدیریت خطا، retry و backoff
برای اسکریپ پایدار باید به این موارد توجه کنید:
- برای خطاهای موقتی مثل 429 یا 5xx از یک مکانیزم retry با backoff افزایشی استفاده کنید (مثلاً exponential backoff).
- برای هر درخواست یک timeout مشخص کنید تا ارتباطهای گیر کرده منابع را قفل نکنند.
- لاگینگ و متریک: تعداد درخواستها، نرخ خطا و متوسط زمان پاسخ را ثبت کنید تا بتوانید همزمانی را بهینه کنید.
مثال سادهٔ retry در Node:
async function fetchWithRetry(url, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
const res = await fetch(url, { timeout: 10000 });
if (res.ok) return res;
if (res.status >= 500) throw new Error('Server error');
} catch (err) {
const wait = Math.pow(2, i) * 500;
await new Promise(r => setTimeout(r, wait));
}
}
throw new Error('Max retries reached');
}تنظیم همزمانی: توصیههای عملی
- شروع با مقدار کوچک مثل 5 و اندازهگیری: افزایش تدریجی تا زمانی که خطا یا نرخ کندی مشاهده شود.
- برای سایتهای حساس به بار، از نرخمحدود (rate limit) مبتنی بر درخواست در ثانیه استفاده کنید نه فقط maxConcurrent.
- برای کارهای بزرگ از صفکاری (job queue) مثل Bull یا Celery (برای پایتون) استفاده کنید تا حافظهٔ اپلیکیشن کنترل شود.
نکات امنیتی و اخلاقی
همیشه قوانین سایت هدف را بررسی کنید، فایل robots.txt و شرایط استفاده را در نظر بگیرید. ارسال حجم زیاد درخواست بدون رعایت میتواند منجر به IP ban شود و حتی مسائل قانونی ایجاد کند.
جمعبندی
اسکریپ کردن همزمان، اگر با محدودیت مناسب و مدیریت خطا پیادهسازی شود، بهطور چشمگیری سرعت جمعآوری داده را افزایش میدهد. در Node.js ترکیب Promise.all با Bottleneck یک الگوی ساده و عملی است؛ برای پایتون معادل آن استفاده از aiohttp و asyncio.Semaphore است. نکات مهم همیشه شامل تنظیم timeout، retry با backoff، لاگینگ و رعایت قوانین سایت هدف است.





