Khắc Phục Sự Cố

Xử lý reCAPTCHA v2 và Cloudflare Turnstile trên cùng một trang web

Cùng một script, cùng API key, số dư vẫn còn: trang đăng nhập qua CAPTCHA bình thường, còn trang thanh toán thì token gửi lên bị từ chối? Khả năng cao site dùng hai provider CAPTCHA khác nhau. Cách xử lý: bỏ hardcode, đọc HTML tìm marker g-recaptcha hay cf-turnstile, map sang đúng method của CaptchaAI (userrecaptcha hoặc turnstile), rồi đặt token vào đúng field.

Vì sao một site chạy cả reCAPTCHA v2 lẫn Turnstile

Không phải lỗi cấu hình mà là lựa chọn có chủ đích:

Tình huống Biểu hiện trên site
Mỗi trang một provider Đăng nhập reCAPTCHA, thanh toán Turnstile
Đang A/B test provider Cùng một URL, mỗi lần tải lại ra một loại
Đang migrate dở Trang cũ còn reCAPTCHA, trang mới đã sang Turnstile
Cơ chế dự phòng Provider chính lỗi → site chuyển sang provider phụ
Phân biệt theo khu vực reCAPTCHA cho khách Mỹ, Turnstile cho EU vì GDPR

Điểm chung: provider có thể đổi giữa hai lần request, nên phải phát hiện lại mỗi lần.

Bảng tra marker: phân biệt reCAPTCHA v2 và Turnstile trong HTML

Provider Marker trong HTML URL script Field chứa token
reCAPTCHA v2 class="g-recaptcha" google.com/recaptcha/api.js g-recaptcha-response
Cloudflare Turnstile class="cf-turnstile" challenges.cloudflare.com/turnstile cf-turnstile-response
hCaptcha class="h-captcha" js.hcaptcha.com/1/api.js h-captcha-response

Cột cuối hay sai nhất: token hợp lệ nhưng đặt sai field vẫn bị từ chối. CaptchaAI không hỗ trợ hCaptcha, dòng đó chỉ để nhận diện.

Python: phát hiện reCAPTCHA v2 hay Turnstile rồi giải

Code dưới đây xác định provider từ HTML, gửi task tới in.php với method tương ứng, rồi polling res.php tới khi có token. detect_captcha_type() ưu tiên class riêng của provider thay vì chỉ bắt data-sitekey, vì thuộc tính này có ở cả hai loại.

import requests
import time
import re
from dataclasses import dataclass

API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"

@dataclass
class CaptchaInfo:
    provider: str     # "recaptcha" or "turnstile"
    method: str       # API method name
    sitekey: str
    pageurl: str
    response_field: str  # Form field name for the token

def detect_captcha_type(html, pageurl):
    """
    Detect which CAPTCHA provider is on the page.
    Returns CaptchaInfo or None.
    """
    # Check for Turnstile
    turnstile_match = re.search(
        r'class=["\'][^"\']*cf-turnstile[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
        html,
    )
    if not turnstile_match:
        turnstile_match = re.search(
            r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*cf-turnstile',
            html,
        )

    if turnstile_match:
        return CaptchaInfo(
            provider="turnstile",
            method="turnstile",
            sitekey=turnstile_match.group(1),
            pageurl=pageurl,
            response_field="cf-turnstile-response",
        )

    # Check for reCAPTCHA
    recaptcha_match = re.search(
        r'class=["\'][^"\']*g-recaptcha[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
        html,
    )
    if not recaptcha_match:
        recaptcha_match = re.search(
            r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*g-recaptcha',
            html,
        )

    # Also check for script-rendered reCAPTCHA
    if not recaptcha_match:
        recaptcha_match = re.search(
            r'grecaptcha\.render\([^,]+,\s*\{[^}]*["\']sitekey["\']\s*:\s*["\']([^"\']+)["\']',
            html,
        )

    if recaptcha_match:
        return CaptchaInfo(
            provider="recaptcha",
            method="userrecaptcha",
            sitekey=recaptcha_match.group(1),
            pageurl=pageurl,
            response_field="g-recaptcha-response",
        )

    return None

