

در این مقاله فنی یاد میگیریم چگونه درخواستهای HTTP را بهصورت همزمان (concurrent) ارسال کنیم تا سرعت وب اسکریپینگ افزایش یابد. مثالها روی اکوسیستم Node.js متمرکز هستند (با استفاده از Bottleneck و Promise.all) و برای خوانندگان پایتون معادل عملیاتی با aiohttp و asyncio آورده شده است. در پایان این مطلب با روشهای محدودسازی همزمانی، نکات خطا، retry و استفاده از پروکسی آشنا میشوید.
وقتی صفحات زیادی را اسکریپ میکنیم، ارسال همزمان چندین درخواست بهجای ارسال پشتسرهم میتواند زمان کلی کار را چندین برابر کاهش دهد. اما افزایش همزمانی بدون کنترل میتواند منجر به:
برای همین باید همزمانی را با صفها، محدودکنندهها (rate limiters) یا سمانتورها کنترل کنیم. در 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);
})();توضیح ورودیها/خروجیها و نقش توابع:
خطبهخط مهم:
اگر شما توسعهدهنده پایتون هستید، معادل عملیاتی این الگو استفاده از 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())نکات مهم در نسخهٔ پایتون:
اگر از سرویس پروکسی استفاده میکنید، معمولاً باید 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) استفاده کنید.نکات عملی دربارهٔ پروکسی:
برای اسکریپ پایدار باید به این موارد توجه کنید:
مثال سادهٔ 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');
}همیشه قوانین سایت هدف را بررسی کنید، فایل robots.txt و شرایط استفاده را در نظر بگیرید. ارسال حجم زیاد درخواست بدون رعایت میتواند منجر به IP ban شود و حتی مسائل قانونی ایجاد کند.
اسکریپ کردن همزمان، اگر با محدودیت مناسب و مدیریت خطا پیادهسازی شود، بهطور چشمگیری سرعت جمعآوری داده را افزایش میدهد. در Node.js ترکیب Promise.all با Bottleneck یک الگوی ساده و عملی است؛ برای پایتون معادل آن استفاده از aiohttp و asyncio.Semaphore است. نکات مهم همیشه شامل تنظیم timeout، retry با backoff، لاگینگ و رعایت قوانین سایت هدف است.


