Xử lý lỗi bộ giải captcha: thử lại, backoff và những gì không bao giờ thử lại
Một lần giải captcha là một lời gọi mạng đến một hệ thống bạn không kiểm soát, nhằm vào một thử thách có thể thay đổi mà không báo trước. Xử lý lỗi tốt là sự khác biệt giữa một pipeline suy giảm một cách êm ái và một pipeline lãng phí thời gian trong một cơn bão thử lại.
Với Scarnox, kinh tế học có lợi cho bạn: các lần giải thất bại không bị tính phí, nên một lần thử lại chỉ tốn thời gian chứ không tốn gì khác. Nhưng thử lại nhầm loại lỗi vẫn lãng phí thời gian đó, và thử lại quá mức có thể tự gây ra vấn đề. Hướng dẫn này cung cấp một chính sách thử lại mà bạn có thể dán vào một job Python.
Scarnox giải PopularCaptcha hiện nay. Cùng cách xử lý lỗi này áp dụng cho bất kỳ bộ giải captcha nào, nên chính sách dưới đây không phụ thuộc vào loại captcha.
Luôn kiểm tra errorId trước
Phản hồi giải có trường errorId. 0 nghĩa là thành công và token nằm dưới solution.token. Bất kỳ giá trị nào khác có nghĩa là lần giải thất bại. Một phản hồi thất bại cũng mang theo errorCode và errorDescription. Hãy ghi log cả hai: mã là thứ bạn sẽ tìm kiếm sau này, còn mô tả là thứ bạn sẽ đọc khi một lần chạy gặp sự cố.
{"errorId": 1, "errorCode": "...", "errorDescription": "..."}
Đừng bao giờ cho rằng một phản hồi là thành công chỉ vì mã trạng thái HTTP là 200. Kiểm tra errorId mỗi lần.
Các lỗi nên thử lại
Một số lỗi là tạm thời. Hãy thử lại chúng với backoff lũy thừa và một độ trễ ngẫu nhiên nhỏ:
- Lỗi mạng: kết nối bị reset, sự cố DNS, timeout khi đọc phía bạn.
- Lỗi máy chủ: phản hồi HTTP 5xx, nghĩa là có sự cố ở phía dịch vụ.
- Giới hạn tần suất: HTTP 429, nghĩa là bạn đang gửi yêu cầu quá nhanh. Hãy chậm lại.
- Timeout: lần giải chạy lâu hơn thời gian client của bạn chờ. Thử lại với thời gian chờ dài hơn nếu bạn chưa đặt.
Những lỗi này đáng để thử lại vì lần tiếp theo có thể thành công mà không cần thay đổi gì ở phía bạn.
Các lỗi cần sửa trước khi thử lại
Các lỗi khác sẽ không tự hết nếu bạn chờ. Thử lại với cùng đầu vào sẽ thất bại theo cùng cách. Hãy sửa những lỗi này trước:
- Loại tác vụ không hợp lệ. Kiểm tra
captcha_typecó được viết đúng như trong tài liệu không. - Sitekey sai hoặc thiếu. Đọc lại sitekey từ trang. Sitekey từ một lần triển khai cũ là nguyên nhân phổ biến.
- URL trang sai.
site_urlnên là trang hiển thị widget. - Vấn đề với proxy. Một proxy chết, gõ sai hoặc bị chặn sẽ làm mọi lần thử thất bại. Hãy kiểm tra proxy riêng trước. Xem proxy so với proxyless.
- Vấn đề tài khoản. Khóa không hợp lệ hoặc tài khoản không đủ số dư sẽ làm mọi lần thử thất bại cho đến khi ai đó sửa tài khoản. Hãy báo cho người phụ trách; đừng thử lại trong vòng lặp.
Khi một lỗi không nằm trong cả hai danh sách, hãy coi nó là lỗi cần sửa trước. Xem thông báo lỗi, quyết định, và thêm mã vào danh sách đúng trong code của bạn. Giữ các danh sách ngắn và được xem xét định kỳ.
Một chính sách bạn có thể dán vào
import os
import random
import time
import requests
API_URL = "https://api.scarnox.com/api/tasks/create"
HEADERS = {"Authorization": f"Bearer {os.environ['SCARNOX_KEY']}"}
MAX_ATTEMPTS = 4
class SolveError(Exception):
def __init__(self, code: str, description: str, retryable: bool):
super().__init__(f"{code}: {description}")
self.code = code
self.retryable = retryable
def solve_once(payload: dict) -> str:
try:
r = requests.post(API_URL, headers=HEADERS, json=payload, timeout=185)
except requests.RequestException as exc:
# Lỗi mạng hoặc timeout phía chúng ta: an toàn để thử lại.
raise SolveError("NETWORK", str(exc), retryable=True)
if r.status_code == 429 or r.status_code >= 500:
raise SolveError(f"HTTP_{r.status_code}", r.text[:200], retryable=True)
try:
data = r.json()
except ValueError:
raise SolveError(f"HTTP_{r.status_code}", r.text[:200], retryable=False)
if data.get("errorId") == 0:
return data["solution"]["token"]
# API báo lỗi. Chỉ thử lại các mã bạn đã thấy là tạm thời.
code = data.get("errorCode", "UNKNOWN")
retryable = code in TRANSIENT_CODES
raise SolveError(code, data.get("errorDescription", ""), retryable=retryable)
# Chỉ thêm mã lỗi API vào đây sau khi bạn đã thấy chúng là tạm thời trong log.
TRANSIENT_CODES: set[str] = set()
def solve(payload: dict) -> str:
for attempt in range(MAX_ATTEMPTS):
try:
return solve_once(payload)
except SolveError as err:
last_attempt = attempt == MAX_ATTEMPTS - 1
if not err.retryable or last_attempt:
raise
delay = min(30, 2 ** attempt) + random.random() # backoff + jitter
print(f"attempt {attempt + 1} failed ({err.code}), retrying in {delay:.1f}s")
time.sleep(delay)
raise RuntimeError("unreachable")
Chính sách này không bao giờ thử lại một lỗi cần sửa trước, và nó dừng sau một số lần thử cố định. Tập TRANSIENT_CODES cố ý bắt đầu rỗng: chỉ thêm một mã khi log của bạn cho thấy nó là tạm thời.
Vì sao cần jitter
Nếu năm mươi worker cùng thất bại một lúc và tất cả đều ngủ đúng hai giây, chúng sẽ cùng thử lại và lại cùng thất bại. Một phần nhỏ ngẫu nhiên của giây sẽ dàn các lần thử ra. Đây cũng là lời khuyên mà hầu hết các nhà cung cấp API lớn đưa ra cho client của họ.
Timeout
Đặt timeout HTTP của bạn cao hơn đáng kể so với thời gian giải thông thường, khoảng 185 giây. Yêu cầu bị chặn cho đến khi token sẵn sàng, nên một timeout phía client ngắn có thể biến một thành công chậm thành một thất bại mà bạn thử lại vô ích. Một timeout phía client dài hơn lần giải là thiết lập an toàn.
Token không thể cache
Token hết hạn trong khoảng hai phút. Đừng cache token giữa các lần chạy hay dùng lại một token cho lần submit thứ hai. Nếu một lần submit thất bại sau khi token đã được dùng, hãy giải lại thay vì gửi lại token cũ. Hướng dẫn hết hạn token trình bày chi tiết hơn về thời gian.
Đồng thời và backoff giữa các worker
Nếu nhiều worker dùng chung một khóa, backoff ở một worker không làm chậm các worker khác. Hãy thêm một giới hạn chung cho số lần giải đồng thời, chẳng hạn một semaphore hoặc hàng đợi, để một đợt thử lại không biến thành một đợt yêu cầu bùng nổ. Một giới hạn đồng thời đơn giản giải quyết phần lớn các lỗi giới hạn tần suất trước khi chúng xuất hiện.
Ghi log hữu ích
Với mỗi lần thử thất bại, hãy ghi errorCode, số thứ tự lần thử, sitekey, host của trang và việc lỗi có được thử lại hay không. Đừng ghi khóa API hay mật khẩu proxy. Một bảng điều khiển đếm mã theo giờ cho bạn biết nhanh vấn đề nằm ở code, proxy hay dịch vụ.
Các bước tiếp theo
- Cách giải PopularCaptcha bằng Python
- Proxy hay proxyless: bạn cần loại tác vụ nào?
- Giữ CI xanh khi ứng dụng của bạn có captcha
- Dùng thử bộ giải captcha miễn phí
Kiểm thử cách xử lý của bạn với bản dùng thử miễn phí: tạo tài khoản và bắt đầu với tín dụng dùng thử miễn phí, không cần thẻ.