Scarnox · Blog

Обробка помилок солвера капчі: повтори, backoff і що ніколи не повторювати

Розв'язання капчі — це мережевий виклик до системи, якою ви не керуєте, для виклику, який може змінитися без попередження. Те, як ви обробляєте збої, визначає, чи пайплайн деградує плавно, чи потрапляє у шторм повторів.

У Scarnox економіка працює на вас: невдалі розв'язання безкоштовні, тож повтор коштує лише часу і більше нічого. Але повторювання не того збою все одно марнує цей час, а надто наполегливі повтори можуть створити власні проблеми. Цей посібник дає політику повторів, яку можна вставити в завдання на Python.

Scarnox розв'язує PopularCaptcha вже зараз. Та сама обробка помилок стосується будь-якого солвера капчі, тож політика нижче не залежить від типу капчі.

Завжди спочатку перевіряйте errorId

У відповіді на розв'язання є поле errorId. Значення 0 означає успіх, а токен лежить у solution.token. Будь-яке інше значення означає, що розв'язання не вдалося. Невдала відповідь також містить errorCode і errorDescription. Записуйте обидва: код ви шукатимете пізніше, а опис читатимете, коли запуск пішов не так.

{"errorId": 1, "errorCode": "...", "errorDescription": "..."}

Ніколи не вважайте відповідь успішною лише через HTTP-статус 200. Перевіряйте errorId щоразу.

Збої, які варто повторити

Деякі збої тимчасові. Повторюйте їх з експоненційним backoff і невеликою випадковою затримкою:

Ці збої варто повторити, бо наступна спроба може пройти успішно без жодних змін з вашого боку.

Збої, які спершу треба виправити

Інші збої не зникнуть від очікування. Повтор із тим самим вхідним значенням провалиться так само. Спершу виправте їх:

Коли збій не входить до жодного зі списків, вважайте його таким, що спершу потребує виправлення. Перегляньте текст помилки, вирішіть і додайте код до правильного списку у вашому коді. Тримайте списки короткими і переглянутими.

Політика, яку можна вставити

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:
        # Мережева помилка або таймаут з нашого боку: повторювати безпечно.
        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 повідомив про збій. Повторюємо лише коди, які ви бачили як тимчасові.
    code = data.get("errorCode", "UNKNOWN")
    retryable = code in TRANSIENT_CODES
    raise SolveError(code, data.get("errorDescription", ""), retryable=retryable)

# Додавайте коди помилок API сюди, лише після того, як побачили в журналах, що вони тимчасові.
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")

Політика ніколи не повторює збій, що потребує виправлення, і зупиняється після фіксованої кількості спроб. Набір TRANSIENT_CODES навмисно порожній на старті: додавайте код лише тоді, коли журнали показують, що він тимчасовий.

Чому важливий jitter

Якщо п'ятдесят воркерів провалюються в один момент і всі спимо рівно дві секунди, вони всі повторять спробу разом і знову провалються разом. Випадкова частка секунди їх розводить. Це та сама порада, яку більшість великих провайдерів API дає своїм клієнтам.

Таймаути

Встановіть таймаут HTTP значно вище типового часу розв'язання, близько 185 секунд. Запит блокується, доки токен не буде готовий, тож короткий клієнтський таймаут може перетворити повільний успіх на збій, який ви потім безглуздо повторюєте. Клієнтський таймаут, довший за розв'язання, — безпечне налаштування.

Токени не кешуються

Токен спливає приблизно за дві хвилини. Не кешуйте токени між запусками і не використовуйте один токен для другої відправки. Якщо відправка провалилася вже після використання токена, розв'яжіть капчу знову, а не відправляйте старий токен повторно. Більше про терміни дії дивіться в посібнику про термін дії токена.

Паралельність і backoff між воркерами

Якщо багато воркерів ділять один ключ, backoff в одному воркері не сповільнює інші. Додайте спільний ліміт одночасних розв'язань, наприклад семафор або чергу, щоб сплеск повторів не перетворився на сплеск запитів. Проста обмеженість паралельності розв'язує більшість проблем з лімітами частоти ще до їх появи.

Логування, яке допомагає

Для кожної невдалої спроби записуйте errorCode, номер спроби, sitekey, хост сторінки і те, чи збій повторено. Не записуйте ключ API або пароль проксі. Панель, що рахує коди за годину, швидко показує, чи проблема в вашому коді, проксі або в сервісі.

Наступні кроки

Перевірте свою обробку на безкоштовній пробній версії: створіть обліковий запис і почніть з безкоштовного пробного кредиту, картка не потрібна.