Use Cases

Solve CAPTCHA dengan Puppeteer dan Node.js menggunakan CaptchaAI

Skrip Puppeteer Anda berjalan mulus sampai halaman target menyajikan reCAPTCHA atau Cloudflare Turnstile — lalu macet total di langkah submit. Solusinya bukan melatih model OCR sendiri atau menyewa tim solving manual: panggil API CaptchaAI dari dalam skrip Node.js yang sama. Puppeteer mengekstrak sitekey dari DOM, CaptchaAI menyelesaikan tantangannya di server, dan Puppeteer menyuntikkan token yang dikembalikan sebelum form dikirim.

Panduan ini membangun integrasi lengkapnya langkah demi langkah — modul solver untuk reCAPTCHA v2 dan Turnstile, konfigurasi browser, penanganan callback JavaScript untuk situs yang tidak memakai submit formulir biasa, sampai contoh kode produksi yang siap dijalankan. Di bagian akhir ada skenario nyata dan gambaran biaya untuk tim scraping kecil.

Persiapan: tools dan API key CaptchaAI

Siapkan empat hal berikut. Kunci API didapat gratis saat mendaftar di captchaai.com — dari kunci ini semua request ke in.php dan res.php diautentikasi.

Persyaratan Detail
Node.js 16+ Dengan npm
Puppeteer npm install puppeteer
axios npm install axios
Kunci API CaptchaAI Dari captchaai.com

Alur kerja: kirim → simpan task ID → polling → pakai token

Integrasi Puppeteer dengan CaptchaAI selalu mengikuti pola empat langkah yang sama, apa pun tipe CAPTCHA-nya:

  1. Puppeteer membuka halaman yang menyajikan CAPTCHA dan menunggu widget-nya termuat
  2. Skrip mengekstrak sitekey dari elemen DOM (.g-recaptcha, .cf-turnstile, dan sejenisnya)
  3. CaptchaAI menerima sitekey dan pageurl, lalu menyelesaikan tantangan di server-nya sendiri
  4. Skrip menyuntikkan token yang dikembalikan ke halaman, lalu melanjutkan submit atau memicu callback

Langkah 1: Bangun modul solver reCAPTCHA v2 dan Turnstile

Pisahkan logika solve ke modul sendiri (solver.js) supaya bisa dipakai ulang di skrip mana pun. Modul ini punya dua fungsi terpisah — satu untuk reCAPTCHA v2 dengan method=userrecaptcha, satu lagi untuk Turnstile dengan method=turnstile — tapi keduanya memakai pola kirim-lalu-polling yang identik: submit ke in.php, simpan task_id yang dikembalikan, lalu polling res.php setiap POLL_INTERVAL sampai statusnya bukan lagi CAPCHA_NOT_READY.

// solver.js
const axios = require("axios");

const API_KEY = "YOUR_API_KEY";
const POLL_INTERVAL = 5000;
const MAX_ATTEMPTS = 60;

async function solveRecaptchaV2(siteKey, pageUrl) {
  // Submit task
  const submitResp = await axios.get("https://ocr.captchaai.com/in.php", {
    params: {
      key: API_KEY,
      method: "userrecaptcha",
      googlekey: siteKey,
      pageurl: pageUrl,
    },
  });

  if (!submitResp.data.startsWith("OK|")) {
    throw new Error(`Submit failed: ${submitResp.data}`);
  }

  const taskId = submitResp.data.split("|")[1];
  console.log(`Task submitted: ${taskId}`);

  // Poll for result
  for (let i = 0; i < MAX_ATTEMPTS; i++) {
    await new Promise((r) => setTimeout(r, POLL_INTERVAL));

    const result = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "get", id: taskId },
    });

    if (result.data === "CAPCHA_NOT_READY") continue;
    if (result.data.startsWith("OK|")) {
      return result.data.split("|")[1];
    }
    throw new Error(`Solve failed: ${result.data}`);
  }
  throw new Error("Solve timed out");
}

