Script giải reCAPTCHA v2 của bạn chạy tốt trên một form, nhưng sang form khác thì server luôn từ chối dù CaptchaAI vẫn trả token về bình thường. Đó gần như luôn là dấu hiệu của biến thể Invisible. Hai biến thể dùng chung định dạng sitekey và cùng trả token vào trường g-recaptcha-response; khác biệt nằm ở hai chỗ: khi gửi task phải thêm invisible=1, và khi có token bạn gần như luôn phải gọi hàm callback thay vì chỉ gán vào trường ẩn.
Phần dưới đi theo đúng thứ tự thao tác khi tích hợp, kết lại bằng một solver dùng chung.
Bốn bước tích hợp và chỗ hai biến thể tách nhau
- Đọc HTML để xác định biến thể — bước này quyết định mọi thứ phía sau.
- Gửi task tới
in.php, thêminvisible=1nếu là biến thể ẩn. - Polling
res.phptới khi nhậnOK|kèm token. - Chèn token: checkbox chỉ cần gán trường ẩn, Invisible còn phải gọi callback.
reCAPTCHA v2 checkbox và Invisible hiện ra thế nào trong HTML
Dấu hiệu nằm ở các thuộc tính đi kèm phần tử g-recaptcha.
Biến thể checkbox
<!-- Standard checkbox widget -->
<div class="g-recaptcha"
data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
data-callback="onSubmit">
</div>
<!-- Widget renders as: -->
<!-- [✓] I'm not a robot reCAPTCHA logo -->
- Người dùng thấy và click trực tiếp vào ô "I'm not a robot".
- Khi Google nghi ngờ, lưới hình ảnh hiện ngay dưới widget.
Biến thể Invisible
<!-- Pattern 1: Invisible widget on a button -->
<button class="g-recaptcha"
data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
data-callback="onSubmit"
data-size="invisible">
Submit
</button>
<!-- Pattern 2: Invisible div (programmatic trigger) -->
<div class="g-recaptcha"
data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
data-size="invisible"
data-callback="onSubmit">
</div>
<!-- Pattern 3: Programmatic render -->
<script>
grecaptcha.render('submit-btn', {
sitekey: '6Le-wvkSAAAAAPBMRTvw...',
callback: onSubmit,
size: 'invisible'
});
</script>
Không có gì hiển thị trên trang. reCAPTCHA chỉ chạy khi người dùng chạm vào phần tử đã gắn, hoặc khi code gọi grecaptcha.execute().
Bảng so sánh reCAPTCHA v2 checkbox và Invisible
Các khác biệt còn lại đều là hệ quả của việc có widget hay không.
| Tiêu chí | v2 checkbox | v2 Invisible |
|---|---|---|
| Widget hiển thị | Có — ô "I'm not a robot" | Không |
| Cách kích hoạt | Người dùng click vào checkbox | Click nút, submit form hoặc tải trang |
| Thử thách hình ảnh | Hiện inline khi Google nghi ngờ bot | Popup góc dưới bên phải |
| Cách nhúng | Div g-recaptcha trên trang |
data-callback trên nút hoặc grecaptcha.execute() |
| Trường token | g-recaptcha-response |
g-recaptcha-response |
| Hàm callback | Tùy chọn | Gần như luôn bắt buộc |
| Tham số CaptchaAI | method=userrecaptcha |
method=userrecaptcha + invisible=1 |
| Hạn dùng của token | 120 giây | 120 giây |
| Thời gian giải qua CaptchaAI | <60 giây | <30 giây |
Dòng 120 giây hay bị hiểu nhầm: token hết hạn tính từ lúc Google phát hành, không phải từ lúc res.php trả về.
Phát hiện biến thể bằng code trước khi gửi task
Viết cứng một biến thể là nguyên nhân phổ biến nhất khiến bộ test gãy khi trang đổi cấu hình.
import requests
from bs4 import BeautifulSoup
import re
def detect_recaptcha_variant(url):
resp = requests.get(url)
soup = BeautifulSoup(resp.text, "html.parser")
# Check for invisible indicators
invisible_widget = soup.find(attrs={"data-size": "invisible", "class": "g-recaptcha"})
if invisible_widget:
return {
"variant": "invisible",
"sitekey": invisible_widget.get("data-sitekey"),
"callback": invisible_widget.get("data-callback")
}
# Check for programmatic invisible in scripts
for script in soup.find_all("script"):
if script.string and "invisible" in str(script.string):
key_match = re.search(r"sitekey['\"]?\s*[:=]\s*['\"]([^'\"]+)", script.string)
if key_match:
return {
"variant": "invisible-programmatic",
"sitekey": key_match.group(1),
"callback": "check grecaptcha.render() call"
}
# Check for standard checkbox
checkbox_widget = soup.find(class_="g-recaptcha")
if checkbox_widget:
return {
"variant": "checkbox",
"sitekey": checkbox_widget.get("data-sitekey"),
"callback": checkbox_widget.get("data-callback")
}
return None
result = detect_recaptcha_variant("https://staging.example.com/qa-login")
print(result)
- Hàm trả về cả sitekey lẫn tên callback, nên bước chèn token không phải đọc lại DOM.
- Nhánh
invisible-programmaticlo trường hợp widget render bằng script.
const axios = require("axios");
const cheerio = require("cheerio");
async function detectRecaptchaVariant(url) {
const { data } = await axios.get(url);
const $ = cheerio.load(data);
// Check for invisible
const invisible = $(".g-recaptcha[data-size='invisible']");
if (invisible.length) {
return {
variant: "invisible",
sitekey: invisible.attr("data-sitekey"),
callback: invisible.attr("data-callback"),
};
}
// Check scripts for programmatic invisible
const scripts = $("script")
.map((_, el) => $(el).html())
.get()
.join("\n");
if (scripts.includes("invisible")) {
const keyMatch = scripts.match(/sitekey['"]?\s*[:=]\s*['"]([^'"]+)/);
if (keyMatch) {
return {
variant: "invisible-programmatic",
sitekey: keyMatch[1],
callback: "check render call",
};
}
}
// Check for standard checkbox
const checkbox = $(".g-recaptcha");
if (checkbox.length) {
return {
variant: "checkbox",
sitekey: checkbox.attr("data-sitekey"),
callback: checkbox.attr("data-callback"),
};
}
return null;
}
Khi debug thủ công, mở DevTools và chạy một dòng là đủ:
const el = document.querySelector('[data-size="invisible"]');
console.log(el ? "Invisible reCAPTCHA" : "Checkbox reCAPTCHA");
| Tín hiệu | v2 checkbox | v2 Invisible |
|---|---|---|
Thuộc tính data-size="invisible" |
Không có | Có |
| Checkbox nhìn thấy được trên trang | Có | Không |
Lời gọi grecaptcha.execute() |
Không (chờ người dùng click) | Có (chạy bằng code) |
| Vị trí popup thử thách | Inline, ngay dưới checkbox | Góc dưới bên phải màn hình |
Gửi task tới CaptchaAI: chỉ khác đúng một tham số
Hai đoạn dưới giống nhau từng dòng, trừ một tham số.
Với biến thể checkbox
import requests
import time
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": "6Le-wvkSAAAA...",
"pageurl": "https://example.com/form"
})
task_id = resp.text.split("|")[1]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY", "action": "get", "id": task_id
})
if result.text.startswith("OK|"):
token = result.text.split("|")[1]
break
Với biến thể Invisible
import requests
import time
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": "6Le-wvkSAAAA...",
"pageurl": "https://example.com/form",
"invisible": 1 # Only parameter difference
})
task_id = resp.text.split("|")[1]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY", "action": "get", "id": task_id
})
if result.text.startswith("OK|"):
token = result.text.split("|")[1]
break
Khác biệt duy nhất là "invisible": 1. Phần còn lại của luồng, gồm cả cách xử lý mã lỗi từ res.php, có trong hướng dẫn giải reCAPTCHA v2 bằng API.
Chèn token vào trang — chỗ dễ sai nhất
Đây là nơi hai biến thể thực sự tách nhau, và cũng tốn thời gian debug nhất.
checkbox: gán vào trường ẩn thường là đủ
# Selenium — inject into hidden field
driver.execute_script(
f'document.getElementById("g-recaptcha-response").value = "{token}";'
)
# If the page uses a callback, also call it
callback = driver.find_element("css selector", ".g-recaptcha").get_attribute("data-callback")
if callback:
driver.execute_script(f'{callback}("{token}");')
Cú click của người dùng đã đăng ký sẵn callback, nên form chấp nhận token ngay.
Invisible: gần như luôn phải gọi callback
# Selenium — inject AND call the callback
driver.execute_script(
f'document.getElementById("g-recaptcha-response").value = "{token}";'
)
# CRITICAL: Invisible reCAPTCHA almost always requires calling the callback
callback_name = driver.find_element(
"css selector", ".g-recaptcha[data-size='invisible']"
).get_attribute("data-callback")
driver.execute_script(f'{callback_name}("{token}");')
// Puppeteer — invisible callback injection
await page.evaluate((tok) => {
// Set the hidden field
document.getElementById("g-recaptcha-response").value = tok;
// Find and call the callback function
const widget = document.querySelector("[data-size='invisible']");
const cbName = widget?.getAttribute("data-callback");
if (cbName && typeof window[cbName] === "function") {
window[cbName](tok);
}
}, token);
Điểm mấu chốt: Invisible không có cú click đó. Không gọi callback tường minh thì trang coi như chưa ai vượt qua thử thách, dù
g-recaptcha-responseđã có token hợp lệ.
Một solver dùng chung cho cả hai biến thể
Trong dự án thật bạn hiếm khi biết trước trang dùng biến thể nào, nên hãy gộp bước phát hiện vào bước giải.
import requests
import time
from bs4 import BeautifulSoup
class RecaptchaV2UniversalSolver:
def __init__(self, api_key):
self.api_key = api_key
def detect_and_solve(self, page_url, page_html=None):
if not page_html:
page_html = requests.get(page_url).text
soup = BeautifulSoup(page_html, "html.parser")
# Detect variant
invisible = soup.find(attrs={"data-size": "invisible", "class": "g-recaptcha"})
widget = invisible or soup.find(class_="g-recaptcha")
if not widget:
raise Exception("No reCAPTCHA widget found")
sitekey = widget.get("data-sitekey")
is_invisible = invisible is not None
callback = widget.get("data-callback")
params = {
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url
}
if is_invisible:
params["invisible"] = 1
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
if not resp.text.startswith("OK|"):
raise Exception(f"Submit failed: {resp.text}")
task_id = resp.text.split("|")[1]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get", "id": task_id
})
if result.text.startswith("OK|"):
return {
"token": result.text.split("|")[1],
"variant": "invisible" if is_invisible else "checkbox",
"callback": callback,
"sitekey": sitekey
}
if result.text != "CAPCHA_NOT_READY":
raise Exception(f"Solve error: {result.text}")
raise Exception("Timed out")
# Usage
solver = RecaptchaV2UniversalSolver("YOUR_API_KEY")
result = solver.detect_and_solve("https://staging.example.com/qa-login")
print(f"Variant: {result['variant']}, Callback: {result['callback']}")
Lợi ích: chỉ còn một chỗ cần sửa khi trang đổi cấu hình, thay vì hai nhánh code dễ lệch nhau.
Tình huống thường gặp ở đội QA tại Việt Nam
Một công ty outsourcing ở TP.HCM chạy regression hằng đêm cho form đăng ký của khách hàng. Bản desktop dùng checkbox, bản mobile cấu hình Invisible, vẫn cùng một sitekey. Bộ test viết cứng theo nhánh checkbox chạy xanh trên desktop và đỏ toàn bộ trên mobile, còn log chỉ ghi "form submit failed".
- Chạy hàm phát hiện ở đầu mỗi test case, đừng lưu biến thể vào file cấu hình.
- Ghi biến thể phát hiện được vào log để tách lỗi tích hợp khỏi lỗi ứng dụng.
- Chỉ chạy trên staging của hệ thống đội bạn được phép kiểm thử, và giữ log tối thiểu — Nghị định 13/2023/NĐ-CP về bảo vệ dữ liệu cá nhân là lý do để không đổ dữ liệu form vào log.
Chi phí có khác nhau giữa hai biến thể không
Không. CaptchaAI tính tiền theo thread (luồng giải đồng thời) chứ không theo từng lượt giải, nên hoá đơn phụ thuộc vào số task chạy song song. Gói BASIC ($15/tháng, 5 thread) đủ cho một bộ regression nhỏ chạy tuần tự; muốn chạy nhiều suite cùng lúc trong cửa sổ ban đêm thì STANDARD ($30/tháng, 15 thread) là bậc tiếp theo.
Lỗi thường gặp và cách xử lý
| Triệu chứng | Với checkbox | Với Invisible |
|---|---|---|
| Server từ chối token | Gán token vào g-recaptcha-response |
Gán token, rồi gọi hàm trong data-callback |
| Không tìm thấy widget | Tìm class .g-recaptcha |
Tìm data-size="invisible" hoặc lời gọi render |
| Form submit xong vẫn lỗi | Xem trang có chờ callback không | Tìm tên hàm callback rồi gọi |
| Trang tự tải lại sau khi chèn | Validation JavaScript không qua | Gọi callback trước khi form tự submit |
Câu hỏi thường gặp
Làm sao biết trang dùng biến thể nào khi không thấy widget nào cả?
Tìm data-size="invisible" trong HTML, hoặc lời gọi grecaptcha.render() có size: 'invisible' trong thẻ script. Không thấy widget chưa chắc đã là Invisible: widget checkbox cũng có thể bị CSS che.
Token của hai biến thể có dùng thay cho nhau được không?
Không. Token gắn với sitekey, domain và phiên phát hành ra nó. Cả hai đều có hạn 120 giây, nên hãy submit form ngay sau khi nhận token.
Vì sao form vẫn lỗi dù token đã được chèn thành công?
Gần như luôn là do chưa gọi callback. Gán giá trị vào g-recaptcha-response chỉ điền dữ liệu vào DOM chứ không kích hoạt logic xác thực của trang.
Biến thể nào giải nhanh hơn?
Invisible thường nhanh hơn: mức trần <30 giây so với <60 giây của checkbox, do ít khi phát sinh thử thách hình ảnh. Với polling 5 giây một lần, chênh lệch này ít khi đáng kể.
Đọc thêm
- Cách giải reCAPTCHA v2 bằng API — hướng dẫn cho biến thể checkbox
- Cách giải reCAPTCHA Invisible bằng API — luồng giải cho biến thể ẩn
- Các lỗi thường gặp với reCAPTCHA Invisible
- Cơ chế hoạt động của reCAPTCHA Invisible
- Khác biệt giữa reCAPTCHA v2 và v3