Tutorial Crawlee: Framework Web Scraping Modern untuk Pengumpulan Data AI/ML
Web scraping merupakan salah satu tahap penting dalam pipeline data, terutama untuk proyek AI dan machine learning yang membutuhkan data dari berbagai sumber web. Crawlee adalah framework web scraping dan crawling open-source yang dikembangkan oleh Apify. Framework ini dirancang untuk membangun scraper yang andal, cepat, dan mudah di-maintain. Crawlee menyediakan berbagai fitur seperti auto-scaling, anti-blocking, session management, dan proxy rotation secara built-in.
Dalam tutorial ini, kita akan mempelajari cara menggunakan Crawlee mulai dari instalasi, penggunaan dasar dengan berbagai jenis crawler, hingga teknik-teknik advanced seperti proxy management, session handling, dan integrasi dengan pipeline data untuk kebutuhan AI/ML.
Mengapa Crawlee?
Sebelum membahas teknis, berikut beberapa alasan mengapa Crawlee layak dipertimbangkan:
- Anti-blocking otomatis: Crawlee secara otomatis menangani fingerprint browser, header rotation, dan retry logic untuk menghindari pemblokiran oleh website target
- Tiga jenis crawler: Mendukung
CheerioCrawler(HTTP + Cheerio untuk parsing HTML cepat),PlaywrightCrawler(browser automation penuh), danJSDOMCrawler(JSDOM untuk rendering JavaScript ringan) - Auto-scaling: Menyesuaikan jumlah concurrent request berdasarkan resource sistem dan response time target
- Request queue: Manajemen URL queue yang persisten sehingga crawling bisa dilanjutkan jika terjadi error
- Session management: Mengelola cookies dan session secara otomatis per request
- Storage built-in: Dataset dan key-value store untuk menyimpan hasil scraping secara terstruktur
- TypeScript-first: Ditulis dalam TypeScript dengan type safety penuh
Instalasi
Prasyarat
Pastikan Node.js versi 18 atau lebih baru sudah terpasang di sistem Anda.
node --version
v18.0.0 atau lebih baru
Membuat Project Baru
Cara tercepat untuk memulai adalah menggunakan CLI Crawlee:
npx crawlee create my-crawler
cd my-crawler
CLI akan memberikan pilihan template. Untuk tutorial ini, pilih template CheerioCrawler sebagai awal.
Instalasi Manual
Jika Anda ingin menambahkan Crawlee ke project yang sudah ada:
# Untuk CheerioCrawler (HTTP-based, tanpa browser)
npm install crawlee
Untuk PlaywrightCrawler (browser automation)
npm install crawlee playwright
Install browser binaries untuk Playwright
npx playwright install chromium
Struktur Project
Setelah instalasi, struktur project minimal Anda akan terlihat seperti ini:
my-crawler/
├── src/
│ ├── main.ts # Entry point
│ ├── routes.ts # Route handlers
│ └── types.ts # Type definitions
├── storage/ # Auto-generated storage
│ ├── datasets/ # Hasil scraping
│ ├── keyvaluestores/ # Key-value storage
│ └── requestqueues/ # URL queue
├── package.json
└── tsconfig.json
Penggunaan Dasar
CheerioCrawler - Scraping Cepat
CheerioCrawler adalah pilihan terbaik untuk website yang tidak memerlukan JavaScript rendering. Crawler ini menggunakan HTTP request dan Cheerio (jQuery-like API) untuk parsing HTML.
import { CheerioCrawler, Dataset } from 'crawlee';
const crawler = new CheerioCrawler({
maxRequestsPerCrawl: 50,
async requestHandler({ request, $, enqueueLinks, log }) {
const title = $('title').text().trim();
const description = $('meta[name="description"]').attr('content') || '';
log.info(Scraping: ${title}, { url: request.url });
const articles = $('article').map((, el) => ({
judul: $(el).find('h2').text().trim(),
ringkasan: $(el).find('p').first().text().trim(),
link: $(el).find('a').attr('href'),
})).get();
await Dataset.pushData({
url: request.url,
title,
description,
articles,
scrapedAt: new Date().toISOString(),
});
await enqueueLinks({
globs: ['https://example.com/blog/'],
label: 'DETAIL',
});
},
failedRequestHandler({ request, log }) {
log.error(Request gagal: ${request.url});
},
});
await crawler.run(['https://example.com/blog']);
Dalam contoh di atas:
$adalah objek Cheerio yang bisa digunakan seperti jQueryenqueueLinkssecara otomatis menemukan dan menambahkan link baru ke queueDataset.pushDatamenyimpan data hasil scraping ke storage lokalmaxRequestsPerCrawlmembatasi jumlah request total
PlaywrightCrawler - Website Dinamis
Untuk website yang membutuhkan JavaScript rendering (SPA, infinite scroll, dsb), gunakan PlaywrightCrawler:
import { PlaywrightCrawler, Dataset } from 'crawlee';
const crawler = new PlaywrightCrawler({
maxRequestsPerCrawl: 20,
headless: true,
launchContext: {
launchOptions: {
args: ['--no-sandbox'],
},
},
async requestHandler({ request, page, log, enqueueLinks }) {
log.info(Membuka: ${request.url});
// Tunggu elemen muncul
await page.waitForSelector('.product-card', { timeout: 10000 });
// Scroll untuk memuat konten lazy-loaded
await page.evaluate(async () => {
for (let i = 0; i < 3; i++) {
window.scrollBy(0, window.innerHeight);
await new Promise(r => setTimeout(r, 1000));
}
});
// Ekstrak data produk
const products = await page.$eval('.product-card', (cards) =>
cards.map((card) => ({
nama: card.querySelector('.product-title')?.textContent?.trim(),
harga: card.querySelector('.product-price')?.textContent?.trim(),
rating: card.querySelector('.rating')?.textContent?.trim(),
gambar: card.querySelector('img')?.getAttribute('src'),
}))
);
await Dataset.pushData({
url: request.url,
products,
totalProducts: products.length,
});
// Cari halaman berikutnya
await enqueueLinks({
selector: 'a.next-page',
label: 'LISTING',
});
},
});
await crawler.run(['https://example.com/products']);
Menggunakan Router untuk Menangani Berbagai Halaman
Dalam project yang lebih kompleks, Anda perlu menangani berbagai jenis halaman dengan cara yang berbeda. Crawlee menyediakan Router untuk ini:
import { CheerioCrawler, createCheerioRouter, Dataset } from 'crawlee';
const router = createCheerioRouter();
// Handler untuk halaman listing
router.addHandler('LISTING', async ({ request, $, enqueueLinks, log }) => {
log.info(Listing page: ${request.url});
const items = $('div.item').map((, el) => {
const link = $(el).find('a.detail-link').attr('href');
return link;
}).get();
for (const link of items) {
if (link) {
await crawler.addRequests([{
url: new URL(link, request.url).href,
label: 'DETAIL',
}]);
}
}
await enqueueLinks({
selector: '.pagination a.next',
label: 'LISTING',
});
});
// Handler untuk halaman detail
router.addHandler('DETAIL', async ({ request, $, log }) => {
log.info(Detail page: ${request.url});
const data = {
url: request.url,
judul: $('h1.title').text().trim(),
konten: $('div.content').text().trim(),
tanggal: $('span.date').text().trim(),
kategori: $('span.category').text().trim(),
tags: $('span.tag').map((, el) => $(el).text().trim()).get(),
};
await Dataset.pushData(data);
});
// Default handler
router.addDefaultHandler(async ({ request, enqueueLinks, log }) => {
log.info(Default handler: ${request.url});
await enqueueLinks({
globs: ['https://example.com/articles/'],
label: 'LISTING',
});
});
const crawler = new CheerioCrawler({
requestHandler: router,
maxRequestsPerCrawl: 100,
});
await crawler.run(['https://example.com']);
Penggunaan Advanced
Proxy Management
Proxy rotation sangat penting untuk web scraping berskala besar agar tidak terblokir:
import { CheerioCrawler, 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 CheerioCrawler({
proxyConfiguration,
sessionPoolOptions: {
maxPoolSize: 20,
sessionOptions: {
maxUsageCount: 50,
},
},
async requestHandler({ request, $, session, log }) {
const statusCode = request.loadedUrl ? 200 : 0;
if ($('title').text().includes('blocked') || $('title').text().includes('captcha')) {
session?.retire();
throw new Error('Terblokir, ganti session');
}
session?.markGood();
await Dataset.pushData({
url: request.url,
title: $('title').text(),
});
},
});
Request Queue dan Persistence
Crawlee menyimpan request queue secara persisten. Artinya, jika crawler terhenti di tengah jalan, Anda bisa melanjutkan dari posisi terakhir:
import { CheerioCrawler, RequestQueue } from 'crawlee';
// Membuat named queue untuk persistence
const requestQueue = await RequestQueue.open('my-queue');
// Menambahkan URL dengan metadata
await requestQueue.addRequests([
{
url: 'https://example.com/page/1',
userData: { pageNumber: 1, category: 'tech' },
label: 'LISTING',
},
{
url: 'https://example.com/page/2',
userData: { pageNumber: 2, category: 'tech' },
label: 'LISTING',
},
]);
const crawler = new CheerioCrawler({
requestQueue,
maxConcurrency: 5,
maxRequestRetries: 3,
requestHandlerTimeoutSecs: 60,
async requestHandler({ request, $, log }) {
const { pageNumber, category } = request.userData;
log.info(Page ${pageNumber}, Kategori: ${category});
// Proses data...
await Dataset.pushData({
page: pageNumber,
category,
items: $('div.item').length,
});
},
});
await crawler.run();
Custom Headers dan Session Rotation
import { CheerioCrawler } from 'crawlee';
const crawler = new CheerioCrawler({
preNavigationHooks: [
async ({ request }, gotOptions) => {
// Custom headers untuk setiap request
gotOptions.headers = {
...gotOptions.headers,
'Accept-Language': 'id-ID,id;q=0.9,en-US;q=0.8',
'Accept': 'text/html,application/xhtml+xml',
'Cache-Control': 'no-cache',
};
// Tambahkan cookie jika diperlukan
if (request.userData.authCookie) {
gotOptions.headers['Cookie'] = request.userData.authCookie;
}
},
],
postNavigationHooks: [
async ({ request, response, log }) => {
if (response && response.statusCode === 403) {
log.warning(403 Forbidden pada: ${request.url});
}
},
],
async requestHandler({ request, $, log }) {
// Handler utama
const data = $('body').text().substring(0, 500);
log.info(Berhasil scrape: ${request.url});
},
});
Export Data ke Berbagai Format
Crawlee menyimpan data dalam format JSON secara default, namun Anda bisa mengekspor ke format lain:
import { CheerioCrawler, Dataset } from 'crawlee';
import fs from 'fs';
const crawler = new CheerioCrawler({
async requestHandler({ $, request }) {
const items = $('tr.data-row').map((, row) => ({
nama: $(row).find('td:nth-child(1)').text().trim(),
nilai: $(row).find('td:nth-child(2)').text().trim(),
tanggal: $(row).find('td:nth-child(3)').text().trim(),
})).get();
await Dataset.pushData(items);
},
});
await crawler.run(['https://example.com/data']);
// Export dataset
const dataset = await Dataset.open();
const { items } = await dataset.getData();
// Export ke JSON
fs.writeFileSync('output.json', JSON.stringify(items, null, 2));
// Export ke CSV
const headers = Object.keys(items[0] || {});
const csvContent = [
headers.join(','),
...items.map(item =>
headers.map(h => "${String(item[h] || '').replace(/"/g, '""')}").join(',')
),
].join('\n');
fs.writeFileSync('output.csv', csvContent);
console.log(Exported ${items.length} records);
Integrasi dengan Pipeline AI/ML
Salah satu kasus penggunaan Crawlee yang paling menarik adalah sebagai bagian dari pipeline pengumpulan data untuk AI/ML:
import { CheerioCrawler, Dataset } from 'crawlee';
import fs from 'fs';
interface TrainingData {
text: string;
label: string;
source: string;
timestamp: string;
}
const crawler = new CheerioCrawler({
maxRequestsPerCrawl: 200,
async requestHandler({ request, $, log }) {
// Scrape artikel untuk training data NLP
const articleBody = $('article .content, .post-body, .entry-content')
.text()
.trim()
.replace(/\s+/g, ' ');
if (articleBody.length < 100) {
log.warning(Konten terlalu pendek, skip: ${request.url});
return;
}
const category = request.userData.category || 'uncategorized';
const trainingData: TrainingData = {
text: articleBody,
label: category,
source: request.url,
timestamp: new Date().toISOString(),
};
await Dataset.pushData(trainingData);
log.info(Collected: ${articleBody.substring(0, 80)}...);
},
});
// Kumpulkan data dari berbagai sumber
await crawler.run([
{ url: 'https://example.com/tech', userData: { category: 'technology' } },
{ url: 'https://example.com/science', userData: { category: 'science' } },
{ url: 'https://example.com/business', userData: { category: 'business' } },
]);
// Baca dataset dan siapkan untuk training
const dataset = await Dataset.open();
const { items } = await dataset.getData();
// Simpan dalam format JSONL (satu JSON per baris, umum untuk training LLM)
const jsonlContent = items
.map(item => JSON.stringify(item))
.join('\n');
fs.writeFileSync('trainingdata.jsonl', jsonlContent);
console.log(Siap: ${items.length} sampel training data);
Rate Limiting dan Politeness
Menjadi "polite" saat scraping adalah praktik yang baik dan mengurangi risiko pemblokiran:
import { CheerioCrawler } from 'crawlee';
const crawler = new CheerioCrawler({
// Batasi kecepatan
maxConcurrency: 3,
maxRequestsPerMinute: 30,
// Tambahkan delay antar request
requestHandlerTimeoutSecs: 30,
navigationTimeoutSecs: 30,
// Hormati robots.txt
respectRobotsTxt: true,
// Retry configuration
maxRequestRetries: 2,
async requestHandler({ request, $, log }) {
log.info(Scraping: ${request.url});
await Dataset.pushData({
url: request.url,
content: $('main').text().trim(),
});
},
});
Error Handling dan Monitoring
import { CheerioCrawler, Dataset, Log } from 'crawlee';
const stats = {
success: 0,
failed: 0,
skipped: 0,
};
const crawler = new CheerioCrawler({
maxRequestRetries: 3,
async requestHandler({ request, $, log }) {
const title = $('title').text();
if (!title) {
stats.skipped++;
log.warning(Halaman tanpa title, skip: ${request.url});
return;
}
await Dataset.pushData({
url: request.url,
title,
contentLength: $('body').text().length,
});
stats.success++;
},
async failedRequestHandler({ request, log }) {
stats.failed++;
log.error(Gagal setelah retry: ${request.url}, {
errors: request.errorMessages,
});
// Simpan URL yang gagal untuk analisis
await Dataset.pushData({
url: request.url,
status: 'FAILED',
errors: request.errorMessages,
});
},
});
await crawler.run(['https://example.com']);
console.log('Statistik crawling:', stats);
Best Practices
1. Pilih Crawler yang Tepat
- Gunakan CheerioCrawler untuk website statis atau server-rendered. Ini jauh lebih cepat dan hemat resource
- Gunakan PlaywrightCrawler hanya jika konten dimuat secara dinamis via JavaScript
- Mulai dengan CheerioCrawler, baru beralih ke PlaywrightCrawler jika dibutuhkan
2. Kelola Resource dengan Bijak
const crawler = new CheerioCrawler({
maxConcurrency: 5,
maxRequestsPerMinute: 60,
maxRequestsPerCrawl: 1000,
requestHandlerTimeoutSecs: 30,
maxRequestRetries: 2,
});
Jangan terlalu agresif dengan concurrency. Mulai dengan angka kecil dan tingkatkan secara bertahap.
3. Gunakan Label untuk Routing
await crawler.addRequests([
{ url: 'https://example.com/list', label: 'LISTING' },
{ url: 'https://example.com/item/1', label: 'DETAIL' },
]);
Label memudahkan pengelolaan handler untuk jenis halaman yang berbeda.
4. Validasi Data Hasil Scraping
Selalu validasi data sebelum menyimpan:
async requestHandler({ $, request }) {
const title = $('h1').text().trim();
const price = parseFloat($('.price').text().replace(/[^\d.]/g, ''));
if (!title || isNaN(price)) {
throw new Error('Data tidak valid, akan di-retry');
}
await Dataset.pushData({ title, price, url: request.url });
}
5. Gunakan Storage Secara Efektif
import { KeyValueStore, Dataset } from 'crawlee';
// Simpan state atau konfigurasi
const store = await KeyValueStore.open();
await store.setValue('last-run', { timestamp: new Date().toISOString() });
// Simpan data terstruktur
await Dataset.pushData(results);
// Baca state sebelumnya
const lastRun = await store.getValue('last-run');
6. Bersihkan Storage Sebelum Run Baru
import { purgeDefaultStorages } from 'crawlee';
// Bersihkan storage dari run sebelumnya
await purgeDefaultStorages();
7. Logging yang Informatif
async requestHandler({ request, $, log }) {
log.info('Mulai scraping', { url: request.url });
log.debug('Detail parsing', { selectorCount: $('div.item').length });
log.warning('Konten tidak lengkap', { url: request.url });
}
Deployment dan Automasi
Menjalankan di Server
// src/main.ts
import { CheerioCrawler, Dataset } from 'crawlee';
async function main() {
const crawler = new CheerioCrawler({
async requestHandler({ request, $, log }) {
const data = {
url: request.url,
title: $('title').text(),
timestamp: new Date().toISOString(),
};
await Dataset.pushData(data);
log.info(Done: ${data.title});
},
});
await crawler.run(['https://example.com']);
// Akses hasil
const dataset = await Dataset.open();
const { items } = await dataset.getData();
console.log(Total: ${items.length} halaman berhasil di-scrape);
}
main().catch(console.error);
Menjalankan sebagai Cron Job
# Jalankan setiap hari pukul 02:00
0 2 cd /path/to/my-crawler && node dist/main.js >> /var/log/crawler.log 2>&1
Docker Deployment
FROM node:20-slim
RUN npx playwright install-deps chromium
WORKDIR /app
COPY package.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", "dist/main.js"]
Kesimpulan
Crawlee adalah framework web scraping yang matang dan production-ready. Dengan fitur anti-blocking bawaan, session management, auto-scaling, dan storage yang persisten, Crawlee menyederhanakan proses pengumpulan data dari web secara signifikan.
Untuk proyek AI/ML, Crawlee sangat berguna sebagai tahap awal pipeline data, mengumpulkan data mentah dari berbagai sumber web yang kemudian bisa diolah menjadi dataset training. Kombinasi CheerioCrawler untuk website statis dan PlaywrightCrawler untuk website dinamis memberikan fleksibilitas penuh dalam menangani berbagai jenis website.
Beberapa poin penting yang perlu diingat:
- Mulai dengan CheerioCrawler, beralih ke PlaywrightCrawler hanya jika dibutuhkan
- Selalu hormati robots.txt dan terapkan rate limiting yang wajar
- Validasi data hasil scraping sebelum menyimpan
- Manfaatkan request queue untuk crawling yang andal dan bisa dilanjutkan
- Gunakan proxy rotation untuk scraping berskala besar
Dengan memahami dasar-dasar yang telah dibahas dalam tutorial ini, Anda sudah siap membangun web scraper yang andal dan scalable untuk kebutuhan pengumpulan data Anda.