Tutorials

Node.js Playwright + CaptchaAI: Integrasi Lengkap

Artikel ini memberi Anda satu hal: integrasi Node.js Playwright + CaptchaAI yang berjalan penuh dari ujung ke ujung — mendeteksi CAPTCHA di halaman, mengirimnya ke CaptchaAI untuk diselesaikan, lalu menyuntikkan token hasilnya kembali agar form bisa dikirim. Semua kode di bawah bisa Anda salin apa adanya: reCAPTCHA v2, Cloudflare Turnstile, dan CAPTCHA gambar ditangani lewat satu fungsi solver yang sama, dan di bagian akhir ada kelas PlaywrightAutomation yang membungkus alur login-dengan-CAPTCHA menjadi beberapa baris pemanggilan.

Penyelesaian CAPTCHA di sini didelegasikan sepenuhnya ke API CaptchaAI, yang ditagih per thread, bukan per solve. Untuk tim scraping dan monitoring harga yang menjalankan banyak worker Playwright sekaligus, model ini membuat biaya tetap terprediksi berapa pun volume permintaan Anda.

Prasyarat

Pasang Playwright beserta runtime browser Chromium-nya. Satu perintah install sudah cukup untuk menyiapkan semuanya:

npm install playwright
npx playwright install chromium

Menyiapkan browser Playwright dengan konfigurasi standar

Sebelum menyentuh CAPTCHA, siapkan context browser dengan nilai yang wajar: userAgent, viewport, dan locale yang konsisten seperti browser desktop biasa. Init script kecil di bawah menormalkan sinyal browser otomatis agar sesi berjalan stabil — bukan untuk menyembunyikan apa pun, melainkan agar konfigurasi tidak terlihat setengah jadi.

const { chromium } = require("playwright");

async function createBrowser() {
  const browser = await chromium.launch({
    headless: false,
    args: [],
  });

  const context = await browser.newContext({
    userAgent:
      "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
      "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    viewport: { width: 1920, height: 1080 },
    locale: "en-US",
  });

  // Remove Playwright detection
  await context.addInitScript(() => {
    Object.defineProperty(navigator, "webdriver", { get: () => undefined });
    delete navigator.__proto__.webdriver;
  });

  const page = await context.newPage();
  return { browser, context, page };
}

Fungsi solver CaptchaAI

Inti integrasi adalah satu fungsi yang memakai pola empat langkah yang sama untuk semua tipe CAPTCHA:

  1. Kirim task ke in.php beserta method dan parameternya.
  2. Simpan task ID dari respons.
  3. Polling res.php sampai hasilnya siap.
  4. Pakai token yang dikembalikan.

Selama polling, jeda 5 detik antar-percobaan sudah cukup; loop berhenti begitu status bernilai 1 atau saat CaptchaAI mengembalikan ERROR_CAPTCHA_UNSOLVABLE.

const API_KEY = "YOUR_API_KEY";

async function solveCaptcha(method, params) {
  // Submit
  const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
    method: "POST",
    body: new URLSearchParams({ key: API_KEY, method, json: "1", ...params }),
  });
  const submitData = await submitResp.json();
  if (submitData.status !== 1) throw new Error(`Submit: ${submitData.request}`);

  const taskId = submitData.request;

  // Poll
  for (let i = 0; i < 30; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const pollResp = await fetch(
      `https://ocr.captchaai.com/res.php?${new URLSearchParams({
        key: API_KEY,
        action: "get",
        id: taskId,
        json: "1",
      })}`
    );
    const data = await pollResp.json();
    if (data.status === 1) return data.request;
    if (data.request === "ERROR_CAPTCHA_UNSOLVABLE") throw new Error("Unsolvable");
  }
  throw new Error("Timed out");
}

Solve reCAPTCHA v2 di Playwright

Alurnya tiga tahap: baca data-sitekey dari DOM, kirim ke CaptchaAI lewat method userrecaptcha bersama pageurl, lalu tempel token yang kembali ke textarea#g-recaptcha-response. Banyak halaman tidak langsung mengirim form setelah token terisi — karena itu blok terakhir memicu callback reCAPTCHA secara eksplisit agar halaman menganggap verifikasi selesai.

