Form có CAPTCHA là lý do phổ biến nhất khiến script Selenium hoặc test QA của bạn dừng lại đúng ở bước gửi cuối cùng. Cách xử lý gồm bốn phần lặp lại được cho mọi form:
- Điền các trường như bình thường (Selenium).
- Phát hiện loại CAPTCHA đang hiển thị trên trang.
- Gửi loại đó tới CaptchaAI để lấy token hoặc văn bản.
- Tiêm kết quả vào đúng trường ẩn rồi bấm submit.
Vì sao tự động hóa gửi form hay bị CAPTCHA chặn
Các đội QA outsourcing ở TP.HCM hay Hà Nội thường chạy hàng loạt kịch bản test trên form đăng ký, form liên hệ hoặc form đăng nhập — mà gần như form nào đủ quan trọng để bảo vệ dữ liệu cũng gắn một loại CAPTCHA. Selenium điền được các trường text bình thường, nhưng dừng lại ngay khi gặp widget CAPTCHA vì nó cần một token hợp lệ do dịch vụ giải phát hành, không phải giá trị bạn tự gõ vào.
Ba loại hay gặp nhất trong form web:
- reCAPTCHA v2 — checkbox "Tôi không phải robot", đôi khi kèm thử thách ảnh.
- Cloudflare Turnstile — widget ẩn hoặc hiện, phổ biến trên form đăng ký gần đây.
- CAPTCHA ảnh (OCR) — chuỗi ký tự trong ảnh, thường thấy ở form đăng nhập cũ hơn.
Luồng xử lý bốn bước cho form có CAPTCHA
Bốn bước ở trên ánh xạ trực tiếp vào một pipeline: load trang, điền field, phát hiện + giải CAPTCHA, rồi submit.
┌────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐
│ Load Form │────▶│ Fill Fields │────▶│ Detect & │────▶│ Submit Form │
│ (Selenium) │ │ │ │ Solve │ │ │
│ │ │ │ │ CAPTCHA │ │ │
└────────────┘ └──────────────┘ └────────────┘ └──────────────┘
Bước "Detect & Solve CAPTCHA" là phần duy nhất khác biệt so với automation form thông thường — mọi thứ còn lại là Selenium tiêu chuẩn.
Ba class dựng nên script
Script chia thành ba class độc lập: một class gọi API CaptchaAI, một class nhận diện loại CAPTCHA, và một class điều phối toàn bộ luồng điền — phát hiện — giải — gửi.
Class giải CAPTCHA (gọi in.php và polling res.php)
import time
import requests
class FormCaptchaSolver:
BASE = "https://ocr.captchaai.com"
def __init__(self, api_key):
self.api_key = api_key
def solve(self, params, initial_wait=10):
params["key"] = self.api_key
params["json"] = 1
resp = requests.post(f"{self.BASE}/in.php", data=params).json()
if resp["status"] != 1:
raise Exception(f"Submit error: {resp['request']}")
task_id = resp["request"]
time.sleep(initial_wait)
for _ in range(60):
result = requests.get(
f"{self.BASE}/res.php",
params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
).json()
if result["request"] == "CAPCHA_NOT_READY":
time.sleep(5)
continue
if result["status"] == 1:
return result["request"]
raise Exception(f"Solve error: {result['request']}")
raise TimeoutError("CAPTCHA solve timed out")
Class này chỉ làm một việc: gửi task tới in.php, chờ initial_wait giây, rồi poll res.php mỗi 5 giây cho tới khi có kết quả hoặc hết 60 lần thử.
Class phát hiện loại CAPTCHA trên trang
import re
from selenium.webdriver.common.by import By
class CaptchaDetector:
def __init__(self, driver):
self.driver = driver
def detect(self):
"""Detect CAPTCHA type on current page."""
html = self.driver.page_source
# Turnstile
turnstile = self.driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile, [data-sitekey]")
for el in turnstile:
if "cf-turnstile" in (el.get_attribute("class") or ""):
return "turnstile", el.get_attribute("data-sitekey")
# reCAPTCHA
recaptcha = self.driver.find_elements(By.CSS_SELECTOR, "[data-sitekey]")
if recaptcha:
sitekey = recaptcha[0].get_attribute("data-sitekey")
if "recaptcha" in html.lower():
return "recaptcha_v2", sitekey
# Image CAPTCHA
img = self.driver.find_elements(By.CSS_SELECTOR, "img[src*='captcha'], img.captcha")
if img:
return "image", img[0].get_attribute("src")
return "none", None
Thứ tự kiểm tra ở đây quan trọng: Turnstile được kiểm tra trước vì selector [data-sitekey] cũng khớp với reCAPTCHA, nên nếu đảo thứ tự, script sẽ nhận diện sai loại trên trang có cả hai.
Class điều phối: điền form, phát hiện, giải, gửi
import base64
import requests as req
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class FormAutomator:
def __init__(self, api_key):
self.solver = FormCaptchaSolver(api_key)
self.driver = webdriver.Chrome()
self.detector = CaptchaDetector(self.driver)
def fill_field(self, selector, value):
field = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
)
field.clear()
field.send_keys(value)
def select_option(self, selector, value):
from selenium.webdriver.support.ui import Select
select = Select(self.driver.find_element(By.CSS_SELECTOR, selector))
select.select_by_value(value)
def solve_captcha(self):
captcha_type, data = self.detector.detect()
page_url = self.driver.current_url
if captcha_type == "recaptcha_v2":
token = self.solver.solve({
"method": "userrecaptcha",
"googlekey": data,
"pageurl": page_url,
})
self.driver.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
return True
if captcha_type == "turnstile":
token = self.solver.solve({
"method": "turnstile",
"sitekey": data,
"pageurl": page_url,
})
self.driver.execute_script(
f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
)
return True
if captcha_type == "image":
img_data = req.get(data).content
img_b64 = base64.b64encode(img_data).decode()
text = self.solver.solve({"method": "base64", "body": img_b64})
captcha_input = self.driver.find_element(
By.CSS_SELECTOR, "input[name*='captcha']"
)
captcha_input.clear()
captcha_input.send_keys(text)
return True
return False # No CAPTCHA detected
def submit_form(self, url, fields, submit_selector="button[type='submit']"):
"""
fields: list of (selector, value) tuples
"""
self.driver.get(url)
for selector, value in fields:
self.fill_field(selector, value)
self.solve_captcha()
submit = self.driver.find_element(By.CSS_SELECTOR, submit_selector)
submit.click()
return self.driver.current_url
def close(self):
self.driver.quit()
solve_captcha() gọi detect() trước, rồi rẽ nhánh theo loại tìm được; nếu trang không có CAPTCHA nào, hàm trả về False và submit_form() vẫn submit bình thường.
Áp dụng thực tế: test form liên hệ trên staging
automator = FormAutomator("YOUR_API_KEY")
try:
result_url = automator.submit_form(
url="https://example.com/contact",
fields=[
("#name", "John Doe"),
("#email", "[email protected]"),
("#subject", "Sales inquiry"),
("#message", "I'd like to learn more about your services."),
],
submit_selector="#submit-btn",
)
print(f"Form submitted. Redirected to: {result_url}")
finally:
automator.close()
Chỉ ba tham số thay đổi khi áp dụng cho form khác: url, danh sách (selector, value), và submit_selector. Phần phát hiện và giải CAPTCHA chạy y hệt cho mọi form, không cần viết lại.
Áp dụng cho các loại form khác
Cùng một FormAutomator, chỉ đổi tham số đầu vào cho từng loại form.
Mẫu đăng nhập
result = automator.submit_form(
url="https://staging.example.com/qa-login",
fields=[
("#username", "testuser"),
("#password", "testpass123"),
],
submit_selector="#login-btn",
)
Mẫu đăng ký
result = automator.submit_form(
url="https://example.com/register",
fields=[
("#first-name", "Jane"),
("#last-name", "Smith"),
("#email", "[email protected]"),
("#password", "SecurePass!123"),
("#confirm-password", "SecurePass!123"),
],
submit_selector="#register-btn",
)
Biểu mẫu tìm kiếm bằng CAPTCHA
result = automator.submit_form(
url="https://example.com/search",
fields=[
("#query", "python developer"),
("#location", "San Francisco"),
],
submit_selector="#search-btn",
)
Với đội test chạy song song nhiều luồng — ví dụ regression test trên hàng chục biến thể form cùng lúc — mỗi luồng Selenium gọi riêng một instance FormCaptchaSolver; số luồng chạy đồng thời bị giới hạn bởi gói CaptchaAI đang dùng (BASIC $15/tháng đi kèm 5 thread, giải không giới hạn trên mỗi thread), chứ không phải bởi tổng số CAPTCHA giải trong ngày.
Lỗi thường gặp và cách xử lý
Bảng dưới liệt kê các lỗi hay gặp nhất khi ghép Selenium với CaptchaAI trong một pipeline gửi form.
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
| Token bị từ chối | Token hết hạn trước khi form kịp submit | Giải CAPTCHA ở bước cuối cùng, submit ngay sau đó |
| Selenium không tìm thấy trường | Trang load nội dung động (JS render sau) | Thêm WebDriverWait tường minh trước khi điền |
| Phát hiện sai loại CAPTCHA | Trang có nhiều phần tử CAPTCHA lồng nhau | Kiểm tra lại thứ tự ưu tiên trong detect() |
| Form reload về trạng thái trống sau submit | Validate phía server thất bại (thiếu trường bắt buộc) | Rà lại toàn bộ trường required trước khi solve CAPTCHA |
| Callback reCAPTCHA không chạy | Form cần gọi hàm callback thủ công | Gọi grecaptcha.execute() ngay sau khi tiêm token |
Câu hỏi thường gặp
Giải một CAPTCHA khi test form mất bao lâu?
Tùy loại: reCAPTCHA v2 và Turnstile thường trả token trong vài giây đến vài chục giây; CAPTCHA ảnh nhanh hơn. Script trong bài sleep 10 giây trước lần poll đầu, sau đó poll res.php mỗi 5 giây cho tới khi có kết quả hoặc hết 60 lần thử.
CaptchaAI có giải được Turnstile ẩn trong form đăng ký không?
Có. CaptchaDetector ở trên nhận diện widget qua class cf-turnstile và thuộc tính data-sitekey, sau đó gọi method: "turnstile" — đúng luồng dùng cho cả Turnstile hiển thị lẫn ẩn.
Chạy song song nhiều form test cùng lúc có bị giới hạn không?
Có, theo số thread của gói đang dùng — không theo số CAPTCHA giải mỗi ngày. BASIC ($15/tháng, 5 thread) đủ cho vài luồng test song song; đội QA chạy nhiều kịch bản cùng lúc nên tính số thread cần trước khi chọn gói.
Gửi form có CAPTCHA mà không cần mở trình duyệt được không?
Với reCAPTCHA và Turnstile thì được — giải lấy token rồi gửi thẳng qua HTTP POST. Nhưng nếu form còn chạy validate bằng JavaScript trước khi cho submit, bạn vẫn cần trình duyệt thật (hoặc headless) để trigger đúng luồng đó.
Form chỉ hiện CAPTCHA sau khi validate thất bại thì xử lý sao?
Một số form chỉ bật CAPTCHA sau lần submit đầu bị từ chối. Cách xử lý đơn giản là gọi lại solve_captcha() ngay trước lần submit tiếp theo, thay vì giả định CAPTCHA luôn có mặt ngay từ đầu.
Hướng dẫn liên quan
Đóng gói phát hiện, giải và gửi lại thành một pipeline duy nhất — giải CAPTCHA bằng CaptchaAI.