async function solveTurnstile(siteKey, pageUrl) {
  const submitResp = await axios.get("https://ocr.captchaai.com/in.php", {
    params: {
      key: API_KEY,
      method: "turnstile",
      sitekey: siteKey,
      pageurl: pageUrl,
    },
  });

  if (!submitResp.data.startsWith("OK|")) {
    throw new Error(`Submit failed: ${submitResp.data}`);
  }

  const taskId = submitResp.data.split("|")[1];

  for (let i = 0; i < MAX_ATTEMPTS; i++) {
    await new Promise((r) => setTimeout(r, POLL_INTERVAL));
    const result = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "get", id: taskId },
    });
    if (result.data === "CAPCHA_NOT_READY") continue;
    if (result.data.startsWith("OK|")) return result.data.split("|")[1];
    throw new Error(`Solve failed: ${result.data}`);
  }
  throw new Error("Solve timed out");
}

module.exports = { solveRecaptchaV2, solveTurnstile };

MAX_ATTEMPTS = 60 dan POLL_INTERVAL = 5000 memberi batas tunggu sekitar 5 menit sebelum skrip menyerah — longgar untuk CAPTCHA yang antre di jam sibuk, tapi tetap mencegah proses menggantung kalau task-nya gagal.

Langkah 2: Konfigurasi browser Puppeteer

Konfigurasi Puppeteer di langkah ini memakai mode standar. Flag --no-sandbox dan --disable-setuid-sandbox umum diperlukan saat Puppeteer jalan di dalam container atau CI, dan user agent diset eksplisit supaya konsisten di setiap run.

const puppeteer = require("puppeteer");

async function createBrowser() {
  const browser = await puppeteer.launch({
    headless: "new",
    args: [
      "--no-sandbox",
      "--disable-setuid-sandbox",
      ],
  });

  const page = await browser.newPage();
  await page.setUserAgent(
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
  );

  // Hide automation indicators
  await page.evaluateOnNewDocument(() => {
    Object.defineProperty(navigator, "webdriver", { get: () => false });
  });

  return { browser, page };
}

page.evaluateOnNewDocument menyelaraskan satu properti otomasi bawaan browser supaya konsisten dengan instalasi Chrome biasa — sekadar penyelarasan standar, karena tugas berat menyelesaikan CAPTCHA ada di sisi CaptchaAI, bukan di browser.

Langkah 3: Ekstrak sitekey dan suntikkan token ke halaman

Fungsi berikut menyatukan Langkah 1 dan 2: navigasi ke halaman, ambil sitekey lewat page.$eval, panggil solveRecaptchaV2, lalu suntikkan token ke elemen tersembunyi g-recaptcha-response sebelum tombol submit diklik.

const { solveRecaptchaV2 } = require("./solver");

async function scrapeWithCaptcha(url) {
  const { browser, page } = await createBrowser();

  try {
    await page.goto(url, { waitUntil: "networkidle2" });

    // Extract site key
    const siteKey = await page.$eval(
      ".g-recaptcha",
      (el) => el.getAttribute("data-sitekey")
    );
    console.log("Site key:", siteKey);

    // Solve with CaptchaAI
    const token = await solveRecaptchaV2(siteKey, url);
    console.log("Token received:", token.substring(0, 50));

    // Inject token
    await page.evaluate((token) => {
      document.getElementById("g-recaptcha-response").innerHTML = token;
      document.getElementById("g-recaptcha-response").style.display = "";
    }, token);

    // Submit the form
    await page.click('button[type="submit"]');
    await page.waitForNavigation({ waitUntil: "networkidle2" });

    // Scrape the content
    const content = await page.content();
    console.log("Page loaded successfully");
    return content;
  } finally {
    await browser.close();
  }
}

Perhatikan urutannya: token disuntikkan lebih dulu, baru submit diklik. Token reCAPTCHA berumur pendek, jadi tekan jeda antara solveRecaptchaV2 selesai dan klik submit seminimal mungkin.

Langkah 4: Tangani callback JavaScript reCAPTCHA

Sebagian situs — terutama yang dibangun dengan framework SPA — tidak memakai submit formulir biasa. reCAPTCHA di situs itu dipasang lewat callback JavaScript yang harus dipanggil manual begitu token diterima; klik tombol submit saja tidak memicu apa-apa karena tombolnya menunggu callback tereksekusi lebih dulu.

// Trigger the reCAPTCHA callback
await page.evaluate((token) => {
  // Method 1: Direct callback
  if (typeof ___grecaptcha_cfg !== "undefined") {
    const clients = ___grecaptcha_cfg.clients;
    Object.keys(clients).forEach((key) => {
      const client = clients[key];
      // Find the callback function
      const findCallback = (obj) => {
        for (const prop in obj) {
          if (typeof obj[prop] === "function") {
            obj[prop](token);
            return true;
          }
          if (typeof obj[prop] === "object" && obj[prop] !== null) {
            if (findCallback(obj[prop])) return true;
          }
        }
        return false;
      };
      findCallback(client);
    });
  }
}, token);

Kode ini menelusuri objek ___grecaptcha_cfg global milik reCAPTCHA, menemukan fungsi callback yang terdaftar untuk widget aktif, lalu memanggilnya langsung dengan token sebagai argumen. Kalau situs tidak mendefinisikan callback publik, submit formulir di Langkah 3 tetap jadi pilihan pertama — pola ini baru diperlukan saat submit biasa tidak memicu navigasi.

Contoh lengkap: dari navigasi sampai submit

Berikut versi ringkas yang menyatukan solver dan Puppeteer dalam satu file, siap dijalankan dengan node index.js setelah API_KEY diisi.

const puppeteer = require("puppeteer");
const axios = require("axios");

const API_KEY = "YOUR_API_KEY";

async function solveCaptcha(siteKey, pageUrl) {
  const submit = await axios.get("https://ocr.captchaai.com/in.php", {
    params: {
      key: API_KEY,
      method: "userrecaptcha",
      googlekey: siteKey,
      pageurl: pageUrl,
    },
  });
  const taskId = submit.data.split("|")[1];

  while (true) {
    await new Promise((r) => setTimeout(r, 5000));
    const result = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "get", id: taskId },
    });
    if (result.data === "CAPCHA_NOT_READY") continue;
    if (result.data.startsWith("OK|")) return result.data.split("|")[1];
    throw new Error(result.data);
  }
}

(async () => {
  const browser = await puppeteer.launch({
    headless: "new",
    args: [],
  });
  const page = await browser.newPage();

  try {
    await page.goto("https://staging.example.com/qa-login", {
      waitUntil: "networkidle2",
    });

    // Get the site key
    const siteKey = await page.$eval(".g-recaptcha", (el) =>
      el.getAttribute("data-sitekey")
    );

    // Solve
    const token = await solveCaptcha(siteKey, page.url());

    // Inject and submit
    await page.evaluate((t) => {
      document.getElementById("g-recaptcha-response").innerHTML = t;
    }, token);

    await page.click("#submit-btn");
    await page.waitForNavigation();

    console.log("Done:", page.url());
  } finally {
    await browser.close();
  }
})();

URL pada page.goto di atas memakai domain staging sebagai contoh — ganti dengan target Anda sendiri sebelum dijalankan di production, dan pastikan Anda memang berwenang menguji halaman tersebut.

Skenario nyata: monitoring harga e-commerce dengan tim scraping kecil

Pola ini paling sering dipakai tim price-monitoring dan agensi data kecil di Indonesia — banyak beroperasi lewat kontrak freelance ala Upwork atau Fastwork — yang menjalankan puluhan job Puppeteer paralel memantau harga di situs berpelindung Cloudflare Turnstile atau reCAPTCHA. CaptchaAI menagih per thread aktif, bukan per solve: paket BASIC ($15/bulan, 5 thread) biasanya cukup untuk uji coba awal, dan tim naik ke ADVANCE ($90/bulan, 50 thread) begitu volume job harian meningkat, tanpa biaya tambahan per CAPTCHA di kedua paket itu.