async function solveRecaptchaV2(page) {
  // Extract sitekey
  const sitekey = await page.evaluate(() => {
    const el = document.querySelector("[data-sitekey]");
    return el ? el.getAttribute("data-sitekey") : null;
  });
  if (!sitekey) throw new Error("Sitekey not found");

  // Solve
  const token = await solveCaptcha("userrecaptcha", {
    googlekey: sitekey,
    pageurl: page.url(),
  });

  // Inject
  await page.evaluate((t) => {
    const textarea = document.getElementById("g-recaptcha-response");
    if (textarea) {
      textarea.value = t;
      textarea.style.display = "block";
    }

    // Trigger callback
    if (typeof ___grecaptcha_cfg !== "undefined") {
      const clients = ___grecaptcha_cfg.clients;
      for (const key in clients) {
        for (const prop in clients[key]) {
          try {
            const cb = clients[key][prop];
            if (cb && typeof cb.callback === "function") cb.callback(t);
          } catch {}
        }
      }
    }
  }, token);

  return token;
}

Solve Cloudflare Turnstile di Playwright

Turnstile memakai atribut data-sitekey yang selalu diawali 0x, jadi selain menyasar .cf-turnstile kita sediakan fallback yang memindai semua elemen ber-data-sitekey. Method-nya turnstile, dan token hasilnya masuk ke seluruh field cf-turnstile-response di halaman. Turnstile termasuk tipe yang ditangani CaptchaAI secara konsisten, sehingga worker headless pun tetap mendapat token yang valid.

async function solveTurnstile(page) {
  // Extract sitekey
  const sitekey = await page.evaluate(() => {
    const el = document.querySelector(".cf-turnstile[data-sitekey]");
    if (el) return el.getAttribute("data-sitekey");

    // Fallback: any data-sitekey starting with 0x
    const all = document.querySelectorAll("[data-sitekey]");
    for (const item of all) {
      const key = item.getAttribute("data-sitekey");
      if (key && key.startsWith("0x")) return key;
    }
    return null;
  });
  if (!sitekey) throw new Error("Turnstile sitekey not found");

  // Solve
  const token = await solveCaptcha("turnstile", {
    sitekey,
    pageurl: page.url(),
  });

  // Inject
  await page.evaluate((t) => {
    document
      .querySelectorAll('[name="cf-turnstile-response"]')
      .forEach((el) => (el.value = t));
  }, token);

  return token;
}

Deteksi dan solve CAPTCHA otomatis

Daripada memanggil solver yang berbeda untuk tiap halaman, biarkan satu fungsi memeriksa DOM lalu memilih rutenya sendiri. detectAndSolve() mengenali tiga hal:

  • reCAPTCHA — lewat atribut data-sitekey dan penanda .g-recaptcha.
  • Turnstile — lewat container .cf-turnstile.
  • CAPTCHA gambar — lewat elemen img yang bertanda captcha.

Setelah tipenya dikenali, fungsi mengarahkan ke method CaptchaAI yang tepat. Pola ini cocok untuk crawler yang menghadapi banyak jenis halaman tanpa tahu di muka tipe CAPTCHA apa yang akan muncul.

async function detectAndSolve(page) {
  const captchaInfo = await page.evaluate(() => {
    // Check reCAPTCHA
    const recaptcha = document.querySelector("[data-sitekey]");
    if (
      recaptcha &&
      (document.querySelector(".g-recaptcha") ||
        document.querySelector('script[src*="recaptcha"]'))
    ) {
      return { type: "recaptcha", sitekey: recaptcha.getAttribute("data-sitekey") };
    }

    // Check Turnstile
    const turnstile = document.querySelector(".cf-turnstile[data-sitekey]");
    if (turnstile) {
      return { type: "turnstile", sitekey: turnstile.getAttribute("data-sitekey") };
    }

    // Check image CAPTCHA
    const captchaImg = document.querySelector(
      'img.captcha, img[alt*="captcha"], img[src*="captcha"]'
    );
    if (captchaImg) {
      return { type: "image" };
    }

    return { type: null };
  });

  if (!captchaInfo.type) return null;

  console.log(`Detected: ${captchaInfo.type}`);

  switch (captchaInfo.type) {
    case "recaptcha":
      return await solveCaptcha("userrecaptcha", {
        googlekey: captchaInfo.sitekey,
        pageurl: page.url(),
      });

    case "turnstile":
      return await solveCaptcha("turnstile", {
        sitekey: captchaInfo.sitekey,
        pageurl: page.url(),
      });

    case "image":
      return await solveImageCaptcha(page);

    default:
      return null;
  }
}

