Scarnox · Blog

Решатель капчи на Java с HttpClient: рабочий пример (2026)

Решатель капчи на Java — это один HTTP-вызов. Java 11 и новее поставляются с java.net.http.HttpClient, поэтому единственная зависимость этого гайда — Gson для JSON. Вы отправляете sitekey и URL страницы в API Scarnox, читаете solution.token и отправляете токен в форму сайта. Гайд охватывает PopularCaptcha, которую Scarnox сегодня решает в варианте proxyless.

Начните с пробного баланса, карта не нужна и проверьте код на странице, которой вы управляете, прежде чем использовать его где-либо ещё.

Требования

Класс решателя

Класс оборачивает один HttpClient, который хранит cookie, поэтому сессия может пройти через отправку формы. Метод решения задаёт заголовок Bearer, отправляет JSON-тело и проверяет errorId:

// ScarnoxSolver.java
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;

public class ScarnoxSolver {
    private static final String API_URL = "https://api.scarnox.com/api/tasks/create";
    private static final Gson GSON = new Gson();

    private final String apiKey;
    private final HttpClient http = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ALL))
            .build();

    public ScarnoxSolver(String apiKey) {
        this.apiKey = apiKey;
    }

    /** Возвращает токен. Вызов блокируется до готовности токена. */
    public String solve(String sitekey, String pageUrl, boolean invisible) throws Exception {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("captcha_type", "PopularCaptchaTaskProxyless");
        body.put("site_key", sitekey);
        body.put("site_url", pageUrl);
        if (invisible) {
            body.put("is_invisible", true);
        }

        HttpRequest request = HttpRequest.newBuilder(URI.create(API_URL))
                .timeout(Duration.ofSeconds(185))
                .header("Authorization", "Bearer " + apiKey)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(body)))
                .build();

        HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());

        JsonObject data;
        try {
            data = JsonParser.parseString(response.body()).getAsJsonObject();
        } catch (RuntimeException e) {
            throw new IllegalStateException("HTTP " + response.statusCode() + ": not JSON");
        }

        if (data.get("errorId").getAsInt() != 0) {
            throw new IllegalStateException(
                    data.get("errorCode").getAsString() + ": "
                    + data.get("errorDescription").getAsString());
        }

        return data.getAsJsonObject("solution").get("token").getAsString();
    }

    /** Отправляет токен в форму сайта и возвращает HTTP-статус. */
    public int submit(String pageUrl, String email, String token) throws Exception {
        // Сначала загружаем страницу, чтобы выставилась cookie сессии.
        http.send(HttpRequest.newBuilder(URI.create(pageUrl)).GET().build(),
                HttpResponse.BodyHandlers.discarding());

        String form = "email=" + enc(email)
                + "&h-captcha-response=" + enc(token)
                + "&g-recaptcha-response=" + enc(token);

        HttpRequest request = HttpRequest.newBuilder(URI.create(pageUrl))
                .header("Content-Type", "application/x-www-form-urlencoded")
                .POST(HttpRequest.BodyPublishers.ofString(form))
                .build();

        return http.send(request, HttpResponse.BodyHandlers.discarding()).statusCode();
    }

    private static String enc(String value) {
        return URLEncoder.encode(value, StandardCharsets.UTF_8);
    }
}

Заголовок Authorization использует префикс Bearer. Таймаут запроса — 185 секунд, потому что API блокируется до готовности токена. Шага опроса нет.

Главная программа

Метод main решает капчу с несколькими повторами, а затем отправляет токен:

// Main.java
public class Main {
    public static void main(String[] args) throws Exception {
        String apiKey = System.getenv("SCARNOX_API_KEY");
        if (apiKey == null || apiKey.isBlank()) {
            throw new IllegalStateException("Set SCARNOX_API_KEY");
        }

        String pageUrl = "https://example.com/signup";
        String sitekey = "YOUR-SITEKEY-UUID";
        ScarnoxSolver solver = new ScarnoxSolver(apiKey);

        String token = null;
        for (int attempt = 1; attempt <= 3 && token == null; attempt++) {
            try {
                token = solver.solve(sitekey, pageUrl, false);
            } catch (IllegalStateException e) {
                System.err.println("Attempt " + attempt + " failed: " + e.getMessage());
                Thread.sleep(attempt * 1000L);
            }
        }
        if (token == null) {
            throw new IllegalStateException("No token after 3 attempts");
        }

        int status = solver.submit(pageUrl, "you@example.com", token);
        System.out.println("Status: " + status);
    }
}

Запуск:

export SCARNOX_API_KEY=scarnox_YOURAPIKEY
javac -cp gson-2.10.1.jar *.java
java -cp .:gson-2.10.1.jar Main

Почему код устроен именно так

Один клиент на решатель. HttpClient потокобезопасен и предназначен для переиспользования. Создавать его на каждый запрос — значит тратить соединения впустую.

Cookie включены. CookieManager у клиента сохраняет сессию, которая загрузила форму, поэтому токен уходит с cookie, которое ожидает сайт.

Явная проверка ошибки. API возвращает errorId равный 0 при успехе. Любое другое значение приходит вместе с errorCode и errorDescription, и решатель выбрасывает исключение с этим текстом. Токен лежит в solution.token.

Оба поля капчи. Форма получает h-captcha-response и g-recaptcha-response. Большинство сайтов читают одно из них.

Тайминг

Токен истекает примерно через две минуты. Решайте непосредственно перед отправкой, а не в начале длинной задачи. Истечение токена капчи объясняет, как рассчитать окно и отличить истёкший токен от отклонённого.

Невидимые виджеты

Передайте true третьим аргументом в solve, если виджет невидимый. Тогда запрос включает is_invisible: true. Если странице нужен JavaScript-колбэк с токеном, серверный клиент его не вызовет, и лучше подойдёт инструмент автоматизации браузера. Шаблоны перечислены в гайде невидимая капча.

Обработка ошибок

Повторы покрывают временные сбои. Прекращайте повторы на ошибках, которые не изменятся, например при неверном sitekey или URL страницы, где виджет не работает, и записывайте их для разбора. Полная политика, включая коды, на которых стоит остановиться, — в обработке ошибок решателя капчи.

Парсинг с ответственностью

Если программа парсит публичные страницы, сначала проверьте robots.txt и условия сайта. Добавьте паузу между запросами и храните только нужные персональные данные. Правовой контекст — в гайде о законности парсинга сайтов.

Тарифы

Вы платите за каждое успешное решение, неудачные решения не списываются. Расценки — на странице цен. Чтобы начать, возьмите пробный баланс, карта не нужна.

Что дальше