Hướng Dẫn Thực Hành

Giải CAPTCHA trong Playwright Node.js với CaptchaAI

Khi tự động hóa một trang có CAPTCHA bằng Playwright cho Node.js, bạn không cần tự giải thử thách — CaptchaAI làm phần đó qua API, còn Playwright lo việc điều khiển trình duyệt. Luồng tích hợp gói gọn trong bốn bước: trích xuất sitekey từ DOM, gửi task tới CaptchaAI, polling để nhận token, rồi chèn token vào form và submit.

Bài này dựng sẵn các hàm cho reCAPTCHA v2, Cloudflare Turnstile và CAPTCHA ảnh, kèm một lớp PlaywrightAutomation xử lý trọn quy trình đăng nhập có CAPTCHA. Định hướng là code chạy được ngay, dành cho dev đang cần một tích hợp thật chứ không phải bản tổng quan lý thuyết.

Điều kiện cần chuẩn bị

Cài Playwright và bản Chromium đi kèm:

npm install playwright
npx playwright install chromium

Thiết lập trình duyệt cho môi trường kiểm thử

Tạo một trình duyệt với user-agent, viewport và locale cố định để môi trường QA ổn định giữa các lần chạy:

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 };
}

Hàm này trả về browser, contextpage để các bước sau dùng lại.

Hàm gọi CaptchaAI: gửi task và polling kết quả

Đây là hàm dùng chung cho mọi loại CAPTCHA. Nó gửi task tới endpoint in.php, rồi polling res.php mỗi 5 giây cho đến khi có token:

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");
}

Chỉ cần thay YOUR_API_KEY bằng API key thật của bạn; mọi hàm phía dưới đều gọi lại solveCaptcha().

Giải reCAPTCHA v2 trong Playwright

Với reCAPTCHA v2, bạn đọc data-sitekey từ DOM, gửi task với method userrecaptcha, rồi gán token vào ô g-recaptcha-response và kích hoạt callback:

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;
}

Bước kích hoạt callback quan trọng: nhiều form chỉ mở nút submit sau khi callback của reCAPTCHA chạy.

Giải Cloudflare Turnstile trong Playwright

Turnstile là loại được hỗ trợ chính thức. Sitekey của Turnstile luôn bắt đầu bằng 0x, nên hàm có thêm bước dự phòng để bắt đúng phần tử:

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;
}

Token trả về được gán vào mọi ô ẩn cf-turnstile-response trên trang.

Tự động phát hiện và giải CAPTCHA

Thay vì viết riêng cho từng loại, bạn có thể để một hàm quét DOM và tự chọn cách giải. Hàm dưới đây kiểm tra lần lượt reCAPTCHA, Turnstile rồi CAPTCHA ảnh:

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;
  }
}

Nếu trang không có CAPTCHA nào khớp, hàm trả về null để bạn xử lý tiếp.

Giải CAPTCHA ảnh trong Playwright

Với CAPTCHA ảnh, bạn chụp riêng phần tử ảnh, mã hóa base64, gửi tới CaptchaAI với method base64 rồi điền kết quả vào ô nhập:

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;
}

Chụp đúng phần tử ảnh (thay vì toàn trang) giúp CaptchaAI nhận được ảnh sạch, không bị nhiễu bởi phần còn lại của giao diện.

Chặn request để lấy tham số CAPTCHA

Một số loại như GeeTest nạp tham số động qua request mạng. Bạn có thể lắng nghe response để bắt các giá trị này ngay khi trang tải:

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;
}

Cách này hữu ích khi sitekey hoặc challenge không nằm sẵn trong HTML tĩnh.

Lớp tự động hóa hoàn chỉnh

Đoạn code dưới đây gom mọi thứ vào một lớp duy nhất. Hình dung một tình huống quen thuộc: team QA tại một công ty outsourcing ở TP.HCM cần chạy hồi quy đăng nhập mỗi đêm trên môi trường staging có Cloudflare Turnstile. Thay vì bấm tay từng lần, họ gọi loginWithCaptcha() cho từng tài khoản kiểm thử và để pipeline tự lo phần giải CAPTCHA:

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();
}