def solve_captcha(info):
    """Solve any detected CAPTCHA type via CaptchaAI."""
    params = {
        "key": API_KEY,
        "method": info.method,
        "json": 1,
    }

    if info.method == "userrecaptcha":
        params["googlekey"] = info.sitekey
        params["pageurl"] = info.pageurl
    elif info.method == "turnstile":
        params["sitekey"] = info.sitekey
        params["pageurl"] = info.pageurl

    resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
    if resp.get("status") != 1:
        raise RuntimeError(f"Submit failed: {resp.get('request')}")

    task_id = resp["request"]
    for _ in range(60):
        time.sleep(5)
        poll = requests.get(RESULT_URL, params={
            "key": API_KEY, "action": "get",
            "id": task_id, "json": 1,
        }, timeout=15).json()

        if poll.get("request") == "CAPCHA_NOT_READY":
            continue
        if poll.get("status") == 1:
            return poll["request"]
        raise RuntimeError(f"Solve failed: {poll.get('request')}")

    raise RuntimeError("Timeout")

def process_page(session, url):
    """Fetch page, detect CAPTCHA type, solve, and return form-ready data."""
    response = session.get(url)
    captcha_info = detect_captcha_type(response.text, url)

    if not captcha_info:
        print(f"No CAPTCHA detected on {url}")
        return None

    print(f"Detected {captcha_info.provider} on {url}")
    print(f"  Sitekey: {captcha_info.sitekey[:30]}...")

    token = solve_captcha(captcha_info)
    print(f"  Solved: {token[:30]}...")

    return {
        "provider": captcha_info.provider,
        "response_field": captcha_info.response_field,
        "token": token,
    }

# Usage: Handle multiple pages with different providers
session = requests.Session()

pages = [
    "https://staging.example.com/qa-login",      # Might have reCAPTCHA
    "https://example.com/checkout",   # Might have Turnstile
]

for url in pages:
    result = process_page(session, url)
    if result:
        form_data = {result["response_field"]: result["token"]}
        # Add other form fields...
        # session.post(url, data=form_data)

Kết quả kèm sẵn response_field, nên bước cuối chỉ là ghép token vào form data.

JavaScript: phát hiện CAPTCHA động trong pipeline Node.js

Cùng logic cho Node.js: ba regex chạy theo thứ tự Turnstile → reCAPTCHA trong HTML → reCAPTCHA render bằng script.

const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";

function detectCaptchaType(html, pageurl) {
  // Turnstile
  const turnstileMatch = html.match(/cf-turnstile[^>]*data-sitekey=["']([^"']+)["']/);
  if (turnstileMatch) {
    return { provider: "turnstile", method: "turnstile", sitekey: turnstileMatch[1], pageurl, field: "cf-turnstile-response" };
  }

  // reCAPTCHA
  const recaptchaMatch = html.match(/g-recaptcha[^>]*data-sitekey=["']([^"']+)["']/);
  if (recaptchaMatch) {
    return { provider: "recaptcha", method: "userrecaptcha", sitekey: recaptchaMatch[1], pageurl, field: "g-recaptcha-response" };
  }

  // Script-rendered reCAPTCHA
  const scriptMatch = html.match(/sitekey["']\s*:\s*["']([^"']+)["']/);
  if (scriptMatch) {
    return { provider: "recaptcha", method: "userrecaptcha", sitekey: scriptMatch[1], pageurl, field: "g-recaptcha-response" };
  }

  return null;
}

async function solveCaptcha(info) {
  const body = new URLSearchParams({ key: API_KEY, method: info.method, json: "1" });
  if (info.method === "userrecaptcha") { body.set("googlekey", info.sitekey); body.set("pageurl", info.pageurl); }
  else if (info.method === "turnstile") { body.set("sitekey", info.sitekey); body.set("pageurl", info.pageurl); }

  const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
  if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);

  const taskId = resp.request;
  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
    const poll = await (await fetch(url)).json();
    if (poll.request === "CAPCHA_NOT_READY") continue;
    if (poll.status === 1) return poll.request;
    throw new Error(`Solve: ${poll.request}`);
  }
  throw new Error("Timeout");
}

