asyncio é poderoso, mas requer reescrever toda a sua cadeia de chamadas como assíncrona. ThreadPoolExecutor oferece paralelismo com código síncrono padrão – coloque-o em projetos existentes sem reestruturação.
Por que ThreadPoolExecutor para CAPTCHAs
A resolução de CAPTCHA é I/O-bound (aguardando respostas HTTP). Threads Python liberam o GIL durante operações I/O, tornando ThreadPoolExecutor eficiente para esta carga de trabalho:
| Abordagem | Complexidade | Adapta-se ao código existente | Paralelismo para I/O |
|---|---|---|---|
| Sequencial | Nenhum | Sim | Nenhum |
| ThreadPoolExecutor | Baixo | Sim | Bom |
| assíncio | Alto | Requer reescrita assíncrona | mais adequado |
| multiprocessamento | Médio | Principalmente | Exagero para I/O |
Implementação Básica
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")
Usando sessão para reutilização de conexão
Criar uma nova conexão TCP por solicitação é uma perda de tempo. Compartilhe um requests.Session por tópico:
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() para operações em lote simples
Quando você não precisa de tratamento de erros por tarefa:
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)}")
Proteção de tempo limite
Evite que threads descontrolados bloqueiem seu pool:
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}")
Retorno de chamada de progresso
Acompanhe a conclusão em tempo real:
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
Escolhendo max_workers
| Trabalhadores | Soluções simultâneas | Despesas gerais | mais adequado para |
|---|---|---|---|
| 5 | 5 | Muito baixo | Lotes pequenos, uso conservador |
| 10 | 10 | Baixo | Uso geral |
| 25 | 25 | Moderado | Pipelines de alto volume |
| 50 | 50 | Superior | Taxa de transferência máxima |
Mais trabalhadores significam mais conexões API simultâneas. Comece em 10, aumente enquanto monitora as taxas de erro.
ThreadPoolExecutor vs assíncrono
# 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)
Use ThreadPoolExecutor quando:
- Sua base de código existente é síncrona
- Você usa bibliotecas que não suportam assíncrono (Selenium, alguns ORMs)
- Você quer paralelismo rápido sem reestruturação
Use assíncio quando:
- Construindo do zero
- A eficiência máxima é importante (menos threads do sistema operacional)
- Já em um framework assíncrono (FastAPI, aiohttp)
Solução de problemas
| Problema | Causa | Correção |
|---|---|---|
| Todos os tópicos bloqueados | Cada thread aguardando time.sleep durante a votação |
Isso é esperado – threads liberam GIL durante o sono |
Picos ConnectionError |
Muitas conexões simultâneas | Reduzir max_workers; usar pool de conexões |
| Resultados fora de ordem | as_completed retorna em ordem de conclusão |
Use map() para resultados ordenados ou acompanhe com dict |
| Memória crescendo | Grandes objetos de resultados mantidos em futuros | Resultados do processo no loop as_completed; não armazene tudo |
Perguntas frequentes
O GIL evita o paralelismo real?
Não - para I/O-bound funcionar como solicitações HTTP e time.sleep, Python libera o GIL. Seus threads são executados de forma verdadeiramente simultânea durante chamadas de rede. O GIL limita apenas o paralelismo vinculado à CPU.
Quantos CAPTCHAs o ThreadPoolExecutor pode manipular por hora?
Com 10 trabalhadores e tempo médio de resolução de 15 segundos: aproximadamente 2.400 por hora. Com 25 trabalhadores: ~6.000 por hora. O gargalo é o tempo de resolução do CaptchaAI, não o threading do Python.
Devo usar ProcessPoolExecutor?
A solução CAPTCHA é I/O-bound. ProcessPoolExecutor adiciona sobrecarga de comunicação entre processos sem nenhum benefício. Fique com fios.
Próximas etapas
Paralelizar a resolução de CAPTCHA -obtenha sua chave API CaptchaAIe coloque ThreadPoolExecutor em seu pipeline.
Guias relacionados: