Решатель капчи на C# с HttpClient: пример для .NET 8
В C# решатель капчи — это небольшой класс вокруг HttpClient. Вы отправляете sitekey и URL страницы в API Scarnox, читаете токен из JSON-ответа и отправляете его в форму сайта. Этот гайд строит такой класс для .NET 8 под PopularCaptcha, которую Scarnox сегодня решает в варианте proxyless.
Начните с пробного баланса, карта не нужна и проверьте код на странице, которой вы управляете, прежде чем запускать его где-либо ещё.
Требования
- SDK .NET 8 (
dotnet --version). - Ключ API Scarnox, который читается из переменной окружения.
- Sitekey виджета PopularCaptcha на странице. Помощь с поиском — в гайде о поиске sitekey капчи.
Создание проекта
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 берёт плату за каждое успешное решение, неудачные решения не списываются. Расценки — на странице цен. Начните с пробного баланса, карта не нужна.