Scarnox · Blog

Быстрое решение капчи: как сократить задержку в конвейере

Быстрый решатель капчи — это тот, который вы вызываете в правильный момент, параллельно и без лишних запросов. Вызов Scarnox возвращается, когда токен готов, поэтому его длительность зависит от типа капчи, сайта и текущей нагрузки. Этот гайд не обещает фиксированного времени. Он показывает, что контролирует ваш код, как это измерить и как запускать много решений сразу, чтобы время, которое вы тратите, шло с пользой.

Что определяет время решения

Важны три вещи, и только одна из них в ваших руках:

Сосредоточьтесь на третьем пункте. Его вы можете измерить и изменить.

Сначала измерьте, потом оптимизируйте

Оберните вызов таймером и записывайте результат. Нужно знать и время решения, и время, которое ваш код тратит вокруг него:

import time
import requests

API_KEY = "scarnox_YOURAPIKEY"
URL = "https://api.scarnox.com/api/tasks/create"

def timed_solve(sitekey: str, page_url: str) -> dict:
    started = time.perf_counter()
    r = requests.post(
        URL,
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={
            "captcha_type": "PopularCaptchaTaskProxyless",
            "site_key": sitekey,
            "site_url": page_url,
        },
        timeout=185,
    )
    elapsed = time.perf_counter() - started
    data = r.json()
    if data.get("errorId") != 0:
        return {"ok": False, "error": data.get("errorCode"), "seconds": elapsed}
    return {"ok": True, "token": data["solution"]["token"], "seconds": elapsed}

result = timed_solve("10000000-ffff-ffff-ffff-000000000001", "https://example.com/signup")
print(result["ok"], round(result["seconds"], 2), "s")

Запускайте это на страницах, которые важны для вас, в те часы, когда вы запускаете задачи. Сохраняйте цифры. Они покажут, действительно ли изменение помогло.

Запускайте решения параллельно

Решения независимы друг от друга. Если у вас десять sitekey, ожидание каждого по очереди тратит время, которое можно потратить на остальные. Для большинства скриптов достаточно пула потоков, потому что каждое решение — это один HTTP-запрос, который большую часть времени просто ждёт:

from concurrent.futures import ThreadPoolExecutor

jobs = [
    ("10000000-ffff-ffff-ffff-000000000001", "https://example.com/signup"),
    ("10000000-ffff-ffff-ffff-000000000001", "https://example.com/register"),
    ("10000000-ffff-ffff-ffff-000000000001", "https://example.com/contact"),
]

def run(job):
    sitekey, page_url = job
    return timed_solve(sitekey, page_url)

# Начинайте с малого. Увеличивайте число воркеров, только пока ошибок мало.
with ThreadPoolExecutor(max_workers=5) as pool:
    results = list(pool.map(run, jobs))

for r in results:
    status = "ok" if r["ok"] else r["error"]
    print(f"{r['seconds']:.2f} s  {status}")

Если при увеличении числа воркеров появляются ошибки, похожие на проблемы с лимитом или ёмкостью, уменьшите его. Параллельность помогает только тогда, когда сервер за ней успевает.

Не решайте заранее

Самый быстрый код — тот, который не ждёт токена, которым ещё не воспользуется. Запрашивать токены заранее и хранить их — частая ошибка. Токены истекают примерно через две минуты после выдачи, поэтому сохранённый токен часто устаревает к моменту, когда он нужен. Решайте, когда форма вот-вот будет отправлена. Подробные причины описаны в истечении токена капчи.

Держите остальной путь коротким

Переиспользуйте то, что не истекает

Часть работы можно безопасно разделять между решениями. Уберите это из горячего пути:

Эти изменения стоят мало и убирают накладные расходы, которые есть в каждом вызове.

Измеряйте хвост, а не среднее

Среднее время решения скрывает медленные вызовы, которые задерживают пакет. Записывайте время каждого решения, затем смотрите медиану и 95-й перцентиль. Конвейер, который заканчивается вместе с самой медленной задачей, ограничен хвостом, поэтому небольшое изменение в том, как вы повторяете медленные вызовы, может повлиять сильнее, чем любое изменение медианы:

import statistics

def summarise(seconds: list[float]) -> str:
    ordered = sorted(seconds)
    p95 = ordered[int(0.95 * (len(ordered) - 1))]
    return f"median {statistics.median(ordered):.2f} s, p95 {p95:.2f} s, n={len(ordered)}"

print(summarise([r["seconds"] for r in results]))

Сравнивайте сводку до и после каждого изменения. Если p95 почти не сдвинулся, время тратится где-то ещё в вашем коде.

Проверяйте результат, а не только скорость

Быстрый сбой — всё равно сбой. Считайте решения, которые вернули ошибку, и воспринимайте растущую долю ошибок как сигнал замедлиться. Неудачные решения не списываются, но время, потраченное на них, всё равно время, которое потерял ваш конвейер. Полный паттерн повторов — в обработке ошибок решателя капчи.

Что дальше