Solve CAPTCHA gambar (OCR)

Untuk CAPTCHA gambar klasik, ambil screenshot elemen gambarnya, ubah ke base64, dan kirim ke method base64. Jawaban yang kembali langsung diketikkan ke input teks. Locator Playwright yang built-in memudahkan memilih elemen gambar maupun kolom input tanpa selector rapuh.

async function solveImageCaptcha(page) {
  const captchaImg = page.locator(
    'img.captcha, img[alt*="captcha"], img[src*="captcha"]'
  ).first();

  // Screenshot the CAPTCHA element
  const imgBuffer = await captchaImg.screenshot();
  const imgBase64 = imgBuffer.toString("base64");

  // Solve via CaptchaAI
  const answer = await solveCaptcha("base64", { body: imgBase64 });

  // Type the answer
  const input = page.locator(
    'input[name="captcha"], input[name="code"], input.captcha-input'
  ).first();
  await input.fill(answer);

  return answer;
}

Intersepsi rute untuk parameter GeeTest

Sebagian CAPTCHA menyimpan parameternya di respons jaringan, bukan di DOM. Dengan mendengarkan event response, Anda bisa menangkap nilai gt dan challenge GeeTest saat halaman memuatnya. GeeTest v3 termasuk tipe yang didukung CaptchaAI; GeeTest v4 belum didukung dan hanya berstatus segera hadir.

async function interceptCaptchaRoutes(page, url) {
  const captchaParams = {};

  // Intercept responses
  page.on("response", async (response) => {
    const respUrl = response.url();

    // GeeTest parameters
    if (respUrl.includes("geetest") || respUrl.includes("gt=")) {
      try {
        const data = await response.json();
        if (data.gt) {
          captchaParams.type = "geetest";
          captchaParams.gt = data.gt;
          captchaParams.challenge = data.challenge;
        }
      } catch {}
    }
  });

  await page.goto(url, { waitUntil: "networkidle" });
  return captchaParams;
}

Kelas otomasi lengkap

Semua potongan di atas jadi lebih rapi jika dibungkus dalam satu kelas. PlaywrightAutomation menyimpan browser, context, dan page sebagai field privat, lalu mengekspos loginWithCaptcha() yang menjalankan seluruh alur: buka halaman, isi form, deteksi dan solve CAPTCHA, suntikkan token, kirim.

const { chromium } = require("playwright");

class PlaywrightAutomation {
  #apiKey;
  #browser;
  #context;
  #page;

  constructor(apiKey) {
    this.#apiKey = apiKey;
  }

  async start(headless = false) {
    this.#browser = await chromium.launch({
      headless,
      args: [],
    });
    this.#context = await this.#browser.newContext({
      userAgent:
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36",
      viewport: { width: 1920, height: 1080 },
    });
    await this.#context.addInitScript(() => {
      Object.defineProperty(navigator, "webdriver", { get: () => undefined });
    });
    this.#page = await this.#context.newPage();
  }

  async stop() {
    await this.#browser?.close();
  }

  async navigate(url) {
    await this.#page.goto(url, { waitUntil: "networkidle" });
  }

  async fillForm(fields) {
    for (const [selector, value] of Object.entries(fields)) {
      await this.#page.fill(selector, value);
    }
  }

  async solveCaptcha() {
    return await detectAndSolve(this.#page);
  }

  async submit(selector = 'button[type="submit"]') {
    await this.#page.click(selector);
    await this.#page.waitForLoadState("networkidle");
    return this.#page.url();
  }

  async loginWithCaptcha(url, fields, submitSelector) {
    await this.navigate(url);
    await this.fillForm(fields);

    const token = await this.solveCaptcha();
    if (token) {
      // Inject token
      await this.#page.evaluate((t) => {
        const re = document.getElementById("g-recaptcha-response");
        if (re) re.value = t;
        document
          .querySelectorAll('[name="cf-turnstile-response"]')
          .forEach((el) => (el.value = t));
      }, token);
    }

    return await this.submit(submitSelector);
  }

  get page() {
    return this.#page;
  }
}

// Usage
const bot = new PlaywrightAutomation("YOUR_API_KEY");
await bot.start();

