لماذا Cheerio + axios هو مكدّس الكشط الخفيف الأول
أنت مطوّر Node.js تريد استخراج بيانات من مواقع تجارية أو نتائج بحث. الحل السريع؟ فتح المتصفح اللامرئي (Puppeteer/Playwright). لكن المتصفح اللامرئي يستهلك ذاكرة هائلة، بطيء، ومعقّد للصيانة. الحقيقة: أغلب المواقع التي تحتاج كشطها تُرسل HTML مكتمل من الخادم — لا تحتاج JavaScript من جانب العميل لعرض المحتوى.
هنا يأتي دور Cheerio + axios: تحليل HTML على الخادم بسرعة فائقة، بدون DOM حقيقي، بدون متصفح. المزيج مثالي للمواقع ذات التصيير من جانب الخادم (SSR) — معظم متاجر التجارة الإلكترونية، المدونات، أدلة الأعمال، وصفحات نتائج البحث.
لكن المشكلة التي تواجهك عاجلاً أم آجلاً: حجب الـ IP. هنا يصبح تدوير البروكسي ضرورة وليس رفاهية. في هذا الدليل سنبني من الصفر خط كشط كامل مع بروكسي سكني متدوّر كـ axios interceptor قابل لإعادة الاستخدام.
إعداد Cheerio مع axios: كشط HTML من جانب الخادم
Cheerio هو تنفيذ jQuery خفيف لـ Node.js يحلّل HTML ويعطيك واجهة برمجية مألوفة لاستخراج البيانات. مع axios للطلبات HTTP، تحصل على مكدّس سريع ونظيف.
import axios from 'axios';
import * as cheerio from 'cheerio';
async function scrapeProductPage(url) {
const { data: html } = await axios.get(url, {
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/125.0.0.0',
'Accept': 'text/html,application/xhtml+xml',
'Accept-Language': 'en-US,en;q=0.9',
},
});
const $ = cheerio.load(html);
return {
title: $('h1.product-title').text().trim(),
price: parseFloat($('span.price').text().replace(/[^0-9.]/g, '')),
availability: $('span.stock-status').text().trim(),
images: $('div.gallery img')
.map((_, el) => $(el).attr('src'))
.get(),
};
}
هذا كل شيء — لا متصفح، لا انتظار، لا ذاكرة مهدرّة. تطلب HTML، تحلّله، تستخرج البيانات. لكن بمجرد أن تُرسل عشرات الطلبات لنفس الموقع، ستحصل على 403 Forbidden أو 429 Too Many Requests. تحتاج بروكسي.
تكامل البروكسي مع axios
الطريقة الأولى: إعدادات البروكسي المدمجة في axios
Axios يدعم بروكسي HTTP/HTTPS بشكل مباشر عبر خيار proxy:
const response = await axios.get('https://example.com/product/123', {
proxy: {
host: 'gate.proxyhat.com',
port: 8080,
auth: {
username: 'user-country-US',
password: 'PASSWORD',
},
},
});
هذه الطريقة تعمل مع بروكسي HTTP. لكنها لا تدعم SOCKS5، ولا تسمح بتدوير ديناميكي لكل طلب بسهولة.
الطريقة الثانية: https-proxy-agent (أكثر مرونة)
للتحكم الكامل — بما في ذلك SOCKS5 والتدوير لكل طلب — استخدم https-proxy-agent أو socks-proxy-agent مع خيار httpsAgent في axios:
import { HttpsProxyAgent } from 'https-proxy-agent';
import axios from 'axios';
// بروكسي سكني مع استهداف جغرافي — جلسة لزجة لمدة 10 دقائق
const agent = new HttpsProxyAgent(
'http://user-country-US-session-abc123:PASSWORD@gate.proxyhat.com:8080'
);
const { data } = await axios.get('https://example.com/api/prices', {
httpsAgent: agent,
});
مع https-proxy-agent يمكنك إنشاء agent مختلف لكل طلب — وهذا مفتاح تدوير البروكسي.
متى Cheerio كافٍ ومتى تحتاج متصفح رأسي؟
القرار حاسم لأن المتصفح اللامرئي يستهلك 10-50x موارد أكثر من Cheerio:
| المعيار | Cheerio + axios | متصفح رأسي (Puppeteer) |
|---|---|---|
| الذاكرة لكل عامل | ~20-50 MB | ~200-500 MB |
| سرعة الطلب | 50-200 ms | 2-8 seconds |
| المواقع المدعومة | SSR / HTML ثابت | SPA / CSR / JS-heavy |
| التعامل مع CAPTCHA | لا (يحتاج بروكسي جيد) | ممكن مع حلّ CAPTCHA |
| التعقيد | منخفض | مرتفع |
استخدم Cheerio عندما:
- الصفحة تُرسل HTML مكتمل (تحقق: View Source — إذا ترى المحتوى هناك، Cheerio كافٍ).
- البيانات في جداول HTML، قوائم منتجات، نتائج بحث.
- تحتاج سرعة وإنتاجية عالية.
- تعمل في بيئة محدودة الموارد (حاويات صغيرة، Serverless).
تحتاج متصفح رأسي عندما:
- المحتوى يُحمّل عبر JavaScript بعد تحميل الصفحة (React/Vue SPA).
- الموقع يتطلب تفاعل (نقر أزرار، تمرير لانهائي).
- البيانات محمية بـ Cloudflare Turnstile أو CAPTCHA معقدة.
قاعدة ذهبية: ابدأ بـ Cheerio. إذا فشل، تحقق إن كان HTML فارغًا من المحتوى المطلوب — عندها انتقل للمتصفح الرأسي.
بناء تدوير بروكسي سكني كـ axios interceptor قابل لإعادة الاستخدام
بدلاً من تمرير البروكسي يدويًا لكل طلب، سنبنيه كـ axios interceptor يُنشئ جلسة جديدة تلقائيًا. هذا النمط يفصل منطق البروكسي عن منطق الكشط تمامًا.
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
import crypto from 'crypto';
// ─── إعدادات البروكسي ────────────────────────────────
const PROXY_CONFIG = {
host: 'gate.proxyhat.com',
port: 8080,
username: 'YOUR_USERNAME',
password: 'YOUR_PASSWORD',
};
// ─── توليد معرّف جلسة عشوائي ──────────────────────────
function generateSessionId() {
return crypto.randomBytes(8).toString('hex');
}
// ─── إنشاء agent بروكسي مع استهداف جغرافي ────────────
function createProxyAgent(country = 'US', sessionId = null) {
const session = sessionId || generateSessionId();
const proxyUser = `${PROXY_CONFIG.username}-country-${country}-session-${session}`;
const proxyUrl = `http://${proxyUser}:${PROXY_CONFIG.password}@${PROXY_CONFIG.host}:${PROXY_CONFIG.port}`;
return { agent: new HttpsProxyAgent(proxyUrl), sessionId: session };
}
// ─── بناء عميل axios مع interceptor تدوير البروكسي ───
function createScrapingClient(country = 'US', maxRetries = 3) {
const client = axios.create({ timeout: 15000 });
// Interceptor الطلب: ضخ agent بروكسي جديد لكل طلب
client.interceptors.request.use((config) => {
const { agent, sessionId } = createProxyAgent(country);
config.httpsAgent = agent;
config.httpAgent = agent;
config.metadata = { ...config.metadata, sessionId, retries: 0 };
return config;
});
// Interceptor الاستجابة: إعادة المحاولة عند 403/429 مع بروكسي جديد
client.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
const retries = originalRequest.metadata?.retries || 0;
if (
retries < maxRetries &&
error.response &&
(error.response.status === 403 || error.response.status === 429)
) {
const { agent } = createProxyAgent(country);
originalRequest.httpsAgent = agent;
originalRequest.httpAgent = agent;
originalRequest.metadata.retries = retries + 1;
// انتظار تراجعي أسي قبل إعادة المحاولة
const delay = Math.pow(2, retries) * 1000 + Math.random() * 1000;
await new Promise((r) => setTimeout(r, delay));
return client(originalRequest);
}
return Promise.reject(error);
}
);
return client;
}
export { createScrapingClient };
الآن أي طلب يمر عبر createScrapingClient() يحصل تلقائيًا على IP سكني جديد، وإذا حصل على 403 أو 429، يُعيد المحاولة بـ IP مختلف — بدون أي كود إضافي في منطق الكشط.
الكشط المتزامن مع p-limit: السيطرة على التدفق
عند كشط 10,000 رابط، لا يمكنك إرسالها كلها دفعة واحدة — ستحصل على حجب فوري وربما تعطيل الحساب. ولا يمكنك معالجتها واحدة تلو الأخرى — سيستغرق الأمر أيامًا. الحل: التحكم في التزامن.
p-limit هي مكتبة خفيفة تحدد عدد الوعود المتزامنة:
import pLimit from 'p-limit';
import { createScrapingClient } from './proxy-client.js';
import * as cheerio from 'cheerio';
// ─── إعداد العميل والحدود ─────────────────────────────
const scraper = createScrapingClient('US', 3);
const limit = pLimit(50); // 50 طلب متزامن كحد أقصى
// ─── كشط منتج واحد ────────────────────────────────────
async function scrapeProduct(url) {
try {
const { data: html } = await scraper.get(url);
const $ = cheerio.load(html);
return {
url,
title: $('h1').text().trim(),
price: parseFloat($('span.price').first().text().replace(/[^0-9.]/g, '')),
inStock: !$('span.out-of-stock').length,
};
} catch (err) {
return { url, error: err.message, status: err.response?.status };
}
}
// ─── كشط 10,000 رابط مع التحكم بالتزامن ──────────────
async function scrapeAll(urls) {
const results = await Promise.all(
urls.map((url) => limit(() => scrapeProduct(url)))
);
const succeeded = results.filter((r) => !r.error);
const failed = results.filter((r) => r.error);
console.log(`نجح: ${succeeded.length}, فشل: ${failed.length}`);
return { succeeded, failed };
}
مع pLimit(50) وبروكسي سكني متدوّر، تُرسل 50 طلبًا في أي لحظة — كل واحد من IP مختلف. هذا يمنحك سرعة عالية دون إثارة أنظمة الحماية.
مثال حقيقي: كشط متجر تجارة إلكترونية عبر 10,000 رابط
لنبني خط كشط كامل يتضمن: تحميل قائمة الروابط، كشط مع تدوير البروكسي، حفظ النتائج، وإعادة محاولة الفاشلة.
import fs from 'fs/promises';
import pLimit from 'p-limit';
import { createScrapingClient } from './proxy-client.js';
import * as cheerio from 'cheerio';
// ─── إعداد ─────────────────────────────────────────────
const BATCH_SIZE = 500;
const CONCURRENCY = 50;
const MAX_RETRIES = 3;
const scraper = createScrapingClient('US', MAX_RETRIES);
const limit = pLimit(CONCURRENCY);
// ─── Circuit Breaker بسيط ─────────────────────────────
class CircuitBreaker {
constructor(failureThreshold = 0.4, cooldownMs = 30000) {
this.failureThreshold = failureThreshold;
this.cooldownMs = cooldownMs;
this.failures = 0;
this.total = 0;
this.openUntil = 0;
}
recordSuccess() {
this.failures = 0;
this.total++;
}
recordFailure() {
this.failures++;
this.total++;
if (this.failures / this.total > this.failureThreshold) {
this.openUntil = Date.now() + this.cooldownMs;
}
}
async waitIfOpen() {
if (Date.now() < this.openUntil) {
const wait = this.openUntil - Date.now();
console.warn(`Circuit breaker مفتوح — انتظار ${wait}ms`);
await new Promise((r) => setTimeout(r, wait));
}
}
}
const circuit = new CircuitBreaker();
// ─── كشط منتج واحد ────────────────────────────────────
async function scrapeProduct(url) {
await circuit.waitIfOpen();
try {
const { data: html } = await scraper.get(url, {
headers: { 'Accept-Encoding': 'gzip' },
});
const $ = cheerio.load(html);
circuit.recordSuccess();
return {
url,
title: $('h1.product-title').text().trim() || null,
price: parseFloat($('span.price').first().text().replace(/[^0-9.]/g, '')) || null,
rating: parseFloat($('div.stars').attr('data-rating')) || null,
inStock: !$('span.out-of-stock').length,
};
} catch (err) {
circuit.recordFailure();
return { url, error: err.message, status: err.response?.status };
}
}
// ─── معالجة دفعات ─────────────────────────────────────
async function processBatch(urls) {
return Promise.all(urls.map((url) => limit(() => scrapeProduct(url))));
}
// ─── الخط الرئيسي ─────────────────────────────────────
async function main() {
const urls = JSON.parse(await fs.readFile('product_urls.json', 'utf-8'));
console.log(`تحميل ${urls.length} رابط`);
const allResults = [];
const failedUrls = [];
for (let i = 0; i < urls.length; i += BATCH_SIZE) {
const batch = urls.slice(i, i + BATCH_SIZE);
console.log(`معالجة الدفعة ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(urls.length / BATCH_SIZE)}`);
const results = await processBatch(batch);
for (const r of results) {
if (r.error) {
failedUrls.push(r.url);
} else {
allResults.push(r);
}
}
// حفظ تقدم جزئي
await fs.writeFile('results_partial.json', JSON.stringify(allResults, null, 2));
// توقف قصير بين الدفعات لتقليل الضغط
await new Promise((r) => setTimeout(r, 2000));
}
// إعادة محاولة الفاشلة
console.log(`إعادة محاولة ${failedUrls.length} رابط فاشل...`);
const retryResults = await processBatch(failedUrls);
const finalResults = [
...allResults,
...retryResults.filter((r) => !r.error),
];
await fs.writeFile('results_final.json', JSON.stringify(finalResults, null, 2));
console.log(`تم بنجاح: ${finalResults.length} منتج من ${urls.length} رابط`);
}
main().catch(console.error);
معالجة الأخطاء: 403/429 إلى تدوير البروكسي و Circuit Breaker
الأخطاء الأكثر شيوعًا عند الكشط:
- 403 Forbidden: الموقع كشف أنك لست مستخدمًا حقيقيًا. الحل: بروكسي سكني + عنوان User-Agent حقيقي.
- 429 Too Many Requests: أرسلت طلبات كثيرة من نفس IP. الحل: تدوير IP مع تراجعي أسي.
- Timeout: البروكسي بطيء أو الموقع لا يستجيب. الحل: مهلة زمنية + إعادة محاولة ببروكسي مختلف.
- CAPTCHA: أنظمة الحماية تحدّد سلوكك. الحل: تقليل التزامن، استخدام بروكسي سكني حقيقي، أو دمج حلّ CAPTCHA.
الـ interceptor الذي بنيناه يعالج 403/429 تلقائيًا بإعادة المحاولة بـ IP جديد. لكن ماذا لو كانت نسبة الفشل مرتفعة بشكل غير طبيعي؟ هذا يعني أن الموقع قد يكون تحت هجوم أو أن البروكسي لديه مشكلة — هنا يأتي دور Circuit Breaker:
الـ Circuit Breaker يتتبع نسبة الفشل. إذا تجاوزت 40%، يُوقف الطلبات لفترة تبريد (30 ثانية)، ثم يستأنف. هذا يمنع هدر الموارد ويمنح الموقع فرصة للاستقرار.
أنماط التوسع: حاويات، أسطول رأسي، وتوزيع الحمل
التشغيل في حاويات Docker
للتوسع أفقيًا، شغّل عدة حاويات لكل واحدة مثيل الكشط مع بروكسي مختلف:
# docker-compose.yml — 4 عمال كشط متوازية
services:
scraper-us:
build: .
environment:
- COUNTRY=US
- CONCURRENCY=50
scraper-de:
build: .
environment:
- COUNTRY=DE
- CONCURRENCY=50
scraper-uk:
build: .
environment:
- COUNTRY=GB
- CONCURRENCY=50
كل حاوية تستهدف بلدًا مختلفًا عبر بروكسي سكني — مما يوزّع الحمل ويقلل خطر الحجب.
قائمة انتظار Redis لتوزيع العمل
للتنسيق بين العمال، استخدم Redis كطابور:
- ادفع الروابط إلى قائمة Redis
LPUSH urls https://... - كل عامل يسحب:
RPOP urls - النتائج تُكتب إلى Redis أو قاعدة بيانات مركزية.
هذا يضمن عدم كشط نفس الرابط مرتين، ويسمح بإضافة عمال ديناميكيًا.
النقاط الرئيسية
- Cheerio + axios كافٍ لأغلب مواقع SSR — أسرع 10-50x من المتصفح الرأسي.
- تحقق من View Source: إذا كان المحتوى في HTML المصدر، Cheerio يعمل. إذا كان فارغًا، تحتاج Puppeteer.
- بروكسي سكني متدوّر كـ interceptor يعزل منطق البروكسي عن منطق الكشط — نظيف وقابل لإعادة الاستخدام.
- p-limit للتحكم في التزامن: ابدأ بـ 50، قلّل إذا حصلت على 429.
- Circuit Breaker يمنع هدر الموارد عند ارتفاع نسبة الفشل.
- مع ProxyHat: استخدم
gate.proxyhat.com:8080مع علامات-country-و-session-في اسم المستخدم للتدوير والاستهداف الجغرافي. - احترم robots.txt وشروط الخدمة — الكشط الأخلاقي يحميك قانونيًا ويحمي الموقع.
جاهز للبدء؟ أنشئ حسابك على ProxyHat واحصل على بروكسي سكني يدور تلقائيًا — أو تصفّح خطط الأسعار والمواقع المتاحة.






