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-recaptcha và cf-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
- Cách giải reCAPTCHA v2 dạng callback bằng API
- Phân biệt Cloudflare Challenge và Turnstile
- So sánh GeeTest và Cloudflare Turnstile
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: