Hướng Dẫn Thực Hành

Giải Cloudflare Turnstile bằng Python requests và CaptchaAI

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:

  1. Đưa URL vào hàng đợi, chỉ gọi solver khi thực sự gặp widget.
  2. 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ờ.
  3. 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ý).
  4. Đặ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.

Bài viết liên quan

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