استخراج بيانات الويب بالطريقة الصحيحة. Crawlee هو إطار عمل TypeScript مفتوح المصدر من Apify يتعامل مع الأجزاء الصعبة — طوابير الطلبات، إعادة المحاولة، تدوير البروكسي، ومكافحة الحظر — حتى تتمكن من التركيز على استخراج البيانات.
ما ستتعلمه
بنهاية هذا الدليل، ستتمكن من:
- إعداد مشروع Crawlee مع TypeScript من الصفر
- بناء أدوات استخراج باستخدام PlaywrightCrawler للمواقع التي تعتمد على JavaScript
- استخدام CheerioCrawler لاستخراج HTML خفيف وسريع
- إدارة طوابير الطلبات لزحف آلاف الصفحات
- تخزين البيانات المستخرجة باستخدام نظام Dataset المدمج في Crawlee
- تطبيق تدوير البروكسي واستراتيجيات مكافحة الحظر
- التعامل مع الصفحات المتعددة والتمرير اللانهائي والمحتوى الديناميكي
- نشر أداة الاستخراج في الإنتاج باستخدام Docker
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبت (
node --version) - خبرة في TypeScript (الأنواع، async/await، generics)
- معرفة أساسية بـ HTML/CSS (المحددات، بنية DOM)
- محرر أكواد — يُنصح بـ VS Code أو Cursor
- Docker مثبت (اختياري، للنشر)
لماذا Crawlee؟
استخراج بيانات الويب في Node.js غالباً يعني تجميع Puppeteer و Cheerio ومكتبات الطلبات ومنطق إعادة المحاولة وإدارة الطوابير بنفسك. Crawlee يوفر كل هذا في إطار عمل واحد متماسك:
| الميزة | Crawlee | يدوي (Puppeteer + Cheerio) | Scrapy (Python) |
|---|---|---|---|
| اللغة | TypeScript/JavaScript | JavaScript | Python |
| دعم المتصفح | Playwright, Puppeteer | إعداد يدوي | Splash/Selenium |
| طابور الطلبات | مدمج مع الحفظ | تنفيذ يدوي | مدمج |
| إعادة المحاولة | قابل للتخصيص | يدوي | مدمج |
| تدوير البروكسي | مدمج مع إدارة الجلسات | يدوي | Middleware |
| مكافحة الحظر | بصمات، headers | يدوي | Middleware |
| تخزين البيانات | Dataset + Key-Value Store | يدوي (JSON/DB) | Item pipelines |
| أمان الأنواع | TypeScript كامل | اختياري | لا |
Crawlee يمنحك قوة Scrapy مع أمان TypeScript وتجربة مطور حديثة.
الخطوة 1: إعداد المشروع
ابدأ بإنشاء مشروع Crawlee جديد. واجهة الأوامر تُنشئ كل ما تحتاجه:
npx crawlee create my-scraper --template playwright-ts
cd my-scraperهذا يُنشئ بنية المشروع التالية:
my-scraper/
├── src/
│ ├── main.ts # نقطة الدخول
│ ├── routes.ts # معالجات المسارات
│ └── types.ts # أنواع مخصصة
├── storage/ # يُنشأ تلقائياً للبيانات والطوابير
├── package.json
├── tsconfig.json
└── Dockerfile # إعداد Docker جاهز للإنتاج
ثبت التبعيات:
npm installCrawlee يُثبت ثلاث حزم أساسية:
crawlee— نواة الإطار مع الزاحفات والطوابير والتخزينplaywright— أتمتة المتصفح للصفحات المُقدمة بـ JavaScript@crawlee/playwright— تكامل Playwright مع Crawlee
الخطوة 2: فهم بنية Crawlee
قبل كتابة الكود، دعنا نفهم كيف يعمل Crawlee:
┌─────────────────────────────────────────────┐
│ Crawlee │
│ │
│ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ طابور │───▶│ الزاحف │───▶│ مجموعة │ │
│ │ الطلبات │ │ (الموجه) │ │ البيانات│ │
│ └──────────┘ └──────────┘ └─────────┘ │
│ │ │ │
│ │ ┌────┴────┐ │
│ │ │ مدير │ │
│ │ │البروكسي │ │
│ │ └─────────┘ │
│ ▼ │
│ إعادة محاولة تلقائية عند الفشل │
│ التحكم في التزامن │
│ تحديد المعدل │
└─────────────────────────────────────────────┘
المكونات الرئيسية:
- طابور الطلبات — يدير عناوين URL للزحف، يتعامل مع التكرارات، ويحفظ الحالة عبر عمليات إعادة التشغيل
- الزاحف — يعالج كل طلب باستخدام دالة المعالجة (Cheerio لـ HTML، Playwright للصفحات المُقدمة بـ JS)
- الموجه — يوجه أنماط URL المختلفة إلى دوال معالجة مختلفة
- مجموعة البيانات — تخزن البيانات المستخرجة كسطور JSON، قابلة للتصدير إلى CSV أو JSON أو أي تنسيق
- مدير البروكسي — يدير تدوير البروكسي والجلسات لتجنب الحظر
الخطوة 3: بناء CheerioCrawler للصفحات الثابتة
لنبدأ بأسرع نوع زاحف — CheerioCrawler. يقوم بتنزيل HTML الخام وتحليله مع Cheerio (واجهة شبيهة بـ jQuery)، بدون تشغيل متصفح. مثالي للمواقع التي لا تتطلب عرض JavaScript.
استبدل محتوى src/main.ts:
import { CheerioCrawler, Dataset, log } from 'crawlee';
// تكوين السجلات
log.setLevel(log.LEVELS.INFO);
// إنشاء الزاحف
const crawler = new CheerioCrawler({
// الحد الأقصى لعدد الطلبات المتزامنة
maxConcurrency: 10,
// الحد الأقصى للطلبات في الدقيقة (تحديد المعدل)
maxRequestsPerMinute: 60,
// إعادة محاولة الطلبات الفاشلة حتى 3 مرات
maxRequestRetries: 3,
// معالج لكل صفحة
async requestHandler({ request, $, enqueueLinks, pushData }) {
const url = request.url;
log.info(`جاري الزحف: ${url}`);
// استخراج البيانات من الصفحة باستخدام محددات CSS
const title = $('h1').first().text().trim();
const description = $('meta[name="description"]').attr('content') || '';
const links = $('a[href]')
.map((_, el) => $(el).attr('href'))
.get()
.filter((href) => href.startsWith('http'));
// دفع البيانات المستخرجة إلى مجموعة البيانات
await pushData({
url,
title,
description,
linksFound: links.length,
scrapedAt: new Date().toISOString(),
});
// تتبع الروابط في الصفحة (زحف عرضي)
await enqueueLinks({
globs: ['https://example.com/**'],
label: 'DETAIL',
});
},
// يُستدعى عند فشل الطلب بعد كل المحاولات
async failedRequestHandler({ request }) {
log.error(`فشل: ${request.url} — ${request.errorMessages.join(', ')}`);
},
});
// بدء الزاحف بعناوين URL أولية
await crawler.run([
'https://example.com',
]);
// تصدير النتائج
const dataset = await Dataset.open();
await dataset.exportToJSON('results');
log.info('اكتمل الزحف! النتائج محفوظة في storage/datasets/default/');شغّله:
npx tsx src/main.tsبياناتك المستخرجة محفوظة في storage/datasets/default/ كملفات JSON فردية.
الخطوة 4: بناء PlaywrightCrawler للمواقع الديناميكية
العديد من المواقع الحديثة تعرض المحتوى باستخدام JavaScript. لهذه المواقع، تحتاج PlaywrightCrawler الذي يُشغل متصفحاً حقيقياً:
import { PlaywrightCrawler, Dataset, log } from 'crawlee';
const crawler = new PlaywrightCrawler({
// استخدام Chromium بدون واجهة
launchContext: {
launchOptions: {
headless: true,
},
},
// صفحات المتصفح مكلفة — حدد التزامن
maxConcurrency: 5,
// مهلة لكل صفحة (30 ثانية)
requestHandlerTimeoutSecs: 30,
async requestHandler({ request, page, enqueueLinks, pushData }) {
const url = request.url;
log.info(`جاري الزحف (متصفح): ${url}`);
// انتظار عرض المحتوى الرئيسي
await page.waitForSelector('.product-card', { timeout: 10000 });
// استخراج بيانات المنتجات
const products = await page.$$eval('.product-card', (cards) =>
cards.map((card) => ({
name: card.querySelector('.product-name')?.textContent?.trim() || '',
price: card.querySelector('.product-price')?.textContent?.trim() || '',
rating: card.querySelector('.product-rating')?.textContent?.trim() || '',
image: card.querySelector('img')?.getAttribute('src') || '',
}))
);
// دفع كل منتج إلى مجموعة البيانات
for (const product of products) {
await pushData({
...product,
sourceUrl: url,
scrapedAt: new Date().toISOString(),
});
}
// تتبع روابط الصفحات التالية
await enqueueLinks({
selector: 'a.pagination-next',
label: 'LISTING',
});
},
});
await crawler.run([
'https://example-shop.com/products?page=1',
]);متى تستخدم أي زاحف
| السيناريو | الزاحف | السبب |
|---|---|---|
| مواقع HTML ثابتة | CheerioCrawler | أسرع 10 مرات، بدون حمل المتصفح |
| محتوى مُقدم بـ JavaScript | PlaywrightCrawler | ينفذ JS، ينتظر العرض |
| تطبيقات الصفحة الواحدة (SPAs) | PlaywrightCrawler | يتعامل مع التوجيه من جانب العميل |
| APIs تُرجع HTML | CheerioCrawler | يحتاج فقط لتحليل HTML |
| مواقع تتطلب تسجيل الدخول | PlaywrightCrawler | يمكنه ملء النماذج |
الخطوة 5: استخدام Router للأنماط المتعددة
أدوات الاستخراج الحقيقية تحتاج للتعامل مع أنواع صفحات مختلفة — صفحات القوائم، صفحات التفاصيل، نتائج البحث. Router في Crawlee يجعل هذا نظيفاً:
أنشئ src/routes.ts:
import { createPlaywrightRouter, Dataset } from 'crawlee';
export const router = createPlaywrightRouter();
// المعالج الافتراضي — صفحات القوائم
router.addDefaultHandler(async ({ request, page, enqueueLinks, log }) => {
log.info(`معالجة القائمة: ${request.url}`);
// استخراج روابط العناصر الفردية
await enqueueLinks({
selector: 'a.item-link',
label: 'DETAIL',
});
// التعامل مع الصفحات المتعددة
const nextButton = await page.$('a.next-page');
if (nextButton) {
await enqueueLinks({
selector: 'a.next-page',
label: 'LISTING',
});
}
});
// معالج صفحة التفاصيل
router.addHandler('DETAIL', async ({ request, page, pushData, log }) => {
log.info(`معالجة التفاصيل: ${request.url}`);
await page.waitForSelector('.article-content', { timeout: 10000 });
const data = await page.evaluate(() => {
const title = document.querySelector('h1')?.textContent?.trim() || '';
const author = document.querySelector('.author-name')?.textContent?.trim() || '';
const date = document.querySelector('time')?.getAttribute('datetime') || '';
const content = document.querySelector('.article-content')?.textContent?.trim() || '';
const tags = Array.from(document.querySelectorAll('.tag'))
.map((tag) => tag.textContent?.trim() || '');
return { title, author, date, content, tags };
});
await pushData({
...data,
url: request.url,
scrapedAt: new Date().toISOString(),
});
});حدّث src/main.ts لاستخدام الموجه:
import { PlaywrightCrawler } from 'crawlee';
import { router } from './routes.js';
const crawler = new PlaywrightCrawler({
requestHandler: router,
maxConcurrency: 5,
maxRequestsPerMinute: 30,
});
await crawler.run([
{ url: 'https://example-blog.com/articles', label: 'LISTING' },
{ url: 'https://example-blog.com/search?q=typescript', label: 'SEARCH' },
]);الخطوة 6: التعامل مع الصفحات المتعددة والتمرير اللانهائي
الصفحات التقليدية
للمواقع التي تحتوي على أزرار "التالي" أو صفحات مرقمة:
router.addHandler('LISTING', async ({ page, enqueueLinks, request, log }) => {
const items = await page.$$eval('.item', (els) =>
els.map((el) => ({
title: el.querySelector('h2')?.textContent?.trim(),
url: el.querySelector('a')?.href,
}))
);
log.info(`الصفحة ${request.userData.page || 1}: وجدنا ${items.length} عنصر`);
for (const item of items) {
if (item.url) {
await enqueueLinks({
urls: [item.url],
label: 'DETAIL',
});
}
}
const nextUrl = await page.$eval('a.next', (el) => el.href).catch(() => null);
if (nextUrl) {
await enqueueLinks({
urls: [nextUrl],
label: 'LISTING',
userData: { page: (request.userData.page || 1) + 1 },
});
}
});التمرير اللانهائي
للمواقع التي تُحمّل المزيد من المحتوى أثناء التمرير:
router.addHandler('INFINITE', async ({ page, pushData, log }) => {
let previousHeight = 0;
let scrollAttempts = 0;
const maxScrolls = 20;
while (scrollAttempts < maxScrolls) {
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(2000);
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
if (currentHeight === previousHeight) {
log.info('لا يوجد محتوى إضافي للتحميل');
break;
}
previousHeight = currentHeight;
scrollAttempts++;
log.info(`التمرير ${scrollAttempts}/${maxScrolls} — الارتفاع: ${currentHeight}`);
}
const allItems = await page.$$eval('.feed-item', (items) =>
items.map((item) => ({
text: item.querySelector('.content')?.textContent?.trim() || '',
author: item.querySelector('.author')?.textContent?.trim() || '',
timestamp: item.querySelector('time')?.getAttribute('datetime') || '',
}))
);
log.info(`تم استخراج ${allItems.length} عنصر إجمالاً`);
for (const item of allItems) {
await pushData(item);
}
});الخطوة 7: تدوير البروكسي ومكافحة الحظر
الحظر هو أكبر تحدٍ في استخراج بيانات الويب. Crawlee يحتوي على أدوات مدمجة للمساعدة:
تدوير البروكسي الأساسي
import { PlaywrightCrawler, ProxyConfiguration } from 'crawlee';
const proxyConfiguration = new ProxyConfiguration({
proxyUrls: [
'http://user:pass@proxy1.example.com:8080',
'http://user:pass@proxy2.example.com:8080',
'http://user:pass@proxy3.example.com:8080',
],
});
const crawler = new PlaywrightCrawler({
proxyConfiguration,
requestHandler: router,
useSessionPool: true,
sessionPoolOptions: {
maxPoolSize: 100,
sessionOptions: {
maxUsageCount: 50,
},
},
});أفضل ممارسات مكافحة الحظر
const crawler = new PlaywrightCrawler({
maxRequestsPerMinute: 20,
browserPoolOptions: {
useFingerprints: true,
fingerprintOptions: {
fingerprintGeneratorOptions: {
browsers: ['chrome', 'firefox'],
operatingSystems: ['windows', 'macos', 'linux'],
locales: ['en-US', 'en-GB'],
},
},
},
preNavigationHooks: [
async ({ page }) => {
const width = 1280 + Math.floor(Math.random() * 200);
const height = 800 + Math.floor(Math.random() * 200);
await page.setViewportSize({ width, height });
await page.setExtraHTTPHeaders({
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
});
},
],
requestHandler: router,
});الخطوة 8: تخزين البيانات والتصدير
Crawlee يوفر نظامين للتخزين:
Dataset — للبيانات الجدولية
import { Dataset } from 'crawlee';
// دفع البيانات أثناء الزحف
await pushData({
name: 'TypeScript Handbook',
price: 29.99,
category: 'Programming',
});
// بعد الزحف، التصدير بتنسيقات مختلفة
const dataset = await Dataset.open();
// تصدير كـ JSON
await dataset.exportToJSON('output');
// تصدير كـ CSV
await dataset.exportToCSV('output');
// الحصول على كل البيانات مرة واحدة
const { items } = await dataset.getData();
console.log(`إجمالي العناصر: ${items.length}`);Key-Value Store — للبيانات المتنوعة
import { KeyValueStore } from 'crawlee';
const store = await KeyValueStore.open();
// حفظ لقطات الشاشة
await store.setValue('homepage-screenshot', await page.screenshot(), {
contentType: 'image/png',
});
// حفظ الإعدادات أو الحالة
await store.setValue('scraper-config', {
lastRun: new Date().toISOString(),
totalPages: 1500,
errors: 12,
});الخطوة 9: معالجة الأخطاء والمرونة
أدوات الاستخراج في الإنتاج يجب أن تتعامل مع الفشل بلطف:
import { PlaywrightCrawler, log } from 'crawlee';
const crawler = new PlaywrightCrawler({
maxRequestRetries: 3,
requestHandlerTimeoutSecs: 60,
async requestHandler({ request, page, pushData, session }) {
try {
const pageTitle = await page.title();
// كشف الحظر الناعم
if (
pageTitle.toLowerCase().includes('captcha') ||
pageTitle.toLowerCase().includes('verify') ||
pageTitle.toLowerCase().includes('access denied')
) {
session?.retire();
throw new Error(`تم كشف حظر في ${request.url}`);
}
// انتظار المحتوى مع بديل
try {
await page.waitForSelector('.main-content', { timeout: 10000 });
} catch {
log.warning(`محدد المحتوى غير موجود في ${request.url}، جاري تجربة البديل`);
await page.waitForSelector('body', { timeout: 5000 });
}
const data = await page.evaluate(() => ({
title: document.querySelector('h1')?.textContent?.trim() || 'بدون عنوان',
content: document.querySelector('.main-content')?.textContent?.trim() || '',
}));
await pushData({
...data,
url: request.url,
retryCount: request.retryCount,
});
} catch (error) {
log.error(`خطأ في زحف ${request.url}`, {
error: (error as Error).message,
retryCount: request.retryCount,
});
throw error;
}
},
async failedRequestHandler({ request, log }) {
log.error(`فشل نهائي: ${request.url}`, {
errors: request.errorMessages,
retries: request.retryCount,
});
const dataset = await Dataset.open('failed-requests');
await dataset.pushData({
url: request.url,
errors: request.errorMessages,
failedAt: new Date().toISOString(),
});
},
});الخطوة 10: مثال واقعي — زحف لوحة الوظائف
لنبني أداة استخراج كاملة تجمع قوائم الوظائف. هذا المثال يوضح كل المفاهيم معاً:
import { PlaywrightCrawler, Dataset, log } from 'crawlee';
interface JobListing {
title: string;
company: string;
location: string;
salary: string;
description: string;
tags: string[];
postedAt: string;
url: string;
scrapedAt: string;
}
const crawler = new PlaywrightCrawler({
maxConcurrency: 3,
maxRequestsPerMinute: 15,
maxRequestRetries: 3,
requestHandlerTimeoutSecs: 45,
browserPoolOptions: {
useFingerprints: true,
},
preNavigationHooks: [
async ({ page }) => {
await page.route('**/*.{png,jpg,jpeg,gif,webp,woff,woff2}', (route) =>
route.abort()
);
},
],
async requestHandler({ request, page, enqueueLinks, pushData, log }) {
const label = request.label || 'LISTING';
if (label === 'LISTING') {
log.info(`زحف صفحة القوائم: ${request.url}`);
await page.waitForSelector('.job-card', { timeout: 15000 });
await enqueueLinks({
selector: '.job-card a.job-title-link',
label: 'JOB_DETAIL',
});
const hasNextPage = await page.$('a[aria-label="Next page"]');
if (hasNextPage) {
await enqueueLinks({
selector: 'a[aria-label="Next page"]',
label: 'LISTING',
});
}
}
if (label === 'JOB_DETAIL') {
log.info(`زحف تفاصيل الوظيفة: ${request.url}`);
await page.waitForSelector('.job-detail', { timeout: 15000 });
const job: JobListing = await page.evaluate(() => {
const getText = (selector: string): string =>
document.querySelector(selector)?.textContent?.trim() || '';
return {
title: getText('h1.job-title'),
company: getText('.company-name'),
location: getText('.job-location'),
salary: getText('.salary-range'),
description: getText('.job-description'),
tags: Array.from(document.querySelectorAll('.skill-tag')).map(
(tag) => tag.textContent?.trim() || ''
),
postedAt: getText('.posted-date'),
url: window.location.href,
scrapedAt: new Date().toISOString(),
};
});
if (job.title && job.company) {
await pushData(job);
log.info(`تم الحفظ: ${job.title} في ${job.company}`);
}
}
},
});
log.info('بدء أداة زحف لوحة الوظائف...');
await crawler.run([
{ url: 'https://example-jobs.com/jobs?q=typescript', label: 'LISTING' },
]);
const dataset = await Dataset.open();
const { items } = await dataset.getData();
log.info(`اكتمل الزحف! تم استخراج ${items.length} قائمة وظائف.`);
await dataset.exportToJSON('jobs');الخطوة 11: النشر باستخدام Docker
مشاريع Crawlee تأتي مع Dockerfile جاهز للإنتاج:
FROM node:20-slim AS builder
RUN npx playwright install-deps chromium
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
FROM node:20-slim
RUN npx playwright install chromium
RUN npx playwright install-deps chromium
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
RUN mkdir -p storage
ENV NODE_ENV=production
ENV CRAWLEE_STORAGE_DIR=./storage
CMD ["node", "dist/main.js"]البناء والتشغيل:
docker build -t my-scraper .
docker run -v $(pwd)/output:/app/storage my-scraperالخطوة 12: أنماط متقدمة
الزحف القابل للاستئناف
Crawlee يحفظ طابور الطلبات على القرص. إذا تعطلت أداة الاستخراج، فقط أعد تشغيلها — ستستأنف من حيث توقفت:
const crawler = new PlaywrightCrawler({
requestHandler: router,
});
// التشغيل الأول: يعالج كل عناوين URL
await crawler.run(['https://example.com/page1', 'https://example.com/page2']);
// إذا أعدت التشغيل، يتم تخطي عناوين URL المكتملة تلقائياًاعتراض طلبات الشبكة
مراقبة وتعديل حركة الشبكة أثناء الزحف:
const crawler = new PlaywrightCrawler({
preNavigationHooks: [
async ({ page }) => {
page.on('response', async (response) => {
const url = response.url();
if (url.includes('/api/products')) {
try {
const data = await response.json();
const store = await KeyValueStore.open();
await store.setValue(`api-response-${Date.now()}`, data);
} catch {
// الاستجابة قد لا تكون JSON
}
}
});
await page.route('**/*', (route) => {
const type = route.request().resourceType();
if (['image', 'font', 'stylesheet'].includes(type)) {
return route.abort();
}
return route.continue();
});
},
],
async requestHandler({ page, pushData }) {
await page.waitForSelector('.content');
const title = await page.title();
await pushData({ title });
},
});استكشاف الأخطاء وإصلاحها
مشاكل شائعة
فشل تشغيل المتصفح في Docker: تأكد من تثبيت تبعيات Playwright:
npx playwright install-deps chromiumالحظر المتكرر:
- قلل
maxConcurrencyوmaxRequestsPerMinute - فعّل تدوير البروكسي
- استخدم
useFingerprints: true - أضف تأخيرات عشوائية بين الطلبات
مشاكل الذاكرة مع عمليات الزحف الكبيرة:
- استخدم
CheerioCrawlerبدلاً منPlaywrightCrawlerعند الإمكان - حدد
maxConcurrencyلتقليل استخدام الذاكرة - اضبط
maxRequestsPerCrawlللمعالجة على دفعات
الخطوات التالية
الآن بعد أن لديك أداة استخراج Crawlee عاملة، إليك بعض الطرق لتوسيعها:
- أضف قاعدة بيانات — خزّن النتائج في PostgreSQL باستخدام Drizzle ORM (اطلع على دليل Drizzle)
- ابنِ API — قدّم البيانات المستخرجة عبر REST API مع Hono (اطلع على دليل Hono)
- جدولة التشغيل — استخدم GitHub Actions (اطلع على دليل CI/CD)
- أضف استخراج بالذكاء الاصطناعي — ادمج مع Claude لتحليل البيانات الذكي (اطلع على دليل AI scraper)
الخلاصة
لقد بنيت أداة استخراج بيانات ويب احترافية باستخدام Crawlee و TypeScript. الآن تعرف كيف:
- تختار بين
CheerioCrawlerوPlaywrightCrawlerحسب الموقع المستهدف - تستخدم الموجهات للتعامل مع أنواع الصفحات المختلفة بنظافة
- تنفذ التعامل مع الصفحات المتعددة والتمرير اللانهائي
- تدير تدوير البروكسي والجلسات لتجنب الحظر
- تخزن وتصدر البيانات بتنسيقات متعددة
- تنشر أداة الاستخراج باستخدام Docker للإنتاج
Crawlee يتعامل مع الأجزاء الصعبة — إدارة الطوابير، إعادة المحاولة، تدوير البروكسي، بصمات المتصفح — حتى تتمكن من التركيز على كتابة منطق الاستخراج المهم.
استخراج سعيد!