Bạn không cần Selenium hay Playwright để qua Cloudflare Turnstile. Ba lệnh gọi HTTP là đủ: đọc HTML lấy sitekey, gửi task tới in.php của CaptchaAI, polling res.php lấy token, rồi gắn token vào field cf-turnstile-response khi POST form. Tất cả chạy trong Python thuần với requests, tốn vài chục MB RAM thay vì cả một trình duyệt headless.
Bài dành cho dev viết script scraping hoặc kiểm thử tự động, cần tích hợp chạy được ngay kèm xử lý lỗi. CaptchaAI hỗ trợ Turnstile chính thức: thời gian giải <10 giây, tỷ lệ giải thành công cao trên mọi chế độ widget.
Cần chuẩn bị những gì
pip install requests
Ba thứ cần có trước khi chạy dòng code đầu tiên:
- API key CaptchaAI lấy tại captchaai.com.
- URL trang đích chứa widget Turnstile.
- Sitekey Turnstile — bước 1 sẽ tự trích xuất.
Gói nhỏ nhất là BASIC ($15/tháng, 5 thread). CaptchaAI tính tiền theo thread (luồng giải đồng thời), không theo từng lần giải — chỉ mức đồng thời mới ảnh hưởng hoá đơn.
Bước 1: lấy sitekey Turnstile từ HTML
Sitekey nằm trong HTML, thường ở data-sitekey của thẻ div.cf-turnstile, và luôn bắt đầu bằng 0x. Hàm dưới thử ba pattern vì mỗi site nhúng widget một kiểu.
import re
import requests
def extract_turnstile_sitekey(url):
"""Extract Cloudflare Turnstile sitekey from page HTML."""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0",
"Accept": "text/html,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
}
response = requests.get(url, headers=headers, timeout=15)
patterns = [
r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']',
r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
r"siteKey\s*[=:]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
]
for pattern in patterns:
match = re.search(pattern, response.text)
if match:
return match.group(1)
return None
sitekey = extract_turnstile_sitekey("https://example.com/signup")
print(f"Sitekey: {sitekey}")
Trả về None nghĩa là widget được render bằng JavaScript — requests không thấy gì. Xem phần khắc phục sự cố.
Bước 2: gửi task giải tới CaptchaAI
Endpoint nhận task là in.php, bắt buộc method=turnstile, sitekey, pageurl. Thêm json=1 cho phản hồi dạng JSON dễ parse.
import requests
API_KEY = "YOUR_API_KEY"
def submit_turnstile(sitekey, page_url):
"""Submit Turnstile solving task to CaptchaAI."""
response = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"json": 1,
})
data = response.json()
if data.get("status") != 1:
raise Exception(f"Submit failed: {data.get('request')}")
return data["request"]
task_id = submit_turnstile("0x4AAAAAAAC3DHQhMMQ_Rxrg", "https://example.com/signup")
print(f"Task ID: {task_id}")
Phản hồi thành công có status: 1; trường request là ID task, giữ lại cho bước sau.
Bước 3: polling res.php để lấy token
Polling là chủ động hỏi kết quả định kỳ. Chờ 5 giây mỗi lần — hỏi dồn dập chỉ tốn request.
import time
def poll_result(task_id, timeout=120):
"""Poll CaptchaAI for the solved Turnstile token."""
start = time.time()
while time.time() - start < timeout:
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": 1,
}).json()
if result.get("status") == 1:
return result["request"]
if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
raise Exception("Turnstile could not be solved")
raise TimeoutError("Solve timed out")
token = poll_result(task_id)
print(f"Token: {token[:50]}...")
Code dừng sớm khi gặp ERROR_CAPTCHA_UNSOLVABLE — đó là lỗi vĩnh viễn của task, không phải hàng đợi chậm.
Ghép ba bước thành script chạy được
Ba bước nối vào nhau, dùng chung một requests.Session để cookie được giữ giữa lần GET và lần POST.
import re
import time
import requests
API_KEY = "YOUR_API_KEY"
TARGET_URL = "https://example.com/signup"
def solve_turnstile(sitekey, page_url):
"""Full Turnstile solve: submit + poll."""
# Submit
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"json": 1,
})
data = submit.json()
if data.get("status") != 1:
raise Exception(f"Submit error: {data.get('request')}")
task_id = data["request"]
print(f"Task submitted: {task_id}")
# Poll
for _ in range(30):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": 1,
}).json()
if result.get("status") == 1:
return result["request"]
raise TimeoutError("Solve timed out")
# --- Main flow ---
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0",
"Accept": "text/html,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
})
# 1. Get page and extract sitekey
response = session.get(TARGET_URL, timeout=15)
match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', response.text)
if not match:
raise ValueError("Turnstile sitekey not found")
sitekey = match.group(1)
print(f"Sitekey: {sitekey}")
# 2. Solve Turnstile
token = solve_turnstile(sitekey, TARGET_URL)
print(f"Token: {token[:50]}...")
# 3. Submit form with token
form_response = session.post(TARGET_URL, data={
"cf-turnstile-response": token,
"email": "[email protected]",
"password": "SecurePass123",
})
print(f"Form status: {form_response.status_code}")
Điểm dễ bỏ sót: token phải gửi từ cùng session đã tải trang. Mở session mới rồi POST token vào là lỗi phổ biến nhất khi lên production.
Khi widget có tham số action
Một số site khai báo data-action và kiểm tra lại ở phía server. Bỏ qua thì token vẫn về nhưng form bị từ chối — rất khó chẩn đoán nếu không biết trước.
def solve_turnstile_with_action(sitekey, page_url, action):
"""Solve Turnstile that requires an action parameter."""
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"action": action, # Include the action from data-action attribute
"json": 1,
})
data = submit.json()
if data.get("status") != 1:
raise Exception(f"Submit error: {data.get('request')}")
task_id = data["request"]
for _ in range(30):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": 1,
}).json()
if result.get("status") == 1:
return result["request"]
raise TimeoutError("Solve timed out")
Thấy data-action="signup" trong HTML thì truyền đúng chuỗi signup vào task.
Ba cách gắn token vào request
Cách 1: POST form với cf-turnstile-response
Phổ biến nhất; Turnstile luôn dùng field cf-turnstile-response.
# Most common — Turnstile uses cf-turnstile-response field
response = session.post(form_url, data={
"cf-turnstile-response": token,
"email": "[email protected]",
})
Cách 2: API dạng JSON
SPA thường gửi JSON, tự đặt tên field.
response = session.post(api_url, json={
"turnstileToken": token,
"email": "[email protected]",
})
Cách 3: tên field tuỳ chỉnh
Một số site đổi tên field, hoặc yêu cầu cả hai.
# Some sites rename the field — check the form HTML
response = session.post(form_url, data={
"cf-turnstile-response": token,
"captcha_token": token, # Custom duplicate field
"action": "signup",
})
Không chắc thì mở DevTools, submit thủ công một lần và đọc payload trong tab Network.
Class dùng được cho production
Chạy hàng nghìn lần mỗi ngày thì cần retry, phân loại lỗi và timeout ở mọi request.
import re
import time
import requests
class TurnstileSolver:
"""Production-ready Turnstile solver with retry logic."""
API_URL = "https://ocr.captchaai.com"
def __init__(self, api_key, max_retries=3):
self.api_key = api_key
self.max_retries = max_retries
def extract_sitekey(self, session, url):
"""Extract Turnstile sitekey from page."""
response = session.get(url, timeout=15)
match = re.search(
r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', response.text
)
return match.group(1) if match else None
def solve(self, sitekey, page_url, action=None):
"""Solve Turnstile with retry logic. Returns token string."""
for attempt in range(1, self.max_retries + 1):
try:
token = self._solve_once(sitekey, page_url, action)
return token
except TimeoutError:
print(f"Attempt {attempt} timed out")
except Exception as e:
error_str = str(e)
if "ERROR_ZERO_BALANCE" in error_str:
raise # Don't retry billing errors
if "ERROR_WRONG_USER_KEY" in error_str:
raise
print(f"Attempt {attempt} failed: {e}")
raise Exception(f"Failed after {self.max_retries} attempts")
def _solve_once(self, sitekey, page_url, action=None):
"""Single solve attempt."""
params = {
"key": self.api_key,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"json": 1,
}
if action:
params["action"] = action
submit = requests.post(f"{self.API_URL}/in.php", data=params, timeout=30)
submit.raise_for_status()
data = submit.json()
if data.get("status") != 1:
raise Exception(f"Submit error: {data.get('request')}")
task_id = data["request"]
for _ in range(30):
time.sleep(5)
result = requests.get(f"{self.API_URL}/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id,
"json": 1,
}, timeout=30).json()
if result.get("status") == 1:
return result["request"]
if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
raise Exception("CAPTCHA unsolvable")
raise TimeoutError("Poll timed out")
# Usage
solver = TurnstileSolver("YOUR_API_KEY")
token = solver.solve("0x4AAAAAAAC3DHQhMMQ_Rxrg", "https://example.com/signup")
ERROR_ZERO_BALANCE (hết số dư) và ERROR_WRONG_USER_KEY (sai API key) được ném ra ngay, không thử lại — retry một lỗi cấu hình chỉ làm job chậm thêm ba lần.
Ví dụ thực tế: theo dõi giá trên sàn thương mại điện tử
Tình huống quen với đội data ở TP.HCM và Hà Nội: thu thập giá công khai trên Shopee, Lazada hay Tiki cho nghiên cứu thị trường. Job chạy 6 lần mỗi ngày, vài nghìn URL, một số luồng có Turnstile chặn giữa chừng. Cách bố trí với TurnstileSolver ở trên:
- Đưa URL vào hàng đợi, chỉ gọi solver khi thực sự gặp widget.
- Chạy số worker bằng đúng số thread trong gói. Với STANDARD ($30/tháng, 15 thread), 15 worker là điểm cân bằng; thêm nữa chỉ làm request xếp hàng chờ.
- Ghi log mỗi lần giải kèm ID task, thời gian và kết quả — để debug và để chứng minh phạm vi dữ liệu đã thu thập theo yêu cầu tối thiểu hoá dữ liệu của Nghị định 13/2023/NĐ-CP (ghi chú vận hành, không phải tư vấn pháp lý).
- Đặt giới hạn tần suất request (rate limit) phía bạn, và chỉ chạy trên dữ liệu công khai hoặc hệ thống bạn có quyền vận hành.
Giá tính bằng USD, thanh toán bằng thẻ quốc tế; đừng quy đổi sang VND khi ước tính chi phí vì tỷ giá làm lệch con số.
Khắc phục sự cố
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
| Có token nhưng form bị từ chối | Sai sitekey hoặc thiếu action |
Lấy lại sitekey, truyền action nếu widget có data-action |
| Không tìm thấy sitekey | Widget render bằng JavaScript | Dùng Selenium hoặc Playwright cho trang động |
| HTTP 403 khi GET trang | Thiếu header trình duyệt | Bổ sung User-Agent, Accept, Accept-Language |
| Giải mất hơn 60 giây | Hàng đợi đông giờ cao điểm | Tăng timeout thay vì huỷ task |
| Token dùng một lần rồi hỏng | Site yêu cầu token mới mỗi lần submit | Giải token mới cho từng lần gửi |
ERROR_ZERO_BALANCE |
Hết số dư | Nạp thêm hoặc nâng gói; đừng thử lại |
Câu hỏi thường gặp
Giải Turnstile bằng Python có bắt buộc dùng Selenium không?
Không. requests là đủ vì token CaptchaAI trả về không gắn với trình duyệt nào. Chỉ cần Selenium hoặc Playwright khi sitekey được JavaScript chèn vào DOM sau khi trang tải.
Token Turnstile sống được bao lâu?
Vài phút, và chỉ dùng được một lần. Gửi form ngay sau khi nhận token; token cũ sẽ bị server từ chối.
Giải 10.000 CAPTCHA Turnstile mỗi ngày tốn bao nhiêu?
Phụ thuộc mức đồng thời, không phải số lần giải: mỗi thread giải không giới hạn. Với <10 giây mỗi lần, STANDARD ($30/tháng, 15 thread) hoặc ADVANCE ($90/tháng, 50 thread) hợp với khối lượng này.
CaptchaAI có hỗ trợ hCaptcha không?
Không. CaptchaAI chưa hỗ trợ hCaptcha và FunCaptcha (Arkose Labs); GeeTest v4 sắp ra mắt. Dùng được: reCAPTCHA v2/v3, Cloudflare Turnstile và Challenge, GeeTest v3, image/OCR, grid-image, BLS, cùng CaptchaFox (beta), Friendly Captcha (beta), Lemin (beta).
Chế độ widget managed hay invisible có ảnh hưởng gì không?
Không. Cả ba chế độ managed, non-interactive và invisible dùng chung method=turnstile; code của bạn không cần đổi gì.
Tóm lại
Turnstile trong Python thuần gọn hơn nhiều người nghĩ: lấy sitekey, gửi task tới in.php, polling res.php, gắn cf-turnstile-response vào form. reCAPTCHA v2/v3 và GeeTest v3 dùng đúng cặp endpoint đó, chỉ khác method và tên field token — nên lớp solver ở trên tái sử dụng gần như nguyên vẹn. Lấy API key tại CaptchaAI và thử với sitekey của bạn.