try {
  const result = await bot.loginWithCaptcha(
    "https://staging.example.com/qa-login",
    {
      "#email": "user@example.com",
      "#password": "pass123",
    },
    "#login-btn"
  );
  console.log(`Redirected to: ${result}`);
} finally {
  await bot.stop();
}

Contoh skenario: tim monitoring harga di Jakarta menjalankan puluhan worker Playwright dari region AWS ap-southeast-3 (Jakarta) atau ap-southeast-1 (Singapura). Setiap worker menyewa satu thread CaptchaAI selama sebuah solve berjalan, lalu thread itu bebas dipakai worker berikutnya. Dua hal yang membuat pola ini praktis untuk tim di Indonesia:

  • Biaya terprediksi. Paket BASIC ($15/bulan, 5 thread) cukup untuk uji coba, sedangkan ADVANCE ($90/bulan, 50 thread) menampung produksi — biaya tetap datar berapa pun jumlah CAPTCHA yang diselesaikan.
  • Kepatuhan data. Sesuai UU Pelindungan Data Pribadi (UU 27/2022), olah hanya data yang memang Anda berhak proses dan hindari data pribadi.

Playwright vs Puppeteer

Fitur Playwright Puppeteer
Dukungan multi-browser Chromium, Firefox, WebKit Hanya Chromium
Gaya API Berbasis locator Berbasis selector
Auto-wait Bawaan Wait manual
Intersepsi jaringan Berbasis route Berbasis request
Konfigurasi default Default yang wajar Perlu plugin tambahan
TypeScript Native Tipe dari komunitas

Pemecahan masalah

Gejala Penyebab Solusi
page.evaluate mengembalikan null Elemen belum dimuat Panggil waitForSelector lebih dulu
Turnstile tidak terdeteksi Dimuat lewat JS setelah halaman render Tunggu selector .cf-turnstile
Token disuntikkan tapi form tidak terkirim Callback tidak ikut terpicu Panggil callback reCAPTCHA secara eksplisit
Terdeteksi sebagai otomasi Init script tidak terpasang Tambahkan init script yang menormalkan sinyal browser otomatis
networkidle timeout Skrip long-polling di halaman Pakai domcontentloaded sebagai gantinya

Pertanyaan yang sering diajukan

Apakah hCaptcha dan FunCaptcha tersedia di CaptchaAI?

Belum. hCaptcha dan FunCaptcha (Arkose Labs) tidak didukung saat ini, jadi kode di atas tidak menyediakan rute untuk keduanya. CaptchaAI menangani reCAPTCHA v2/v3, Cloudflare Turnstile dan Challenge, GeeTest v3, serta CAPTCHA gambar/OCR.

Berapa thread yang saya butuhkan untuk menjalankan banyak instance Playwright sekaligus?

Satu thread menangani satu solve yang sedang berjalan. Jika Anda menjalankan 20 worker paralel dan tiap worker menghadapi satu CAPTCHA dalam satu waktu, sekitar 20 thread sudah memadai — misalnya paket ADVANCE ($90/bulan, 50 thread) memberi ruang lebih. Karena penagihan per thread, jumlah solve di dalam bulan itu tidak menambah biaya.

Apakah detectAndSolve() bisa menangani reCAPTCHA v3 dan GeeTest v3?

Fungsi contoh ini fokus ke reCAPTCHA v2, Turnstile, dan gambar. reCAPTCHA v3 tetap didukung lewat method userrecaptcha dengan parameter action dan skor, sedangkan GeeTest v3 diambil parameternya melalui fungsi intersepsi rute. GeeTest v4 belum tersedia dan hanya berstatus segera hadir.

Perlukah menjalankan Playwright dengan GUI, atau bisa headless di server?

Bisa headless. Setel headless: true di launch() saat men-deploy ke server tanpa layar. Karena penyelesaian CAPTCHA berjalan di sisi CaptchaAI, mode headless tidak menurunkan tingkat keberhasilan solve.

Ringkasan

Node.js Playwright + CaptchaAI memberi Anda tumpukan otomasi modern: deteksi otomatis, intersepsi rute, dan dukungan banyak tipe CAPTCHA dalam satu integrasi. Kelas PlaywrightAutomation merangkum seluruh alur login-dengan-CAPTCHA sehingga siap dipakai worker produksi Anda.

Artikel Terkait

Komentar dinonaktifkan untuk artikel ini.