Use Cases

无头浏览器验证码问题和解决方案

Puppeteer 或 Selenium 脚本本地跑得好好的,一换成无头模式就频繁弹验证码——这是无头浏览器爬虫工程师最常遇到的场景之一。原因很直接:无头 Chrome、Firefox、WebKit 在 JavaScript 层面暴露一批和真实浏览器不一样的信号,reCAPTCHA、Cloudflare、DataDome 靠这些信号打分,分低了就甩验证码。下面按优先级给出三套方案:API 识别、降低出现频率、单独处理 Cloudflare 验证流程。

无头浏览器为什么更容易触发验证码

无头浏览器不是被"发现是无头",而是被"发现和真实用户不一样"——修完一个信号,下一个又会暴露。按修复成本从低到高排列:

  1. navigator.webdriver:无头模式下固定为 true,最容易被抓也最容易修——加一条 excludeSwitches 或劫持该属性即可。
  2. 窗口尺寸 / 视口:默认常是 800x600;改成 1920x1080 或随机化,一行代码搞定。
  3. User-Agent 字符串:携带 "HeadlessChrome" 特征子串,需配合标准配置插件一起改写。
  4. WebGL 渲染器:Puppeteer 尤其明显,常回退到 "SwiftShader",靠插件难完全掩盖。
  5. 浏览器插件 / Permissions API:缺 PDF 查看器等常见插件,权限查询结果和真实浏览器不一致,修复成本最高。
  6. Chrome DevTools Protocol:调试端口保持开放,是 Cloudflare、DataDome 重点检测信号之一。
  7. Playwright 的 WebKit 通道:比 Chromium 通道更易暴露渲染差异,条件允许优先用带标准补丁的 Chromium 通道。

方案一:验证码出现时,直接用 API 识别

与其让无头浏览器"看起来像"真实浏览器,不如换个思路:验证码该出现就让它出现,出现后用 API 在服务端识别掉。CaptchaAI 不依赖浏览器是否被判定为无头。API 识别是兜底方案——顺手降低出现频率还能再省一笔识别成本,下面几个动作照做即可。

Selenium(Python)

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import requests
import time

API_KEY = "YOUR_API_KEY"

options = Options()
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
driver = webdriver.Chrome(options=options)

driver.get("https://staging.example.com/qa-login")

# Check for CAPTCHA
recaptcha = driver.find_elements("class name", "g-recaptcha")
if recaptcha:
    site_key = recaptcha[0].get_attribute("data-sitekey")

    # Solve via CaptchaAI
    resp = requests.get("https://ocr.captchaai.com/in.php", params={
        "key": API_KEY, "method": "userrecaptcha",
        "googlekey": site_key, "pageurl": driver.current_url
    })
    task_id = resp.text.split("|")[1]

    while True:
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": task_id
        })
        if result.text == "CAPCHA_NOT_READY": continue
        token = result.text.split("|")[1]
        break

    # Inject token
    driver.execute_script(
        f"document.getElementById('g-recaptcha-response').innerHTML = '{token}';"
    )
    driver.find_element("css selector", "form").submit()

Puppeteer(Node.js)

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

const API_KEY = "YOUR_API_KEY";

const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
await page.goto("https://staging.example.com/qa-login");

// Check for CAPTCHA
const siteKey = await page
  .$eval(".g-recaptcha", (el) => el.getAttribute("data-sitekey"))
  .catch(() => null);

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

  let token;
  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;
    token = result.data.split("|")[1];
    break;
  }

  await page.evaluate((t) => {
    document.getElementById("g-recaptcha-response").innerHTML = t;
  }, token);
  await page.click('button[type="submit"]');
}

方案二:降低验证码出现的频率

# Selenium
options.add_argument("--no-sandbox")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
// Puppeteer
await page.evaluateOnNewDocument(() => {
  Object.defineProperty(navigator, "webdriver", { get: () => false });
});

设置贴近真实用户的窗口尺寸

# Selenium
driver.set_window_size(1920, 1080)
// Puppeteer
await page.setViewport({ width: 1920, height: 1080 });

引入现成的标准配置插件

# Puppeteer
npm install puppeteer-extra puppeteer-extra-plugin-stealth
const puppeteer = require("puppeteer-extra");
const StealthPlugin = require("puppeteer-extra-plugin-stealth");
puppeteer.use(StealthPlugin());
# Selenium
pip install undetected-chromedriver
import undetected_chromedriver as uc
driver = uc.Chrome(headless=True)
# CaptchaAI handles full Cloudflare 验证流程s
resp = requests.get("https://ocr.captchaai.com/in.php", params={
    "key": API_KEY,
    "method": "cloudflare_challenge",
    "pageurl": "https://example.com",
    "proxy": "http://user:pass@proxy:port",
    "proxytype": "HTTP"
})
task_id = resp.text.split("|")[1]

# Result includes qa_session_cookie cookie and user_agent
# Use both to make subsequent requests

常见问题

无头浏览器一定会被识别出来吗?

不一定,做好标准配置调整后大部分网站不会特意标记。但 Cloudflare、PerimeterX 这类系统仍能通过更细粒度的特征采集识别出无头环境。

Playwright 的检测方式和 Selenium/Puppeteer 一样吗?

思路一致,细节不同:WebKit 通道更容易暴露和桌面版不一致的渲染特征。遇到验证码同样是拿 sitekey 调 CaptchaAI API 识别,再把 token 注回页面。

  • Headless 和 headed 模式该怎么选? Headed 更不易被识别,但服务器上得配 Xvfb 之类的虚拟显示服务;不管选哪种,CaptchaAI 的 API 识别流程都不受影响。
  • 目标站点用的是 GeeTest,无头脚本要注意什么? 国内站点更常见 GeeTest(极验)滑块而非 reCAPTCHA,CaptchaAI 支持 GeeTest v3,流程一致,只是参数换成 gtchallenge 等字段。

相关指南

该文章已禁用评论。