async function processPage(url) {
  const response = await fetch(url);
  const html = await response.text();
  const info = detectCaptchaType(html, url);

  if (!info) { console.log(`No CAPTCHA on ${url}`); return null; }
  console.log(`${info.provider} detected on ${url}`);

  const token = await solveCaptcha(info);
  return { provider: info.provider, field: info.field, token };
}

// Usage
const pages = ["https://staging.example.com/qa-login", "https://example.com/checkout"];
for (const url of pages) {
  const result = await processPage(url);
  if (result) {
    console.log(`Solved ${result.provider}: ${result.token.substring(0, 30)}...`);
  }
}

Với Playwright hoặc Puppeteer, thay fetch(url) bằng page.content() sau khi trang load xong: widget render bằng JavaScript chỉ có trong DOM.

Ví dụ: reCAPTCHA v2 ở đăng nhập, Turnstile ở thanh toán

Một team QA thuê ngoài ở TP.HCM bảo trì site bán lẻ: luồng đăng nhập trên staging còn reCAPTCHA v2, luồng thanh toán mới đã sang Turnstile. Suite regression hằng đêm hardcode method=userrecaptcha nên mọi test case thanh toán fail vì token bị từ chối. Chuyển sang phát hiện động, một worker chạy được cả hai luồng.

Chi phí không đổi: CaptchaAI tính theo thread (số CAPTCHA giải đồng thời), không theo lần giải. BASIC ($15/tháng, 5 thread) đủ cho suite chạy tuần tự ban đêm; chạy nhiều môi trường staging song song thì lên STANDARD ($30/tháng, 15 thread), giá tính bằng USD.

Lỗi thường gặp khi hai provider cùng tồn tại

Vấn đề Nguyên nhân Cách xử lý
Phát hiện nhầm loại CAPTCHA Regex khớp phần tử khác Ưu tiên class riêng của provider, không chỉ data-sitekey
Token đúng nhưng bị từ chối Đặt token sai field g-recaptcha-response cho reCAPTCHA, cf-turnstile-response cho Turnstile
CAPTCHA đổi giữa các lần truy cập Site A/B test hoặc chọn theo khu vực Phát hiện lại mỗi lần tải trang, không cache provider
Thấy cả hai provider trên một trang Một widget đang ẩn Chỉ giải widget hiển thị và có data-sitekey khác rỗng
Không tìm thấy marker nào CAPTCHA render bằng script Tìm grecaptcha.render() / turnstile.render(), hoặc đọc DOM sau khi load

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

Làm sao biết trang dùng reCAPTCHA v2 hay Turnstile?

Tải HTML bằng curl rồi grep g-recaptchacf-turnstile. Không thấy chuỗi nào thì widget render bằng JavaScript, cần trình duyệt headless để đọc DOM.

CaptchaAI có hỗ trợ hCaptcha không?

Không. CaptchaAI giải reCAPTCHA v2/v3 (bao gồm reCAPTCHA Enterprise), Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, CAPTCHA ảnh/OCR, grid-image và BLS; CaptchaFox, Friendly Captcha, Lemin ở giai đoạn beta. hCaptcha và FunCaptcha không hỗ trợ, GeeTest v4 sắp ra mắt.

Site dùng hai loại CAPTCHA thì có tốn gấp đôi thread không?

Không. Thread là số CAPTCHA đang giải đồng thời, không phân biệt loại, và mỗi gói không giới hạn số lần giải trong tháng. Số thread cần dùng phụ thuộc mức độ song song của pipeline.

Nên cache kết quả phát hiện provider hay kiểm tra lại mỗi lần?

Kiểm tra lại mỗi lần tải trang. Cache theo domain sẽ hỏng ngay khi site A/B test hoặc đổi provider giữa phiên; một lần chạy regex rẻ hơn một task giải bỏ phí.

Bài viết liên quan

Bước tiếp theo

Lấy API key CaptchaAI và cho worker tự nhận diện provider trước khi gửi task.

Hướng dẫn liên quan:

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