Server job di region AWS ap-southeast-1 (Singapura) atau GCP asia-southeast2 (Jakarta) umumnya tetap berlatensi rendah ke endpoint CaptchaAI; sisakan jeda polling yang longgar untuk koneksi mobile-first yang kadang kurang stabil. Untuk alur yang menyentuh data pihak lain, pastikan target adalah sistem yang memang berwenang Anda uji, sejalan dengan UU Pelindungan Data Pribadi (UU 27/2022) dan UU ITE.

Mengatasi masalah umum integrasi

Sebagian besar kegagalan bukan soal API CaptchaAI, tapi timing atau selector di sisi Puppeteer:

Masalah Penyebab Solusi
page.$eval gagal CAPTCHA dimuat setelah render awal Gunakan page.waitForSelector('.g-recaptcha') sebelum ekstraksi
Token tidak berfungsi Kedaluwarsa sebelum diserahkan Suntikkan token segera setelah solveRecaptchaV2 selesai
Situs mendeteksi Puppeteer Konfigurasi browser masih default dan mencolok Pasang puppeteer-extra dengan plugin deteksi-otomatisasi (mode standar)
Navigation timeout Halaman tidak bernavigasi setelah token dikirim Periksa apakah situs memakai AJAX, bukan submit formulir biasa
Polling lambat di jaringan tidak stabil Interval terlalu rapat untuk koneksi mobile-first Naikkan POLL_INTERVAL, pertahankan MAX_ATTEMPTS longgar

Pertanyaan umum

Apakah kombinasi Puppeteer dan CaptchaAI menambah latensi berarti ke pipeline scraping?

Tidak signifikan untuk sebagian besar kasus. Waktu tambahan yang muncul adalah waktu solve CaptchaAI sendiri, ditambah jeda polling yang Anda atur di POLL_INTERVAL — bukan overhead dari sisi Puppeteer. Job paralel tetap berjalan normal selama alokasi thread mencukupi.

Apakah menjalankan banyak instance Puppeteer paralel berarti saya butuh thread CaptchaAI sebanyak itu juga?

Kurang lebih ya — setiap instance Puppeteer yang sedang menunggu solve memakai satu thread CaptchaAI yang aktif. Kalau job paralel lebih banyak dari alokasi thread di paket Anda, request tambahan akan antre sampai thread yang sedang dipakai selesai; naikkan paket (misalnya dari BASIC ke ADVANCE) kalau antrean mulai jadi bottleneck.

Bisakah CaptchaAI menyelesaikan Cloudflare Turnstile di script Puppeteer yang sama dengan reCAPTCHA v2?

Ya. Kedua fungsi solver — solveRecaptchaV2 dan solveTurnstile — hidup di modul yang sama dan dipanggil sesuai tipe CAPTCHA yang terdeteksi di halaman; cukup ekstrak sitekey dari elemen .cf-turnstile dan kirim dengan method=turnstile.

Apa yang terjadi kalau token reCAPTCHA kedaluwarsa sebelum sempat disuntikkan ke form?

Situs akan menolak submit dan biasanya meminta CAPTCHA baru. Token reCAPTCHA hanya valid dalam rentang waktu singkat setelah diterbitkan, jadi tekan jeda antara respons CaptchaAI dan aksi submit seminimal mungkin — jangan menyisipkan proses lain di antara keduanya.

Bagaimana menangani beberapa CAPTCHA dengan tipe berbeda dalam satu alur scraping?

Deteksi tipe CAPTCHA di tiap halaman terlebih dulu (elemen .g-recaptcha vs .cf-turnstile), lalu panggil fungsi solver yang sesuai. Untuk beberapa CAPTCHA di halaman yang sama, ekstrak tiap sitekey secara terpisah dan jalankan solve-nya paralel dengan Promise.all().

Panduan terkait

Komentar dinonaktifkan untuk artikel ini.