Trường Hợp Sử Dụng

Gửi biểu mẫu tự động với xử lý CAPTCHA

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:

  1. Điền các trường như bình thường (Selenium).
  2. Phát hiện loại CAPTCHA đang hiển thị trên trang.
  3. Gửi loại đó tới CaptchaAI để lấy token hoặc văn bản.
  4. 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ề Falsesubmit_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.

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