Lớp này điều hướng, điền form, tự phát hiện và giải CAPTCHA rồi submit — chỉ trong một lời gọi loginWithCaptcha().

Playwright hay Puppeteer? Bảng so sánh

Cả hai đều điều khiển Chromium, nhưng Playwright có lợi thế rõ khi dự án cần đa trình duyệt và ít code chờ thủ công:

Tính năng Playwright Puppeteer
Đa trình duyệt Chromium, Firefox, WebKit Chỉ Chromium
Kiểu API Dựa trên locator Dựa trên selector
Tự động chờ Tích hợp sẵn Phải chờ thủ công
Chặn request mạng Theo route Theo request
Cấu hình QA mặc định Tốt sẵn Cần plugin bổ sung
TypeScript Hỗ trợ gốc Kiểu từ cộng đồng

Xử lý sự cố thường gặp

Triệu chứng Nguyên nhân Cách xử lý
page.evaluate trả về null Phần tử chưa được tải Dùng waitForSelector trước
Không phát hiện được Cloudflare Turnstile Nạp bằng JS sau khi trang đã tải Chờ selector .cf-turnstile
Chèn token xong nhưng form không submit Thiếu bước kích hoạt callback Gọi callback của reCAPTCHA một cách tường minh
Trình duyệt bị nhận diện là tự động hóa Thiếu init script Thêm đoạn ghi đè navigator.webdriver
networkidle bị timeout Trang có script polling chạy dài Dùng domcontentloaded thay thế

Câu hỏi thường gặp

CaptchaAI có giải được Cloudflare Turnstile trong Playwright không?

Có. Turnstile là loại được hỗ trợ chính thức (GA). Bạn đọc data-sitekey từ phần tử .cf-turnstile, gửi task với method turnstile, nhận token rồi gán vào ô ẩn cf-turnstile-response. Ngoài Turnstile, CaptchaAI còn giải reCAPTCHA v2/v3, GeeTest v3, Cloudflare Challenge và CAPTCHA ảnh/OCR.

Chạy Playwright ở chế độ headless có làm giảm tỷ lệ giải không?

Không. Đặt headless: true trong launch() là đủ. CaptchaAI giải phía máy chủ và trả về token độc lập với việc trình duyệt có giao diện hay không, nên chế độ headless không ảnh hưởng đến kết quả giải.

Chi phí giải CAPTCHA với CaptchaAI được tính thế nào?

CaptchaAI tính tiền theo thread (luồng giải đồng thời), không tính theo từng lần giải. Mỗi thread giải không giới hạn số CAPTCHA trong tháng. Gói nhỏ nhất là BASIC ($15/tháng, 5 thread); nếu chạy nhiều task song song trong CI, cân nhắc ADVANCE ($90/tháng, 50 thread) hoặc cao hơn.

detectAndSolve() tự nhận diện loại CAPTCHA dựa vào đâu?

Hàm quét DOM theo thứ tự reCAPTCHA → Turnstile → CAPTCHA ảnh và trả về loại đầu tiên khớp selector. Nếu một loại được nạp động bằng JavaScript sau khi trang tải, hãy await page.waitForSelector(...) cho đúng phần tử trước khi gọi, nếu không hàm có thể trả về null.

Tóm tắt

Ghép Playwright cho Node.js với CaptchaAI, bạn có một stack tự động hóa hiện đại: tự phát hiện loại CAPTCHA, chặn request để lấy tham số, và giải nhiều loại trong cùng một luồng. Lớp PlaywrightAutomation gói trọn quy trình đăng nhập có CAPTCHA, từ điền form đến chèn token và submit.

Bài viết liên quan

Os comentários estão desativados para este artigo.