asyncio mạnh mẽ nhưng yêu cầu viết lại toàn bộ chuỗi cuộc gọi của bạn dưới dạng không đồng bộ. ThreadPoolExecutor cung cấp cho bạn khả năng song song với mã đồng bộ tiêu chuẩn - thả nó vào các dự án hiện có mà không cần cơ cấu lại.
Tại sao ThreadPoolExecutor dành cho CAPTCHA
Giải CAPTCHA là I/O-bound (chờ phản hồi HTTP). Các luồng Python giải phóng GIL trong các hoạt động I/O, giúp ThreadPoolExecutor hoạt động hiệu quả cho khối lượng công việc này:
| Cách tiếp cận | Độ phức tạp | Phù hợp với mã hiện có | Tính song song của I/O |
|---|---|---|---|
| tuần tự | không có | Có | không có |
| ThreadPoolExecutor | Thấp | Có | Tốt |
| asyncio | Cao | Yêu cầu viết lại không đồng bộ | Tốt nhất |
| đa xử lý | Trung bình | Hầu hết | Quá mức cần thiết cho I/O |
Triển khai cơ bản
import os
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
def solve_captcha(sitekey, pageurl):
"""Synchronous CAPTCHA solve — submit and poll."""
# Submit
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
raise RuntimeError(data.get("request", "Submit failed"))
captcha_id = data["request"]
# Poll for result
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": captcha_id,
"json": 1
}).json()
if result.get("status") == 1:
return result["request"]
if result.get("request") != "CAPCHA_NOT_READY":
raise RuntimeError(result.get("request", "Unknown error"))
raise TimeoutError("Solve timeout after 300s")
# Batch solve with ThreadPoolExecutor
tasks = [
{"sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-", "pageurl": f"https://example.com/page/{i}"}
for i in range(20)
]
start = time.time()
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {
executor.submit(solve_captcha, t["sitekey"], t["pageurl"]): t
for t in tasks
}
solved = 0
failed = 0
for future in as_completed(futures):
task = futures[future]
try:
solution = future.result()
solved += 1
print(f"[OK] {task['pageurl']}: {solution[:30]}...")
except Exception as e:
failed += 1
print(f"[ERR] {task['pageurl']}: {e}")
elapsed = time.time() - start
print(f"\nDone: {solved} solved, {failed} failed in {elapsed:.1f}s")
Sử dụng phiên để tái sử dụng kết nối
Tạo kết nối TCP mới cho mỗi yêu cầu sẽ lãng phí thời gian. Chia sẻ requests.Session trên mỗi chủ đề:
import threading
# Thread-local storage for sessions
thread_local = threading.local()
def get_session():
"""Get or create a thread-local session."""
if not hasattr(thread_local, "session"):
thread_local.session = requests.Session()
# Configure connection pooling
adapter = requests.adapters.HTTPAdapter(
pool_connections=10,
pool_maxsize=10,
max_retries=2
)
thread_local.session.mount("https://", adapter)
return thread_local.session
def solve_captcha_pooled(sitekey, pageurl):
"""Solve using thread-local connection pooling."""
session = get_session()
resp = session.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
raise RuntimeError(data.get("request"))
captcha_id = data["request"]
for _ in range(60):
time.sleep(5)
result = session.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": captcha_id,
"json": 1
}).json()
if result.get("status") == 1:
return result["request"]
if result.get("request") != "CAPCHA_NOT_READY":
raise RuntimeError(result.get("request"))
raise TimeoutError("Solve timeout")
map() cho các hoạt động hàng loạt đơn giản
Khi bạn không cần xử lý lỗi theo từng tác vụ:
def solve_task(task):
"""Wrapper that returns result dict."""
try:
solution = solve_captcha_pooled(task["sitekey"], task["pageurl"])
return {"url": task["pageurl"], "solution": solution, "error": None}
except Exception as e:
return {"url": task["pageurl"], "solution": None, "error": str(e)}
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(solve_task, tasks))
solved = [r for r in results if r["solution"]]
failed = [r for r in results if r["error"]]
print(f"Solved: {len(solved)}, Failed: {len(failed)}")
Bảo vệ thời gian chờ
Ngăn chặn các chủ đề chạy trốn chặn nhóm của bạn:
from concurrent.futures import TimeoutError as FuturesTimeout
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {
executor.submit(solve_captcha_pooled, t["sitekey"], t["pageurl"]): t
for t in tasks
}
for future in as_completed(futures, timeout=600): # 10 min global timeout
task = futures[future]
try:
solution = future.result(timeout=120) # 2 min per task
print(f"[OK] {task['pageurl']}")
except FuturesTimeout:
print(f"[TIMEOUT] {task['pageurl']}")
except Exception as e:
print(f"[ERR] {task['pageurl']}: {e}")
Gọi lại tiến độ
Theo dõi hoàn thành trong thời gian thực:
import threading
progress_lock = threading.Lock()
progress = {"done": 0, "total": 0}
def solve_with_progress(task):
result = solve_task(task)
with progress_lock:
progress["done"] += 1
pct = progress["done"] / progress["total"] * 100
print(f'\r Progress: {progress["done"]}/{progress["total"]} ({pct:.0f}%)', end="")
return result
progress["total"] = len(tasks)
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(solve_with_progress, tasks))
print() # Newline after progress
Chọn max_workers
| Công nhân | Giải quyết đồng thời | Chi phí chung | Tốt nhất cho |
|---|---|---|---|
| 5 | 5 | Rất thấp | Lô nhỏ, sử dụng thận trọng |
| 10 | 10 | Thấp | sử dụng chung |
| 25 | 25 | Trung bình | Đường ống khối lượng lớn |
| 50 | 50 | Cao hơn | Thông lượng tối đa |
Nhiều công nhân hơn có nghĩa là có nhiều kết nối API đồng thời hơn. Bắt đầu ở mức 10, tăng dần trong khi theo dõi tỷ lệ lỗi.
ThreadPoolExecutor vs asyncio
# ThreadPoolExecutor — drop into existing sync code
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(solve_task, tasks))
# asyncio — requires async function chain
async def main():
async with aiohttp.ClientSession() as session:
tasks = [solve_async(session, t) for t in task_list]
results = await asyncio.gather(*tasks)
Sử dụng ThreadPoolExecutor khi:
- Cơ sở mã hiện tại của bạn là đồng bộ
- Bạn sử dụng các thư viện không hỗ trợ async (Selenium, một số ORM)
- Bạn muốn song song nhanh chóng mà không cần tái cơ cấu
Sử dụng asyncio khi:
- Xây dựng từ đầu
- Vấn đề hiệu quả tối đa (ít luồng hệ điều hành hơn)
- Đã có trong khung không đồng bộ (FastAPI, aiohttp)
Khắc phục sự cố
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
| Tất cả các chủ đề bị chặn | Mọi chủ đề đang chờ trên time.sleep trong quá trình bỏ phiếu |
Điều này được mong đợi - các luồng giải phóng GIL trong khi ngủ |
ConnectionError gai |
Quá nhiều kết nối đồng thời | Giảm max_workers; sử dụng kết nối tổng hợp |
| Kết quả không đúng thứ tự | as_completed trả về theo thứ tự hoàn thành |
Sử dụng map() để có kết quả được sắp xếp hoặc theo dõi bằng dict |
| Trí nhớ ngày càng tăng | Các đối tượng có kết quả lớn được nắm giữ trong tương lai | Xử lý kết quả theo vòng lặp as_completed; không lưu trữ tất cả |
Câu hỏi thường gặp
GIL có ngăn chặn sự song song thực sự không?
Không - để I/O-bound hoạt động giống như các yêu cầu HTTP và time.sleep, Python sẽ phát hành GIL. Chủ đề của bạn chạy thực sự đồng thời trong các cuộc gọi mạng. GIL chỉ giới hạn tính song song của CPU.
ThreadPoolExecutor có thể xử lý bao nhiêu CAPTCHA mỗi giờ?
Với 10 công nhân và thời gian giải quyết trung bình 15 giây: ~2.400 mỗi giờ. Với 25 công nhân: ~6.000/giờ. Nút thắt là thời gian giải quyết CaptchaAI chứ không phải luồng Python.
Tôi có nên sử dụng ProcessPoolExecutor thay thế không?
Không. Việc giải CAPTCHA là I/O-bound. ProcessPoolExecutor bổ sung chi phí liên lạc giữa các quá trình mà không mang lại lợi ích gì. Gắn bó với chủ đề.
Các bước tiếp theo
Song song hóa việc giải CAPTCHA –lấy khóa API CaptchaAI của bạnvà thả ThreadPoolExecutor vào đường dẫn của bạn.
Hướng dẫn liên quan: