Tutorials

Node.js Playwright + CaptchaAI 完整集成

用 Node.js + Playwright 跑自动化时,最容易卡住的一步往往是验证码。

本教程给出可复用的做法:提取页面 sitekey,交给 CaptchaAI API 识别,再把 token 写回页面提交。

覆盖三类最常见的场景:

  • reCAPTCHA v2 识别与回调触发
  • Cloudflare Turnstile sitekey 提取与写回
  • 图片验证码识别

所有示例仅用于你自有或已授权的 QA、staging 环境,请勿用于未授权的第三方站点。


环境准备与安装

安装 Playwright 与浏览器内核,国内可搭配 npm 镜像加速:

npm install playwright
npx playwright install chromium

浏览器初始化配置

标准初始化:指定 User-Agent、视口和 locale,保证不同机器上行为一致。

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

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

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

封装 CaptchaAI 识别接口

CaptchaAI 调用分两步,后面各类验证码都复用它:

  1. in.php 提交任务,拿到 ID
  2. 每 5 秒轮询 res.php,直到结果就绪
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");
}

用 Playwright 识别 reCAPTCHA v2

流程固定为三步:

  1. 从 DOM 读出 data-sitekey
  2. userrecaptcha 方法提交给 API
  3. token 写回 g-recaptcha-response 并触发回调
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;
}

用 Playwright 识别 Cloudflare Turnstile

Turnstile 的 sitekey 以 0x 开头,常在页面加载后由 JS 注入。

提取与写回顺序:

  1. 先找 .cf-turnstile 元素
  2. 回退到遍历 0x 开头的 data-sitekey
  3. token 写入隐藏字段 cf-turnstile-response
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;
}

自动检测验证码类型并识别

真实任务常遇到不同类型的验证码:国内站点多用极验(GeeTest)滑块,海外站更常见 reCAPTCHA 与 Turnstile。

函数按顺序判断类型:

  • 先判断 reCAPTCHA
  • 再判断 Turnstile
  • 最后回退图片验证码
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;
  }
}

用 Playwright 处理图片验证码

对传统图片验证码:截图元素、转成 base64、用 base64 方法提交,再把结果填进输入框。

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

通过路由拦截提取验证码参数

有些参数(如 GeeTest 的 gtchallenge)不在 DOM 里,而由接口返回。

这时监听网络响应来提取:

  • 监听 response 事件
  • 匹配含 geetest/gt= 的响应,读出 gtchallenge
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;
}

完整的自动化封装类

把上面的能力整合成一个类,几行代码就能跑通登录流程。

loginWithCaptcha 是可复用的入口,它会依次:

  • 打开页面并填写表单
  • 自动检测并识别验证码
  • 写回 token 后提交
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: ["--no-sandbox"],
    });
    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();
}

Playwright 与 Puppeteer 对比

选型参考:Playwright 默认配置更省心,原生支持 TypeScript 与多浏览器。

特性 Playwright Puppeteer
多浏览器支持 Chromium、Firefox、WebKit 仅 Chromium
API 风格 基于 Locator 基于选择器
自动等待 内置 需手动等待
网络拦截 基于路由 基于请求
默认配置 合理的开箱默认 需额外插件
TypeScript 原生支持 社区类型定义

故障排查

常见问题速查:

症状 原因 处理方式
page.evaluate 返回 null 元素尚未加载 先调用 waitForSelector 等待
检测不到 Turnstile 页面加载后才由 JS 注入 等待 .cf-turnstile 出现
写回 token 后表单不提交 没有触发回调 显式调用 reCAPTCHA 回调
浏览器被识别 缺少初始化脚本 补上 webdriver 覆盖脚本
networkidle 超时 页面有长轮询请求 改用 domcontentloaded

常见问题

CaptchaAI 支持哪些 GeeTest 版本?

目前支持 GeeTest(极验)v3,通过 geetest 方法调用。

GeeTest v4 官方标注为即将支持,暂不可用;hCaptcha 与 FunCaptcha 也不支持。

国内测试 reCAPTCHA 加载不出来怎么办?

reCAPTCHA 依赖 Google 托管的脚本,在部分网络下页面可能渲染不全。

好在识别不依赖页面渲染:拿到 sitekey 和 pageurl 提交给 API,token 返回后写回页面即可。

Playwright 并发跑多个任务,应该选哪个套餐?

CaptchaAI 按并发线程计费,每个同时进行的识别任务占用一个线程。

轻量脚本用 BASIC($15/月,5 线程)够用;并发更高可升级到 ADVANCE($90/月,50 线程),按实际并发数选择。

写回 token 后表单还是没提交,是什么原因?

reCAPTCHA 需显式触发回调,仅设 g-recaptcha-response 值不够;Turnstile 要确认 cf-turnstile-response 已赋值。

先检查回调是否被调用,再看提交按钮是否被脚本禁用。

headless 模式会影响识别成功率吗?

不会。识别由 CaptchaAI 在服务端完成,与浏览器是否有界面无关。

launch()headless 设为 true 即可在 CI 等无界面环境运行。


小结

Node.js + Playwright + CaptchaAI 组成一套现代自动化方案:自动检测类型、路由拦截取参、多类验证码统一识别。PlaywrightAutomation 类把「登录 + 验证码」流程封装到一起,复制即可跑通。

相关文章

该文章已禁用评论。