Spider Crawlee Anda lancar di halaman biasa, tapi begitu ketemu reCAPTCHA v2 langsung mentok di requestHandler? Itu bukan bug — Crawlee memang tidak dirancang untuk solve CAPTCHA sendiri, jadi bagian itu perlu dipasangkan dengan layanan solver seperti CaptchaAI. Berikut integrasinya langsung ke kode: satu fungsi solveCaptcha() yang dipakai ulang di CheerioCrawler, PlaywrightCrawler, dan pola session pool, supaya crawler tidak berhenti setiap kali CAPTCHA muncul.
Setup CheerioCrawler: Fungsi solveCaptcha() yang Dipakai Ulang
Polanya sama di seluruh integrasi ini, empat langkah yang berulang di setiap crawler pada panduan ini:
- Kirim sitekey dan URL halaman ke endpoint
in.phpCaptchaAI dengan methoduserrecaptcha. - Simpan task ID dari respons
in.php— dipakai untuk polling. - Polling
res.phpsetiap 5 detik sampai status berubah jadi1(solved). - Pakai token reCAPTCHA yang dikembalikan di request atau form submission berikutnya.
Berikut penerapannya di CheerioCrawler — cocok untuk halaman yang CAPTCHA dan kontennya sudah ada di HTML awal, tanpa perlu render JavaScript:
const { CheerioCrawler } = require('crawlee');
const https = require('https');
const API_KEY = process.env.CAPTCHAAI_API_KEY;
async function solveCaptcha(sitekey, pageurl) {
// Submit task
const submitData = new URLSearchParams({
key: API_KEY,
method: 'userrecaptcha',
googlekey: sitekey,
pageurl: pageurl,
json: '1',
});
const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
method: 'POST',
body: submitData,
});
const submitResult = await submitResp.json();
if (submitResult.status !== 1) {
throw new Error(`Submit error: ${submitResult.request}`);
}
const taskId = submitResult.request;
// Poll for result
await new Promise(r => setTimeout(r, 15000));
for (let i = 0; i < 24; i++) {
const pollResp = await fetch(
`https://ocr.captchaai.com/res.php?key=${API_KEY}&action=get&id=${taskId}&json=1`
);
const pollResult = await pollResp.json();
if (pollResult.status === 1) return pollResult.request;
if (pollResult.request !== 'CAPCHA_NOT_READY') {
throw new Error(`Solve error: ${pollResult.request}`);
}
await new Promise(r => setTimeout(r, 5000));
}
throw new Error('Solve timeout');
}
// Crawlee spider with CAPTCHA handling
const crawler = new CheerioCrawler({
maxConcurrency: 5,
requestHandlerTimeoutSecs: 180,
async requestHandler({ request, $, log }) {
// Check if page has CAPTCHA
const captchaDiv = $('[data-sitekey]');
if (captchaDiv.length > 0) {
const sitekey = captchaDiv.attr('data-sitekey');
log.info(`CAPTCHA found on ${request.url}, solving...`);
const token = await solveCaptcha(sitekey, request.url);
log.info('CAPTCHA solved, submitting form');
// Submit form with token
const formData = new URLSearchParams({
'g-recaptcha-response': token,
});
const resp = await fetch(request.url, {
method: 'POST',
body: formData,
});
const html = await resp.text();
// Parse the result page...
}
// Extract data
const title = $('title').text();
const data = $('table tr').map((i, row) => ({
col1: $(row).find('td:eq(0)').text().trim(),
col2: $(row).find('td:eq(1)').text().trim(),
})).get();
log.info(`Scraped ${data.length} rows from ${request.url}`);
},
failedRequestHandler({ request, log }) {
log.error(`Failed: ${request.url}`);
},
});
// Run
(async () => {
await crawler.run([
'https://example.com/page1',
'https://example.com/page2',
]);
})();
requestHandler di atas mengecek elemen [data-sitekey] di setiap halaman yang di-scrape. Kalau ketemu, solveCaptcha() dipanggil dulu sebelum form dikirim ulang lewat POST dengan token g-recaptcha-response — scraping jalan terus tanpa campur tangan manual tiap kali CAPTCHA muncul.
PlaywrightCrawler: Solve CAPTCHA di Halaman yang Di-render JavaScript
Kalau situs target merender CAPTCHA lewat JavaScript — sitekey baru muncul di DOM setelah halaman selesai load — CheerioCrawler tidak cukup karena tidak menjalankan browser sungguhan. Di sinilah PlaywrightCrawler dipakai: fungsi solveCaptcha() yang sama tetap jalan, tokennya tinggal di-inject ke textarea g-recaptcha-response lewat page.evaluate(), lalu callback reCAPTCHA dipicu manual sebelum form di-submit.
const { PlaywrightCrawler } = require('crawlee');
const crawler = new PlaywrightCrawler({
maxConcurrency: 3,
requestHandlerTimeoutSecs: 180,
launchContext: {
launchOptions: {
headless: true,
args: [],
},
},
async requestHandler({ request, page, log }) {
await page.goto(request.url, { waitUntil: 'networkidle' });
// Check for reCAPTCHA
const sitekey = await page.evaluate(() => {
const el = document.querySelector('[data-sitekey]');
return el ? el.getAttribute('data-sitekey') : null;
});
if (sitekey) {
log.info(`CAPTCHA detected, solving for ${request.url}`);
const token = await solveCaptcha(sitekey, request.url);
// Inject token
await page.evaluate((t) => {
const ta = document.querySelector('[name="g-recaptcha-response"]');
if (ta) {
ta.style.display = 'block';
ta.value = t;
}
// Trigger callback
const widget = document.querySelector('.g-recaptcha');
if (widget) {
const cb = widget.getAttribute('data-callback');
if (cb && typeof window[cb] === 'function') {
window[cb](t);
}
}
}, token);
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle' });
}
// Extract data
const title = await page.title();
const content = await page.textContent('body');
log.info(`Page: ${title}, length: ${content.length}`);
},
});
Browser Playwright di atas jalan headless secara default — cocok dijalankan di server atau container tanpa display, termasuk kalau nanti di-deploy sebagai Apify Actor.
Reuse Token CAPTCHA dengan Session Pool Crawlee
Solve CAPTCHA berulang kali untuk domain yang sama itu boros — baik dari sisi waktu maupun thread CaptchaAI yang terpakai. useSessionPool di Crawlee memungkinkan token CAPTCHA yang sudah di-solve disimpan di session.userData, jadi request berikutnya dalam sesi yang sama tidak perlu solve ulang selama token itu masih berlaku.
const { CheerioCrawler, Session } = require('crawlee');
const crawler = new CheerioCrawler({
useSessionPool: true,
sessionPoolOptions: {
maxPoolSize: 10,
sessionOptions: {
maxUsageCount: 50,
},
},
async requestHandler({ request, $, session, log }) {
// If blocked, solve CAPTCHA and mark session as usable
if ($('.captcha-container').length > 0) {
const sitekey = $('[data-sitekey]').attr('data-sitekey');
const token = await solveCaptcha(sitekey, request.url);
// Store token in session for subsequent requests
session.userData = session.userData || {};
session.userData.captchaToken = token;
session.userData.tokenTime = Date.now();
log.info('CAPTCHA solved, session updated');
}
// Normal scraping
const items = $('div.item').map((i, el) => ({
name: $(el).find('.name').text().trim(),
price: $(el).find('.price').text().trim(),
})).get();
log.info(`Found ${items.length} items`);
},
});
Dampaknya kentara buat tim yang cost-sensitive — misalnya agensi price-monitoring atau freelancer scraping yang jalan di paket BASIC ($15/bulan, 5 thread). Kalau session pool dikonfigurasi dengan benar, satu token bisa dipakai untuk puluhan request berturut-turut dalam sesi yang sama, bukan solve satu-satu per request — thread yang terpakai jadi jauh lebih hemat. Untuk crawler yang di-deploy di region Asia Tenggara — AWS ap-southeast-1 (Singapura) atau GCP asia-southeast2 (Jakarta) — latensi ke endpoint CaptchaAI juga jadi pertimbangan; naikkan requestHandlerTimeoutSecs kalau round-trip solve-nya lebih lambat dari asumsi default 180 detik.
Kalau target scraping menyentuh data pribadi, UU Pelindungan Data Pribadi (UU 27/2022) tetap berlaku — pastikan hanya scrape data yang memang berwenang Anda proses.
Kenapa CAPTCHA Butuh Solver Terpisah dari Crawlee
Crawlee sudah kuat untuk session management, auto retry, rotasi proxy, dan request queue — tapi keempatnya tidak menyelesaikan CAPTCHA itu sendiri, seperti terlihat di tiga contoh kode di atas. CaptchaAI mengisi bagian yang kosong itu, dan manfaatnya menempel langsung ke fitur Crawlee yang sudah Anda pakai:
- Manajemen session bawaan — token CAPTCHA yang sudah di-solve ikut tersimpan di session yang sama, jadi sinyal browser tetap konsisten antar-request.
- Auto retry — request yang gagal karena CAPTCHA otomatis di-retry Crawlee setelah token dari CaptchaAI didapat.
- Rotasi proxy — rotasi proxy Crawlee bisa dipasangkan dengan dukungan proxy CaptchaAI untuk request yang perlu solve dari IP berbeda.
- Request queue — solve CAPTCHA cukup disisipkan sebagai langkah tambahan di
requestHandler, antrean scraping tetap jalan tanpa terganggu.
FAQ: Crawlee + CaptchaAI
Berapa thread CaptchaAI yang dibutuhkan untuk trafik tinggi di Crawlee?
CaptchaAI pakai model thread-based, bukan per-solve — paket BASIC ($15/bulan) sudah dapat 5 thread dengan solve tanpa batas per thread, sampai VIP-3 ($7,500/bulan) untuk 5.000 thread. Kebutuhan thread di Crawlee kira-kira mengikuti maxConcurrency crawler Anda; kalau session pool dipakai dengan benar seperti di atas, kebutuhannya bisa lebih rendah karena token di-reuse, bukan di-solve ulang tiap request.
CAPTCHA jenis apa saja yang bisa di-solve lewat integrasi ini?
Contoh di atas pakai reCAPTCHA v2 (method userrecaptcha), tapi endpoint yang sama juga menangani reCAPTCHA v3, Cloudflare Turnstile, Cloudflare Challenge, dan GeeTest v3 — tinggal ganti parameter method. CaptchaFox, Friendly Captcha, dan Lemin didukung dalam status beta. hCaptcha dan FunCaptcha belum didukung; GeeTest v4 masih berstatus segera hadir.
CheerioCrawler atau PlaywrightCrawler, mana yang cocok?
Pakai CheerioCrawler kalau CAPTCHA dan konten muncul langsung di HTML awal — jauh lebih ringan dan cepat karena tidak menjalankan browser. Pindah ke PlaywrightCrawler begitu situs target merender sitekey lewat JavaScript atau butuh interaksi browser sungguhan sebelum token bisa di-inject. PuppeteerCrawler jadi alternatif kalau proyek Anda sudah lebih familiar dengan API Puppeteer.
Apakah integrasi ini bisa jalan di Apify Actor?
Bisa. Deploy actor Crawlee seperti biasa di Apify, lalu simpan API key CaptchaAI sebagai environment variable di konfigurasi Actor, bukan hardcode di kode. Panggilan ke in.php dan res.php tetap HTTP biasa, jadi tidak perlu dependency tambahan di luar yang sudah dipakai Crawlee.
Kenapa hasil scraping kosong padahal CAPTCHA sudah ke-solve?
Paling sering karena callback reCAPTCHA tidak ikut dipicu setelah token disisipkan ke textarea — situs modern biasanya butuh callback lewat window (properti data-callback pada widget) dipanggil manual, seperti pada contoh PlaywrightCrawler di atas. Penyebab lain: requestHandlerTimeoutSecs terlalu pendek sehingga request keburu timeout sebelum polling res.php selesai — naikkan kalau solve time CAPTCHA di situs target biasanya lebih lama dari batas waktu default.
Panduan Terkait
- Middleware Scrapy untuk CaptchaAI
- Panduan quickstart CaptchaAI
Sambungkan CaptchaAI ke Crawlee sekarang — ambil API key CaptchaAI Anda.