Một luồng checkout TMĐT điển hình đi qua 5 bước: giỏ hàng → giao hàng → thanh toán → xác nhận, và CAPTCHA có thể "phục kích" ở bất kỳ bước nào — không cố định một chỗ. Đội QA kiểm thử checkout nội bộ trên staging (quen thuộc tại các sàn TMĐT như Shopee, Tiki, Lazada) sẽ thấy script đứng im ngay tại bước có CAPTCHA nếu không xử lý được. Bài này build class Python CheckoutAutomator tự phát hiện và giải CAPTCHA bằng CaptchaAI ở bất kỳ bước nào, thay vì viết logic riêng cho từng trang.
CAPTCHA xuất hiện ở đâu trong luồng checkout?
Trước khi code, xem CAPTCHA thường rơi vào bước nào:
| Giai đoạn | Loại CAPTCHA thường gặp | Nguyên nhân kích hoạt |
|---|---|---|
| Thêm vào giỏ hàng | reCAPTCHA v3 (vô hình) | Chặn bot |
| Đăng nhập / checkout khách | reCAPTCHA v2 | Xác minh tài khoản |
| Địa chỉ giao hàng | Cloudflare Turnstile | Giới hạn tần suất request |
| Trang thanh toán | reCAPTCHA v2/v3 | Chống gian lận |
| Xác nhận đơn hàng | reCAPTCHA v2 | Xác nhận cuối cùng |
Vì vị trí không cố định, kiểm tra CAPTCHA ở cả đầu và cuối mỗi bước thay vì đoán trước. Với các đội outsourcing chạy QA cho nhiều client TMĐT cùng lúc — mô hình phổ biến ở các công ty phần mềm tại TP.HCM và Hà Nội — một hàm dùng chung như vậy quan trọng hơn nữa, vì mỗi client có thể đổi vị trí CAPTCHA sau mỗi lần cập nhật giao diện checkout mà không báo trước.
Sơ đồ luồng 5 bước
Class dưới đây bọc từng bước checkout trong một hàm riêng, kiểm tra CAPTCHA ở cả điểm vào lẫn điểm ra:
┌────────┐ ┌───────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐
│ Cart │───▶│ Shipping │───▶│ Payment │───▶│ Review │───▶│ Confirm │
│ │ │ Address │ │ Info │ │ Order │ │ │
└────────┘ └───────────┘ └──────────┘ └─────────┘ └──────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
[CAPTCHA?] [CAPTCHA?] [CAPTCHA?] [CAPTCHA?] [CAPTCHA?]
Class CheckoutAutomator: xử lý cả 5 bước bằng một hàm dùng chung
_solve_if_captcha() là trung tâm: quét DOM tìm .cf-turnstile hoặc [data-sitekey], phân biệt v2/v3 qua grecaptcha.execute, gửi task qua in.php, polling res.php tới khi có token, rồi bơm token vào field ẩn bằng execute_script. Mỗi hàm bước — add_to_cart, fill_shipping, fill_payment, confirm_order — chỉ gọi lại hàm này trước khi submit:
import time
import requests
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 CaptchaSolver:
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(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(result["request"])
raise TimeoutError("Timed out")
class CheckoutAutomator:
def __init__(self, api_key):
self.solver = CaptchaSolver(api_key)
self.driver = webdriver.Chrome()
self.wait = WebDriverWait(self.driver, 15)
def _find(self, selector):
return self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))
def _click(self, selector):
self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, selector))).click()
def _fill(self, selector, value):
el = self._find(selector)
el.clear()
el.send_keys(value)
def _solve_if_captcha(self):
"""Check for and solve any CAPTCHA on the current page."""
html = self.driver.page_source
page_url = self.driver.current_url
# Turnstile
turnstile = self.driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile")
if turnstile:
sitekey = turnstile[0].get_attribute("data-sitekey")
token = self.solver.solve({
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
})
self.driver.execute_script(
f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
)
return "turnstile"
# reCAPTCHA
recaptcha = self.driver.find_elements(By.CSS_SELECTOR, "[data-sitekey]")
if recaptcha and "recaptcha" in html.lower():
sitekey = recaptcha[0].get_attribute("data-sitekey")
# Check if v3 (invisible)
if "grecaptcha.execute" in html:
token = self.solver.solve({
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"version": "v3",
"action": "checkout",
}, initial_wait=20)
else:
token = self.solver.solve({
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
})
self.driver.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
return "recaptcha"
return None
# --- Checkout Steps ---
def add_to_cart(self, product_url, quantity=1):
"""Step 1: Navigate to product and add to cart."""
self.driver.get(product_url)
time.sleep(2)
# Set quantity if field exists
qty_fields = self.driver.find_elements(By.CSS_SELECTOR, "input[name='quantity']")
if qty_fields:
qty_fields[0].clear()
qty_fields[0].send_keys(str(quantity))
self._solve_if_captcha()
self._click("[data-action='add-to-cart'], .add-to-cart, #add-to-cart")
time.sleep(2)
return True
def fill_shipping(self, address):
"""Step 2: Fill shipping address."""
self._solve_if_captcha()
field_map = {
"first_name": "#shipping-first-name, [name='firstName']",
"last_name": "#shipping-last-name, [name='lastName']",
"address": "#shipping-address, [name='address1']",
"city": "#shipping-city, [name='city']",
"state": "#shipping-state, [name='state']",
"zip": "#shipping-zip, [name='postalCode']",
"phone": "#shipping-phone, [name='phone']",
}
for field, selectors in field_map.items():
if field in address:
for selector in selectors.split(", "):
elements = self.driver.find_elements(By.CSS_SELECTOR, selector)
if elements:
elements[0].clear()
elements[0].send_keys(address[field])
break
self._solve_if_captcha()
self._click("[data-step='shipping-submit'], .continue-to-payment")
time.sleep(2)
return True
def fill_payment(self, payment):
"""Step 3: Fill payment information."""
self._solve_if_captcha()
# Handle iframe for card fields (common pattern)
iframes = self.driver.find_elements(By.CSS_SELECTOR, "iframe[name*='card']")
if iframes:
self.driver.switch_to.frame(iframes[0])
self._fill("input[name='cardnumber']", payment["card_number"])
self.driver.switch_to.default_content()
if len(iframes) > 1:
self.driver.switch_to.frame(iframes[1])
self._fill("input[name='exp-date']", payment["expiry"])
self.driver.switch_to.default_content()
if len(iframes) > 2:
self.driver.switch_to.frame(iframes[2])
self._fill("input[name='cvc']", payment["cvv"])
self.driver.switch_to.default_content()
else:
# Direct fields
self._fill("[name='cardNumber'], #card-number", payment["card_number"])
self._fill("[name='expiry'], #card-expiry", payment["expiry"])
self._fill("[name='cvv'], #card-cvv", payment["cvv"])
self._solve_if_captcha()
self._click("[data-step='payment-submit'], .continue-to-review")
time.sleep(2)
return True
def confirm_order(self):
"""Step 4: Review and confirm order."""
self._solve_if_captcha()
self._click("[data-step='confirm'], .place-order, #place-order")
time.sleep(3)
# Check for confirmation
html = self.driver.page_source.lower()
if "order confirmed" in html or "thank you" in html or "confirmation" in html:
return True
return False
def run_checkout(self, product_url, address, payment, quantity=1):
"""Run complete checkout flow."""
steps = [
("Add to cart", lambda: self.add_to_cart(product_url, quantity)),
("Proceed to checkout", lambda: self._click(".checkout-btn, a[href*='checkout']")),
("Fill shipping", lambda: self.fill_shipping(address)),
("Fill payment", lambda: self.fill_payment(payment)),
("Confirm order", lambda: self.confirm_order()),
]
for step_name, step_fn in steps:
print(f"Step: {step_name}...")
try:
result = step_fn()
print(f" ✓ {step_name} complete")
except Exception as e:
print(f" ✗ {step_name} failed: {e}")
return False
return True
def close(self):
self.driver.quit()
Ví dụ chạy: checkout đầy đủ từ giỏ hàng đến xác nhận
Gọi run_checkout() với product_url, địa chỉ giao hàng và thông tin thanh toán — class tự lo phần CAPTCHA, không cần can thiệp thêm:
automator = CheckoutAutomator("YOUR_API_KEY")
try:
success = automator.run_checkout(
product_url="https://store.example.com/product/widget-pro",
address={
"first_name": "Jane",
"last_name": "Smith",
"address": "123 Test Street",
"city": "San Francisco",
"state": "CA",
"zip": "94102",
"phone": "415-555-0100",
},
payment={
"card_number": "4111111111111111",
"expiry": "12/26",
"cvv": "123",
},
quantity=1,
)
print(f"Checkout {'succeeded' if success else 'failed'}")
finally:
automator.close()
Các lỗi thường gặp và cách xử lý
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
| Token bị từ chối lúc thanh toán | Token hết hạn trước khi submit | Giải ngay trước khi nhấn gửi, không để dành |
| Trường thẻ không điền được | Trường nằm trong iframe Stripe/Braintree | Chuyển đúng iframe trước khi gọi _fill |
| Checkout chuyển hướng về đăng nhập | Phiên (session) đã hết hạn | Thêm bước đăng nhập lại trước checkout |
| Đơn hàng fail âm thầm | Thiếu trường bắt buộc | Kiểm tra lỗi sau mỗi bước, đừng chỉ tin return True |
Xử lý các tình huống đặc biệt
CAPTCHA bật lại sau khi validate lỗi
Một số form hiển thị CAPTCHA mới sau submit thất bại. Bọc bước đó trong logic retry:
def retry_step_with_captcha(self, step_fn, max_retries=2):
for attempt in range(max_retries + 1):
try:
return step_fn()
except Exception:
if attempt < max_retries:
self._solve_if_captcha()
continue
raise
CAPTCHA chỉ xuất hiện ở request đầu phiên
Nhiều trang chỉ hiển thị CAPTCHA một lần mỗi phiên. Giữ nguyên cookie trình duyệt giữa các bước để tránh giải lại nhiều lần. Nếu script log lại dữ liệu địa chỉ hoặc thông tin thanh toán sandbox trong quá trình test, hãy giới hạn thời gian lưu và xoá log định kỳ — nguyên tắc tối thiểu hoá dữ liệu này phù hợp với Nghị định 13/2023/NĐ-CP về bảo vệ dữ liệu cá nhân, dù script chỉ chạy nội bộ trên staging.
Trường form load động (lazy-loaded)
Một số trang render lại field bằng JavaScript sau khi chuyển bước. Chờ một selector chỉ báo trước khi thao tác:
def wait_for_step(self, indicator_selector, timeout=15):
"""Wait for a step to fully load before proceeding."""
WebDriverWait(self.driver, timeout).until(
EC.presence_of_element_located((By.CSS_SELECTOR, indicator_selector))
)
Câu hỏi thường gặp
CaptchaAI có giải được CAPTCHA ở trang thanh toán không?
Có — reCAPTCHA v2, reCAPTCHA v3 và Cloudflare Turnstile, ba loại xuất hiện nhiều nhất ở các bước trong ví dụ này.
Test checkout mà không cần thanh toán thật có được không?
Được. Dùng số thẻ sandbox như 4111111111111111; CaptchaAI giải cùng loại CAPTCHA đó trên môi trường test.
Có cần gọi _solve_if_captcha() ở mọi bước không?
Nên, vì vị trí CAPTCHA không cố định (xem bảng đầu bài). Hàm chỉ trả về None khi không có CAPTCHA.
Checkout một trang thì xử lý sao?
Gộp mọi field vào một trang, chỉ gọi _solve_if_captcha() một lần trước khi submit lần cuối.
Làm sao tránh bị từ chối token vì hết hạn?
Luôn giải ngay trước submit, không cache token để dùng sau — nguyên nhân phổ biến nhất trong bảng lỗi ở trên.
Hướng dẫn liên quan
Kiểm thử checkout tự động — giải CAPTCHA từng bước với CaptchaAI.