Scarnox · Blog

Obsługa błędów solvera captcha: ponawianie, backoff i czego nigdy nie ponawiać

Rozwiązanie captcha to wywołanie sieciowe do systemu, którego nie kontrolujesz, dla wyzwania, które może się zmienić bez ostrzeżenia. Dobra obsługa błędów decyduje o tym, czy potok działa łagodnie, czy marnuje czas w lawinie ponowień.

Przy Scarnox ekonomia Ci pomaga: nieudane rozwiązania są bezpłatne, więc ponowienie kosztuje tylko czas i nic więcej. Ale ponawianie niewłaściwego błędu nadal marnuje ten czas, a zbyt agresywne ponawianie może samo spowodować problemy. Ten przewodnik daje politykę ponawiania, którą możesz wkleić do zadania w Pythonie.

Scarnox rozwiązuje dziś PopularCaptcha. Ta sama obsługa błędów dotyczy każdego solvera captcha, więc poniższa polityka nie zależy od typu captcha.

Zawsze najpierw sprawdź errorId

Odpowiedź na rozwiązanie zawiera pole errorId. 0 oznacza sukces, a token znajduje się w solution.token. Każda inna wartość oznacza nieudane rozwiązanie. Nieudana odpowiedź zawiera też errorCode i errorDescription. Zapisuj oba: kod będziesz później wyszukiwać, a opis przeczytasz, gdy uruchomienie pójdzie źle.

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

Nigdy nie zakładaj, że odpowiedź to sukces, bo status HTTP to 200. Sprawdzaj errorId za każdym razem.

Błędy do ponowienia

Niektóre błędy są tymczasowe. Ponawiaj je z wykładniczym backoffem i małym losowym opóźnieniem:

Te błędy warto ponowić, bo kolejna próba może się udać bez żadnej zmiany po Twojej stronie.

Błędy do naprawienia przed ponowieniem

Inne błędy nie znikną z czasem. Ponowienie z tym samym wejściem zakończy się tym samym błędem. Najpierw je napraw:

Gdy błąd nie znajduje się na żadnej z tych list, traktuj go jako błąd do naprawienia najpierw. Przeczytaj tekst błędu, zdecyduj i dodaj kod do właściwej listy w swoim kodzie. Trzymaj listy krótkie i sprawdzone przez kogoś.

Polityka, którą możesz wkleić

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:
        # Błąd sieci lub timeout po naszej stronie: bezpieczne do ponowienia.
        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 zgłosiło błąd. Ponawiaj tylko kody, które zaobserwowano jako przejściowe.
    code = data.get("errorCode", "UNKNOWN")
    retryable = code in TRANSIENT_CODES
    raise SolveError(code, data.get("errorDescription", ""), retryable=retryable)

# Dodawaj tu kody błędów API dopiero po tym, jak zobaczysz w logach, że są przejściowe.
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 + losowe opóźnienie
            print(f"attempt {attempt + 1} failed ({err.code}), retrying in {delay:.1f}s")
            time.sleep(delay)
    raise RuntimeError("unreachable")

Polityka nigdy nie ponawia błędu do naprawienia i zatrzymuje się po stałej liczbie prób. Zbiór TRANSIENT_CODES na początku jest celowo pusty: dodaj kod dopiero wtedy, gdy logi pokażą, że jest tymczasowy.

Dlaczego losowe opóźnienie ma znaczenie

Jeśli pięćdziesiąt workerów zawiedzie w tym samym momencie i wszystkie zaśpią dokładnie na dwie sekundy, ponowią próbę razem i razem znowu zawiodą. Losowa ułamek sekundy je rozprasza. To ta sama rada, którą większość dużych dostawców API daje swoim klientom.

Timeouty

Ustaw timeout HTTP wyraźnie powyżej typowego czasu rozwiązania, czyli około 185 sekund. Żądanie blokuje się, aż token będzie gotowy, więc krótki timeout klienta może zamienić powolny sukces w błąd, który potem ponawiasz na darmo. Bezpieczne ustawienie to timeout dłuższy niż samo rozwiązanie.

Tokeny nie nadają się do cache'owania

Token wygasa w ciągu około dwóch minut. Nie buforuj tokenów między uruchomieniami i nie używaj jednego ponownie do drugiego wysłania. Jeśli wysyłanie nie powiedzie się po tym, jak token został użyty, rozwiąż captcha ponownie zamiast wysyłać stary token. Przewodnik o wygasaniu tokenu omawia czas szczegółowo.

Równoległość i backoff między workerami

Jeśli wielu workerów współdzieli jeden klucz, backoff w jednym workerze nie spowalnia pozostałych. Dodaj wspólny limit równoczesnych rozwiązań, na przykład semafor lub kolejkę, aby seria ponowień nie zamieniła się w serię żądań. Prosty limit równoległości zapobiega większości błędów limitu tempa, zanim w ogóle się pojawią.

Logowanie, które pomaga

Dla każdej nieudanej próby zapisuj errorCode, numer próby, sitekey, host strony i to, czy błąd został ponowiony. Nie loguj klucza API ani hasła do proxy. Panel liczący kody na godzinę szybko pokaże, czy problem leży w Twoim kodzie, w proxy, czy w usłudze.

Następne kroki

Przetestuj swoją obsługę błędów na darmowym okresie próbnym: załóż konto i zacznij od darmowego kredytu próbnego, bez karty.