Tutorial Crawlee: Framework Web Scraping Modern untuk Pengumpulan Data AI/ML

# 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...

By Ruby Abdullah · · tutorial
CrawleeWeb ScrapingData CollectionNode.jsAI

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), dan JSDOMCrawler (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 jQuery
  • enqueueLinks secara otomatis menemukan dan menambahkan link baru ke queue
  • Dataset.pushData menyimpan data hasil scraping ke storage lokal
  • maxRequestsPerCrawl membatasi 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.

Artikel Terkait

Crawl4AI: Web Crawling yang Dioptimalkan untuk Pipeline AI

Crawl4AI: Web Crawling yang Dioptimalkan untuk Pipeline AI Dalam membangun aplikasi AI modern, terutama yang berbasis Re...

Tutorial Scrapy + BeautifulSoup: Data Collection untuk ML

Tutorial 9: Scrapy + BeautifulSoup - Pengumpulan Data untuk ML Daftar Isi Pendahuluan Prasyarat Dasar-Dasar BeautifulSou...

Tutorial Lengkap Mistral AI API: Membangun Aplikasi AI dengan Model LLM dari Mistral

Tutorial Lengkap Mistral AI API: Membangun Aplikasi AI dengan Model LLM dari Mistral Mistral AI adalah salah satu penyed...

Tutorial Groq API: Inferensi LLM Super Cepat untuk Aplikasi AI

Tutorial Groq API: Inferensi LLM Super Cepat untuk Aplikasi AI Introduction Groq telah menjadi salah satu platform infer...