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, context và page để 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.