Туториалы

Python ThreadPoolExecutor для решения CAPTCHA параллелизма

asyncio — мощный инструмент, но требует перезаписи всей цепочки вызовов как асинхронной. ThreadPoolExecutor обеспечивает параллелизм со стандартным синхронным кодом — добавьте его в существующие проекты без реструктуризации.

Почему ThreadPoolExecutor для CAPTCHA

Решение CAPTCHA — I/O-bound (ожидание HTTP-ответов). Потоки Python освобождают GIL во время операций I/O, что делает ThreadPoolExecutor эффективным для этой рабочей нагрузки:

Подход Сложность Подходит для существующего кода Параллелизм для I/O
Последовательный Никто Да Никто
ThreadPoolExecutor Низкий Да Хороший
асинхронный Высокий Требуется асинхронная перезапись подходящий
многопроцессорность Середина По большей части Избыток для I/O

Базовая реализация

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")

Использование сеанса для повторного использования соединения

Создание нового TCP-соединения для каждого запроса отнимает время. Поделитесь requests.Session в каждой теме:

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() для простых пакетных операций

Если вам не нужна обработка ошибок для каждой задачи:

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)}")

Защита от тайм-аута

Не позволяйте неконтролируемым потокам блокировать ваш пул:

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}")

Обратный вызов прогресса

Отслеживайте завершение в режиме реального времени:

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

Выбор max_workers

Рабочие Параллельные решения Накладные расходы подходящий для
5 5 Очень низкий Небольшие партии, консервативное использование
10 10 Низкий Общее использование
25 25 Умеренный Трубопроводы большого объёма
50 50 Выше Максимальная пропускная способность

Больше работников означает больше одновременных подключений API. Начните с 10, увеличивайте, отслеживая частоту ошибок.

ThreadPoolExecutor против 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)

Используйте ThreadPoolExecutor, когда:

  • Ваша существующая кодовая база синхронна
  • Вы используете библиотеки, которые не поддерживают асинхронность (Selenium, некоторые ORM)
  • Вам нужен быстрый параллелизм без реструктуризации

Используйте asyncio, когда:

  • Строительство с нуля
  • Максимальная эффективность имеет значение (меньше потоков ОС)
  • Уже в асинхронной среде (FastAPI, aiohttp)

Поиск неисправностей

| Проблема | Причина | Исправить |


Следующие шаги

  • CaptchaAI Quickstart: ваше первое решение CAPTCHA за 5 минут
  • Как решить reCAPTCHA v2 через API: пошаговое руководство
  • Как решить Cloudflare Turnstile через API
  • Как решить GeeTest v3 с помощью API
Комментарии для этой статьи отключены.