Xác thực tham số bằng Pydantic trước khi gửi request giúp bạn bắt lỗi cấu hình ngay trên máy, thay vì đợi CaptchaAI trả về ERROR_WRONG_CAPTCHA_ID sau một vòng round-trip tốn vài giây. Bài này xây dựng client Python hoàn chỉnh: model Pydantic v2 cho từng loại CAPTCHA (reCAPTCHA v2/v3, Turnstile, ảnh), class client gửi task tới in.php, polling res.php, và trả về token đã kiểm tra kiểu dữ liệu — dành cho dev cần code chạy được ngay.
Bốn bước client thực hiện, mỗi bước có model Pydantic riêng đứng chắn ở đầu vào:
- Định nghĩa model cho từng loại CAPTCHA —
sitekey,pageurlvà tham số riêng được validate ngay khi khởi tạo. - Gửi task tới
in.phpvới payload đã qua kiểm tra kiểu dữ liệu. - Polling
res.phptheo chu kỳpoll_intervalcho đến khi có kết quả. - Parse token trả về thành
SolveResult— không còn dòdict["key"].
Vì sao nên xác thực tham số bằng Pydantic trước khi gọi CaptchaAI
Không validate ở tầng client, một sitekey rỗng vẫn được gửi đi bình thường — bạn chỉ biết sai sau khi CaptchaAI trả lỗi. Pydantic chặn ba lớp lỗi phổ biến nhất trước khi có lệnh gọi mạng nào:
- Tham số rỗng hoặc sai kiểu —
sitekeyrỗng,pageurlkhông phải URL hợp lệ. - Field thiếu khi parse response — response API thiếu key mong đợi, tránh
KeyErrorkhi truy cập trực tiếp bằngdict["key"]. - Nhầm lẫn tham số giữa các loại CAPTCHA — gửi tham số của reCAPTCHA cho request Turnstile.
| Không có Pydantic | Có Pydantic |
|---|---|
Gửi sitekey rỗng, nhận lỗi API sau vài giây chờ |
ValidationError báo ngay lập tức, chưa gọi mạng |
Parse response bằng dict["key"], dễ dính KeyError khi field thiếu |
Model có kiểu dữ liệu, giá trị mặc định và validation rõ ràng |
| IDE không gợi ý được tham số nào hợp lệ | Type hint đầy đủ, autocomplete hoạt động trên mọi field |
Với team QA/scraping ở các công ty outsourcing tại TP.HCM hay Hà Nội chạy nhiều thread song song trên plan STANDARD ($30/tháng, 15 thread), một tham số sai lặp lại trên nhiều thread cùng lúc tốn thời gian hơn nhiều so với một request đơn lẻ — validate sớm chặn lỗi đó ngay tại nguồn.
Định nghĩa model Pydantic cho từng loại CAPTCHA
Mỗi loại CAPTCHA có tập tham số khác nhau. Các model dưới đây tách riêng từng loại, xác thực độ dài sitekey, định dạng pageurl, và tự dựng đúng payload mà in.php cần.
Ba điểm mỗi model kiểm tra:
- Độ dài
sitekey(min_length=20cho reCAPTCHA,min_length=10cho Turnstile). pageurlhợp lệ — kiểuHttpUrltự từ chối chuỗi thiếu scheme.- Response
in.php/res.phpparse thành model cóstatus,success,token.
# models.py
from pydantic import BaseModel, Field, field_validator, HttpUrl
from enum import Enum
from typing import Optional
class CaptchaMethod(str, Enum):
RECAPTCHA_V2 = "userrecaptcha"
RECAPTCHA_V3 = "userrecaptcha" # Differentiated by version field
TURNSTILE = "turnstile"
HCAPTCHA = "hcaptcha"
IMAGE = "base64"
GEETEST = "geetest"
class RecaptchaV2Request(BaseModel):
"""Parameters for solving reCAPTCHA v2."""
sitekey: str = Field(min_length=20, max_length=100, description="Site's reCAPTCHA sitekey")
pageurl: HttpUrl = Field(description="URL where CAPTCHA appears")
invisible: bool = False
cookies: Optional[str] = None
@field_validator("sitekey")
@classmethod
def validate_sitekey(cls, v: str) -> str:
if v.strip() != v:
raise ValueError("Sitekey must not have leading/trailing whitespace")
return v
def to_params(self) -> dict:
params = {
"method": "userrecaptcha",
"googlekey": self.sitekey,
"pageurl": str(self.pageurl),
}
if self.invisible:
params["invisible"] = "1"
if self.cookies:
params["cookies"] = self.cookies
return params
class RecaptchaV3Request(BaseModel):
"""Parameters for solving reCAPTCHA v3."""
sitekey: str = Field(min_length=20, max_length=100)
pageurl: HttpUrl
action: str = Field(default="verify", min_length=1, max_length=100)
def to_params(self) -> dict:
return {
"method": "userrecaptcha",
"version": "v3",
"googlekey": self.sitekey,
"pageurl": str(self.pageurl),
"action": self.action,
}
class TurnstileRequest(BaseModel):
"""Parameters for solving Cloudflare Turnstile."""
sitekey: str = Field(min_length=10, max_length=100)
pageurl: HttpUrl
action: Optional[str] = None
cdata: Optional[str] = None
def to_params(self) -> dict:
params = {
"method": "turnstile",
"sitekey": self.sitekey,
"pageurl": str(self.pageurl),
}
if self.action:
params["action"] = self.action
if self.cdata:
params["data"] = self.cdata
return params
class ImageRequest(BaseModel):
"""Parameters for solving image/text CAPTCHA."""
base64_image: str = Field(min_length=100, description="Base64-encoded image")
case_sensitive: bool = False
min_length: Optional[int] = Field(default=None, ge=1, le=50)
max_length: Optional[int] = Field(default=None, ge=1, le=50)
@field_validator("base64_image")
@classmethod
def validate_base64(cls, v: str) -> str:
# Strip data URI prefix if present
if v.startswith("data:"):
parts = v.split(",", 1)
if len(parts) == 2:
return parts[1]
return v
def to_params(self) -> dict:
params = {
"method": "base64",
"body": self.base64_image,
}
if self.case_sensitive:
params["regsense"] = "1"
if self.min_length is not None:
params["min_len"] = str(self.min_length)
if self.max_length is not None:
params["max_len"] = str(self.max_length)
return params
class SubmitResponse(BaseModel):
"""Parsed API submit response."""
status: int
request: str
@property
def success(self) -> bool:
return self.status == 1
@property
def task_id(self) -> str:
if not self.success:
raise ValueError(f"No task ID — submission failed: {self.request}")
return self.request
class PollResponse(BaseModel):
"""Parsed API poll response."""
status: int
request: str
@property
def ready(self) -> bool:
return self.request != "CAPCHA_NOT_READY"
@property
def success(self) -> bool:
return self.status == 1
@property
def token(self) -> str:
if not self.success:
raise ValueError(f"No token — solve failed: {self.request}")
return self.request
class SolveResult(BaseModel):
"""Result of a successful solve."""
token: str
task_id: str
solve_time: float = Field(description="Solve time in seconds")
Viết client Python gọi API CaptchaAI
Class CaptchaAI bọc toàn bộ vòng đời một task: gửi tới in.php, polling res.php mỗi poll_interval giây, rồi parse kết quả qua các model ở trên. Tham số đầu vào đã qua validation nên phần này chỉ còn lo I/O và lỗi API.
Bốn method public:
solve_recaptcha_v2()/solve_recaptcha_v3()solve_turnstile()solve_image()get_balance()— kiểm tra số dư
# client.py
import time
import requests
from pydantic import ValidationError
from models import (
RecaptchaV2Request,
RecaptchaV3Request,
TurnstileRequest,
ImageRequest,
SubmitResponse,
PollResponse,
SolveResult,
)
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
class CaptchaAIError(Exception):
def __init__(self, code: str, message: str = ""):
self.code = code
super().__init__(f"{code}: {message}" if message else code)
class CaptchaAI:
def __init__(self, api_key: str, poll_interval: int = 5, timeout: int = 180):
if not api_key or len(api_key) < 10:
raise ValueError("Invalid API key")
self.api_key = api_key
self.poll_interval = poll_interval
self.timeout = timeout
def _submit(self, params: dict) -> str:
params["key"] = self.api_key
params["json"] = 1
resp = requests.post(SUBMIT_URL, data=params, timeout=30)
result = SubmitResponse.model_validate(resp.json())
if not result.success:
raise CaptchaAIError(result.request, "Submit failed")
return result.task_id
def _poll(self, task_id: str) -> str:
start = time.monotonic()
while time.monotonic() - start < self.timeout:
time.sleep(self.poll_interval)
resp = requests.get(RESULT_URL, params={
"key": self.api_key,
"action": "get",
"id": task_id,
"json": 1,
}, timeout=15)
result = PollResponse.model_validate(resp.json())
if not result.ready:
continue
if result.success:
return result.token
raise CaptchaAIError(result.request, "Solve failed")
raise CaptchaAIError("TIMEOUT", f"Task {task_id} timed out after {self.timeout}s")
def _solve(self, params: dict) -> SolveResult:
start = time.monotonic()
task_id = self._submit(params)
token = self._poll(task_id)
elapsed = time.monotonic() - start
return SolveResult(
token=token,
task_id=task_id,
solve_time=round(elapsed, 1),
)
def solve_recaptcha_v2(self, sitekey: str, pageurl: str, **kwargs) -> SolveResult:
"""Solve reCAPTCHA v2 with validated parameters."""
req = RecaptchaV2Request(sitekey=sitekey, pageurl=pageurl, **kwargs)
return self._solve(req.to_params())
def solve_recaptcha_v3(self, sitekey: str, pageurl: str, **kwargs) -> SolveResult:
"""Solve reCAPTCHA v3 with validated parameters."""
req = RecaptchaV3Request(sitekey=sitekey, pageurl=pageurl, **kwargs)
return self._solve(req.to_params())
def solve_turnstile(self, sitekey: str, pageurl: str, **kwargs) -> SolveResult:
"""Solve Cloudflare Turnstile with validated parameters."""
req = TurnstileRequest(sitekey=sitekey, pageurl=pageurl, **kwargs)
return self._solve(req.to_params())
def solve_image(self, base64_image: str, **kwargs) -> SolveResult:
"""Solve image/text CAPTCHA with validated parameters."""
req = ImageRequest(base64_image=base64_image, **kwargs)
return self._solve(req.to_params())
def get_balance(self) -> float:
"""Get current account balance."""
resp = requests.get(RESULT_URL, params={
"key": self.api_key,
"action": "getbalance",
"json": 1,
}, timeout=10)
result = SubmitResponse.model_validate(resp.json())
return float(result.request)
Lưu ý:
poll_intervalmặc định 5 giây; giảm xuống 2–3 giây nếu cần token nhanh hơn, đổi lại gọires.phpnhiều lần hơn.
Ví dụ chạy thử: request hợp lệ và các lỗi bị chặn sớm
Ba tình huống dưới đây minh hoạ đúng giá trị của việc validate sớm:
- Request hợp lệ — validation pass, gọi API bình thường.
- Sitekey rỗng —
ValidationErrorchặn trước khi có request nào ra mạng. - Lỗi API thật (
CaptchaAIError) — tham số đã hợp lệ nhưng CaptchaAI xử lý thất bại.
from pydantic import ValidationError
from client import CaptchaAI, CaptchaAIError
client = CaptchaAI("YOUR_API_KEY", timeout=120)
# Valid request — passes validation, calls API
result = client.solve_recaptcha_v2(
sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl="https://staging.example.com/qa-login",
)
print(f"Token: {result.token[:40]}...")
print(f"Solved in {result.solve_time}s")
# Invalid sitekey — caught immediately, no API call
try:
client.solve_recaptcha_v2(sitekey="", pageurl="https://example.com")
except ValidationError as e:
print(e)
# sitekey: String should have at least 20 characters
# Invalid score — caught before API call
try:
client.solve_recaptcha_v3(
sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl="https://example.com",
)
except ValidationError as e:
print(e)
# API error — caught during request
try:
result = client.solve_turnstile(
sitekey="0x4AAAAAAADnPIDROrmt1Wwj",
pageurl="https://example.com",
)
except CaptchaAIError as e:
print(f"API error: {e.code}")
Cài các gói cần thiết trước khi chạy:
pip install pydantic requests
Xử lý lỗi thường gặp khi validate
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
ValidationError dù sitekey trông hợp lệ |
sitekey ngắn hơn 20 ký tự |
Kiểm tra lại độ dài sitekey; chỉnh min_length nếu site mục tiêu dùng key ngắn hơn |
ValidationError trên pageurl |
URL thiếu scheme | Thêm tiền tố https:// |
| Xác thực ảnh base64 thất bại | Chuỗi quá ngắn hoặc còn tiền tố data: |
Validator tự loại bỏ tiền tố data:; đảm bảo phần base64 thực còn lại > 100 ký tự |
CaptchaAIError: ERROR_ZERO_BALANCE |
Tài khoản hết số dư | Nạp thêm tại dashboard CaptchaAI |
| Lỗi import với Pydantic v1 | Đang dùng sai phiên bản Pydantic | Cài Pydantic v2: pip install 'pydantic>=2.0' |
Trước khi báo lỗi, kiểm tra nhanh:
- Đã cài
pydantic>=2.0chưa (pip show pydantic). sitekey/pageurlđúng định dạng site đang test.- Số dư tài khoản còn đủ không.
Câu hỏi thường gặp về Pydantic và CaptchaAI
Có bắt buộc dùng Pydantic v2 không?
Nên dùng — field_validator và model_validate trong bài này là API của v2. Pydantic v1 thiếu field_validator (dùng validator thay thế); cài pip install 'pydantic>=2.0' để tránh lỗi import.
Model có tự retry khi gặp ERROR_ZERO_BALANCE không?
Không. Model chỉ validate tham số đầu vào — retry, backoff hay quản lý thread song song là logic ở tầng client, tách biệt với tầng validation.
Có tái dùng cho GeeTest v3 hoặc BLS CAPTCHA không?
Có. Tạo thêm BaseModel cho từng loại (GeeTest v3 dùng method: "geetest", BLS dùng method: "bls") theo mẫu RecaptchaV2Request ở trên, rồi thêm hàm solve_* tương ứng vào client.
Validate có ảnh hưởng gì khi chạy nhiều thread song song không?
Không đáng kể — validation là tác vụ CPU thuần, chỉ vài micro giây và không giữ I/O, kể cả trên các plan nhiều thread như STANDARD (15 thread) hay ADVANCE ($90/tháng, 50 thread). Thời gian tốn thực sự vẫn là round-trip HTTP tới in.php/res.php.
Validate bằng Pydantic có tính vào chi phí gọi CaptchaAI không?
Không. Validation chạy hoàn toàn ở phía client, trước khi có request nào gửi tới in.php — CaptchaAI chỉ tính phí trên task thực sự được submit. Một sitekey sai bị ValidationError chặn lại không tốn gì cả, kể cả trên plan trả theo thread như STANDARD.
Bài viết liên quan
- Hướng dẫn đầy đủ dùng CaptchaAI với Playwright cho Python
- Xây dựng pipeline client-CAPTCHA với CaptchaAI
- Xác thực bảo mật webhook/callback của CaptchaAI
Bước tiếp theo
Ghép các model Pydantic ở trên vào project của bạn — lấy API key tại CaptchaAI và chạy thử ví dụ solve_recaptcha_v2 với sitekey thật của trang bạn đang kiểm thử.
- Copy
models.py,client.pyvào project,pip install pydantic requests. - Test với
sitekey/pageurlthật trước khi đưa vào pipeline CI.
Hướng dẫn liên quan: