Scarnox · Blog

Решатель капчи на C# с HttpClient: пример для .NET 8

В C# решатель капчи — это небольшой класс вокруг HttpClient. Вы отправляете sitekey и URL страницы в API Scarnox, читаете токен из JSON-ответа и отправляете его в форму сайта. Этот гайд строит такой класс для .NET 8 под PopularCaptcha, которую Scarnox сегодня решает в варианте proxyless.

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

Требования

Создание проекта

dotnet new console -n CaptchaDemo
cd CaptchaDemo

Дополнительные пакеты не нужны. System.Net.Http и System.Text.Json входят в состав платформы.

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

Класс ниже владеет одним HttpClient с заголовком Bearer и таймаутом 185 секунд. Он возвращает токен или выбрасывает исключение с текстом ошибки из API:

// ScarnoxSolver.cs
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;

public sealed class ScarnoxSolver : IDisposable
{
    private const string ApiUrl = "https://api.scarnox.com/api/tasks/create";
    private readonly HttpClient _http;

    public ScarnoxSolver(string apiKey)
    {
        _http = new HttpClient { Timeout = TimeSpan.FromSeconds(185) };
        _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
    }

    public async Task<string> SolveAsync(
        string sitekey,
        string pageUrl,
        bool invisible = false,
        CancellationToken ct = default)
    {
        var body = new Dictionary<string, object>
        {
            ["captcha_type"] = "PopularCaptchaTaskProxyless",
            ["site_key"] = sitekey,
            ["site_url"] = pageUrl,
        };
        if (invisible)
        {
            body["is_invisible"] = true;
        }

        // Вызов возвращается, когда токен готов. Опроса нет.
        using var resp = await _http.PostAsJsonAsync(ApiUrl, body, ct);
        var data = await resp.Content.ReadFromJsonAsync<CreateTaskResponse>(cancellationToken: ct)
                   ?? throw new InvalidOperationException(
                       $"HTTP {(int)resp.StatusCode}: empty response");

        if (data.ErrorId != 0)
        {
            throw new InvalidOperationException($"{data.ErrorCode}: {data.ErrorDescription}");
        }

        return data.Solution?.Token
               ?? throw new InvalidOperationException("Response had no token");
    }

    public void Dispose() => _http.Dispose();
}

public sealed class CreateTaskResponse
{
    [JsonPropertyName("errorId")]
    public int ErrorId { get; set; }

    [JsonPropertyName("errorCode")]
    public string? ErrorCode { get; set; }

    [JsonPropertyName("errorDescription")]
    public string? ErrorDescription { get; set; }

    [JsonPropertyName("solution")]
    public Solution? Solution { get; set; }
}

public sealed class Solution
{
    [JsonPropertyName("token")]
    public string? Token { get; set; }
}

Важнее всего строка с проверкой errorId. API возвращает 0 при успехе. Любое другое значение приходит вместе с errorCode и errorDescription, и класс передаёт оба.

Отправка токена

Токен уходит в форму сайта. Используйте второй HttpClient с контейнером cookie, чтобы сессия, которая загрузила форму, переиспользовалась в POST:

// Program.cs
using System.Net;

var apiKey = Environment.GetEnvironmentVariable("SCARNOX_API_KEY")
             ?? throw new InvalidOperationException("Set SCARNOX_API_KEY");

const string pageUrl = "https://example.com/signup";
const string sitekey = "YOUR-SITEKEY-UUID";

using var solver = new ScarnoxSolver(apiKey);
string token = "";

// Повторяем временные сбои. Неудачные решения не списываются.
for (int attempt = 1; attempt <= 3; attempt++)
{
    try
    {
        token = await solver.SolveAsync(sitekey, pageUrl);
        break;
    }
    catch (InvalidOperationException ex) when (attempt < 3)
    {
        Console.Error.WriteLine($"Attempt {attempt} failed: {ex.Message}");
        await Task.Delay(TimeSpan.FromSeconds(attempt));
    }
}

var cookies = new CookieContainer();
var handler = new HttpClientHandler { CookieContainer = cookies, UseCookies = true };
using var site = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(60) };

// Сначала GET, чтобы сайт выставил cookie сессии.
await site.GetAsync(pageUrl);

using var form = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["email"] = "you@example.com",
    ["h-captcha-response"] = token,
    ["g-recaptcha-response"] = token,
});

using var response = await site.PostAsync(pageUrl, form);
Console.WriteLine($"Status: {(int)response.StatusCode}");

Запуск:

set SCARNOX_API_KEY=scarnox_YOURAPIKEY
dotnet run

В PowerShell используйте вместо этого $env:SCARNOX_API_KEY = "scarnox_YOURAPIKEY".

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

**Один HttpClient на решатель.** HttpClient задуман для переиспользования, а не для создания при каждом вызове. Решатель владеет одним экземпляром и освобождает его в конце.

Таймаут 185 секунд. API блокируется до готовности токена. Более короткий таймаут отменит решения, которые вот-вот бы завершились.

Типизированная десериализация. ReadFromJsonAsync сопоставляет JSON с CreateTaskResponse, поэтому проверки ошибок и токена читаются как обычный C#.

Отправка формы. Сайт получает и h-captcha-response, и g-recaptcha-response, потому что сайты читают одно из них. Контейнер cookie сохраняет сессию между GET и POST.

Тайминг

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

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

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

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

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

Тарифы

Scarnox берёт плату за каждое успешное решение, неудачные решения не списываются. Расценки — на странице цен. Начните с пробного баланса, карта не нужна.

Что дальше