Как решить Capy Puzzle на C# и отправить все три поля

solve capy puzzle in c# - How to Solve Capy Puzzle in C# and Submit All Three Fields

Чтобы решить Capy Puzzle на C#, считайте со страницы ключ PUZZLE_ сайта, вызовите CapyAsync с этим ключом и URL страницы, а затем отправьте три возвращённых значения в полях формы capy_captchakey, capy_challengekey и capy_answer, все вместе и сразу же. Вот и вся интеграция, а спотыкаются обычно на форме ответа. Большинство типов, которые вы, возможно, уже решали, возвращают один токен. Capy возвращает три значения, которые работают только в наборе, и одно из них быстро истекает. CapSkip добавил Capy Puzzle в версии 1.4.0. В этом руководстве разобраны ключ, вызов, намеренное удержание ответа на две секунды и отправка.

Что понадобится

  • CapSkip 1.4.0 или новее на машине с Windows. Поддержка Capy появилась именно в этом выпуске, вместе с CaptchaFox и Friendly Captcha.
  • Пакет CapSkip для .NET версии 1.3.0 или новее: именно в этом выпуске появился CapyAsync. Он собран под .NET Standard 2.0 и зависит от System.Text.Json 8, поэтому поддерживается на .NET Framework 4.6.2 и выше, а также на .NET 6 и новее. В примерах используются операторы верхнего уровня и неявные директивы using консольного проекта .NET 6, поэтому запускайте их на .NET 6 или новее.
  • Два значения с целевой страницы: ключ Capy, который начинается с PUZZLE_, и URL страницы, на которой работает виджет. Где лежит ключ, показывает шаг 1, и там же видно, нужно ли вам третье, необязательное значение.
  • Адрес решателя. Режим Local отвечает на 127.0.0.1 и только для этого устройства; режим Server слушает ваш сетевой адрес или публичный IP, чтобы другая машина могла обращаться к нему через API. Оба режима задаются в разделе Настройки подключения, а о том, когда переключаться, рассказывает отдельный раздел ниже.
# dotnet add package CapSkip
dotnet add package CapSkip

Шаг 1: находим ключ PUZZLE_ и хост Capy

Всё, что нужно, чтобы решить Capy Puzzle на C#, берётся с целевой страницы. Ключ публичен и одинаков для всех посетителей, а находится он в двух местах. Параметр k в URL скрипта виджета есть в HTML, который отправляет сервер: именно этот HTML вы видите в исходном коде страницы, и его же получает HttpClient. После того как виджет отработал в браузере, ключ также лежит в скрытом поле capy_captchakey, которое виджет записывает в форму, и там его показывает панель Elements в DevTools.

<!-- In the HTML the server sends: the k parameter of the widget script -->
<script src="https://jp.api.capy.me/puzzle/get_js/?k=PUZZLE_YOUR_KEY"></script>

<!-- Written into the form by the widget once it has run (DevTools only) -->
<input type="hidden" name="capy_captchakey" value="PUZZLE_YOUR_KEY" />

Скопируйте ключ точно, вместе с префиксом. Раз уж вы смотрите на этот тег script, запомните его хост. Всё, что стоит в URL скрипта до /puzzle/get_js/, указывает на Capy API, за которым стоит ключ, и CapSkip называет его api_server. По умолчанию это https://jp.api.capy.me, где и работает действующий сервис, поэтому передавать его нужно, только если страница загружает виджет откуда-то ещё.

Одна ловушка здесь идёт из документации других решателей. В некоторых из них до сих пор указан api.capy.me без регионального префикса, а этот хост больше не резолвится. Если вы скопировали его из старого примера, удалите опцию, и CapSkip возьмёт значение по умолчанию.

Шаг 2: вызов CapyAsync

Метод принимает ключ, URL страницы и необязательный словарь опций. Для страницы на хосте по умолчанию не нужно ничего, кроме первых двух.

// dotnet add package CapSkip
using CapSkip;

var solver = new CapSkipClient(host: "127.0.0.1", port: 8080);

// The PUZZLE_ key, and the page the widget runs on.
var result = await solver.CapyAsync(
    "PUZZLE_YOUR_KEY",
    "https://example.com/login");

Console.WriteLine(result.CaptchaKey);    // capy_captchakey
Console.WriteLine(result.ChallengeKey);  // capy_challengekey
Console.WriteLine(result.Answer);        // capy_answer

Читайте три именованных свойства. Свойство Code содержит тот же ответ в виде сырого JSON, что удобно для логирования, а RespKey возвращается пустым, потому что существует только для совместимости с другими сервисами. Answer представляет собой длинную строку, начало которой выглядит примерно как 0xax8ex0xax84x. Это траектория перетаскивания, которую виджет записал бы при движении фрагмента, а не координата.

Словарь опций принимает api_server, proxy, proxytype и useragent, а также таймаут на один вызов в секундах. User agent, если вы его зададите, отправляется в единственном запросе, которым CapSkip загружает головоломку, и нужен он редко. Есть ещё опция version, но принимается только значение puzzle: другое семейство Capy, avatar, представляет собой иное задание за другим эндпоинтом, поэтому SDK отклоняет его с ValidationException, а не возвращает ответ, который сайт отверг бы. Неизвестное имя опции, пустой ключ PUZZLE_, пустой URL страницы или неподдерживаемый proxytype вызывают то же исключение ещё до какой-либо отправки. Опция со значением null просто отбрасывается, поэтому необязательный хост можно передать без оператора if.

// The script tag's host. jp.api.capy.me is the default, so pass
// this only when the page loads the widget from somewhere else.
var result = await solver.CapyAsync(
    "PUZZLE_YOUR_KEY",
    "https://example.com/login",
    new Dictionary<string, object?>
    {
        ["api_server"] = "https://jp.api.capy.me/",
    });

Почему решение Capy занимает около двух секунд

Само распознавание происходит быстро. CapSkip загружает изображение головоломки, находит вырез с помощью несложной попиксельной математики и строит траекторию перетаскивания, и всё это без браузера и без модели. А затем намеренно ждёт.

Capy измеряет время между выдачей головоломки и получением ответа и отклоняет всё, что приходит быстрее, чем человек успел бы перетащить фрагмент. По собственным замерам CapSkip на Capy нижняя граница составляет около одной секунды: ответ через полсекунды отклоняется, а ответы в диапазоне от одной до четырёх секунд (это самое долгое из проверенного) принимаются. Отказ приходит с тем же сообщением, что и при неверном ответе, так что верное решение, доставленное слишком быстро, выглядит в точности как сломанный решатель. Поэтому CapSkip придерживает каждый результат Capy, пока с момента получения головоломки не пройдут две секунды. Ожидание сделано через sleep: оно добавляет задержку, но не тратит CPU, а поскольку CapyAsync сам опрашивает результат, вы видите лишь вызов, который длится несколько секунд.

Почему несколько, а не две: опросы SDK начинаются с интервалом в четверть секунды, а затем интервал удваивается до значения pollingInterval, по умолчанию 5 секунд. С настройками по умолчанию ответ обычно забирается примерно на четвёртой секунде. Если клиент решает в основном Capy, создайте его с pollingInterval: 0.5, и вызов будет возвращаться гораздо ближе к двум секундам.

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

Шаг 3: отправляем все три значения одним запросом

Три значения идут в те поля, которые виджет сам записал бы в форму. Отправляйте их вместе, в том же запросе, что и остальную форму.

// http is your HttpClient; result comes from Step 2.
var form = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["username"] = "someone",
    ["capy_captchakey"] = result.CaptchaKey!,
    ["capy_challengekey"] = result.ChallengeKey!,
    ["capy_answer"] = result.Answer!,
});

var response = await http.PostAsync("https://example.com/login", form);

Отправляйте ответ ровно в том виде, в каком он вернулся. Это траектория перетаскивания, и бэкенд сайта сверяет её с выданной головоломкой, поэтому обрезка, пересборка из частей или любое другое наведение порядка делают её недействительной. Процентное кодирование для тела формы допустимо, потому что это транспорт, и сервер первым делом его снимает.

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

Повторяйте то, что отправляет настоящая форма. На большинстве страниц есть и собственные скрытые поля, например токен защиты от подделки запросов, а некоторые отправляют данные скриптом в теле JSON, а не обычной отправкой формы. Многие также отклоняют запрос без заголовка User-Agent, а HttpClient его не отправляет, если вы его не добавите. Отправьте форму один раз вручную с открытыми DevTools, скопируйте запрос и поставьте три значения Capy туда, куда их ставит страница.

Шаг 4: сбои, повторы и много решений одновременно

Когда вы решаете Capy Puzzle на C# в сколько-нибудь заметном объёме, часть решений будет проваливаться. Неудачное решение Capy приходит в виде ApiException, и через SDK в обоих важных случаях оно выглядит как нерешаемое. Реагировать на эти случаи нужно противоположным образом, а различить их можно по тому, как часто они случаются. Оба проваливаются рано, ещё до двухсекундного удержания, потому что CapSkip придерживает только решённый ответ.

Что произошлоКак это выглядитЧто делать
Вырез не найденИзредка, и следующая попытка обычно проходитПовторите. Каждая попытка получает совершенно новую головоломку на другой фотографии, так что повтор действительно независим
Capy API отклонил ключПри каждой попытке для этого ключа, а в списке задач Capy в CapSkip видно Invalid captcha keyПроверьте ключ и api_server. CapSkip не повторяет отклонённый ключ, потому что его отклонят точно так же

Промахи редки, поэтому одного повтора хватает почти всегда. Повторять можно в своём коде, как это делает полный пример, или задать Retries в разделе Capy в настройках CapSkip: по умолчанию там 0, а максимум составляет три повтора на задачу.

CapyAsync опрашивает результат с таймаутом defaultTimeout, 120 секунд, потому что решение Capy сводится к одной загрузке и небольшой арифметике, а не к браузерной сессии. Для обычного решения этого хватает с многократным запасом. Поднимите его в конструкторе, если увеличиваете Retries или ставите в очередь намного больше решений, чем CapSkip выполняет одновременно. Настройка Max. Threads в разделе Capy по умолчанию равна 10, и её можно увеличить, а лишние задачи ждут свободного потока.

Когда вы запускаете много решений параллельно, держите каждое решение в паре с его отправкой. Если запустить сотню вызовов CapyAsync через Task.WhenAll и отправлять результаты потом, самые ранние ключи заданий будут стареть, пока завершаются последние решения. Оберните решение и отправку в одну задачу, а число одновременно выполняемых задач ограничьте с помощью SemaphoreSlim с лимитом, равным числу потоков. А на объёме добавьте прокси. Каждое решение получает новую головоломку от Capy API, а равномерный поток таких запросов с одного адреса и есть тот шаблон, ради которого существует ограничение частоты запросов. Настройте пул прокси в CapSkip или передавайте прокси на каждый запрос. Он влияет только на загрузку головоломки, а это единственный запрос, который делает решение Capy.

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

В примерах стоит 127.0.0.1, потому что это верно, пока ваш код и CapSkip делят одну машину. Как только приложение .NET запускается где-то ещё, например в контейнере, на сборочном агенте, на VPS или в сервисе приложений, локальная петля указывает не на ту машину, и первое же решение бросает NetworkException. Переключите CapSkip в режим Server, и он начнёт слушать ваш сетевой адрес или публичный IP, так что любой из этих вариантов сможет обратиться к нему через API. Если маршрут идёт через интернет, используйте статический публичный IP, включите проверку API-ключа и ограничьте порт ожидаемыми адресами с помощью правила Windows Firewall. Это по-прежнему ваша собственная машина с Windows, и решение капчи по-прежнему не тарифицируется.

Клиент сам не читает переменные окружения. Читайте CAPSKIP_HOST и CAPSKIP_API_KEY в своём коде и передавайте их в конструктор, как это делает полный пример, чтобы одна и та же сборка работала и у вас на столе, и на сервере.

Полный рабочий пример

// dotnet add package CapSkip
using System.Text.RegularExpressions;
using CapSkip;

var pageUrl = "https://example.com/login";
var http = new HttpClient();
var solver = new CapSkipClient(
    apiKey: Environment.GetEnvironmentVariable("CAPSKIP_API_KEY") ?? "capskip",
    host: Environment.GetEnvironmentVariable("CAPSKIP_HOST") ?? "127.0.0.1",
    port: 8080,
    // Capy answers after a two second hold; poll often enough to catch it.
    pollingInterval: 0.5);

// The widget script's k parameter. The capy_captchakey input only
// exists once the widget has run in a browser.
var html = await http.GetStringAsync(pageUrl);
var key = Regex.Match(html, "PUZZLE_[A-Za-z0-9_-]+").Value;
if (key.Length == 0)
    throw new InvalidOperationException("No PUZZLE_ key in the HTML; check DevTools.");

// The script URL up to /puzzle/get_js/, when there is one; a null value is dropped.
var host = Regex.Match(html, @"(https://[^""'\s<>]+?)/puzzle/get_js/").Groups[1].Value;
var options = new Dictionary<string, object?>
{
    ["api_server"] = host.Length > 0 ? host : null,
};

try
{
    // A missed hole comes back unsolvable; a retry draws a new puzzle.
    SolveResult? result = null;
    for (var attempt = 1; result is null; attempt++)
    {
        try
        {
            result = await solver.CapyAsync(key, pageUrl, options);
        }
        catch (ApiException ex) when (attempt < 3 && ex.Message.Contains("UNSOLVABLE"))
        {
            Console.WriteLine($"attempt {attempt}: {ex.Message}");
        }
    }

    // All three together, straight away: the challenge key is single-use.
    var form = new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["username"] = "someone",
        ["capy_captchakey"] = result.CaptchaKey!,
        ["capy_challengekey"] = result.ChallengeKey!,
        ["capy_answer"] = result.Answer!,
    });

    // Post wherever the form's action attribute points.
    var response = await http.PostAsync(pageUrl, form);
    Console.WriteLine((int)response.StatusCode);
}
catch (CapSkip.ValidationException ex)
{
    // An empty key or URL, avatar as the version, or an unknown option.
    Console.WriteLine($"not sent: {ex.Message}");
}
catch (CapSkip.TimeoutException)
{
    Console.WriteLine("gave up waiting; defaultTimeout is 120 seconds");
}
catch (CapSkipError ex)
{
    // The third miss, a refused key, or CapSkip unreachable.
    Console.WriteLine($"gave up: {ex.Message}");
}

Регулярное выражение для ключа ищет префикс PUZZLE_ в любом месте HTML, поэтому вытаскивает ключ из URL скрипта виджета, не разбирая сам тег. Поля capy_captchakey в скачанном HTML нет вообще, потому что виджет записывает его только тогда, когда работает в браузере. Регулярное выражение для хоста сохраняет всё, что стоит до /puzzle/get_js/ в URL скрипта по https, включая путь, а если оно ничего не находит, значение null отбрасывается и CapSkip использует хост по умолчанию. Если провалились все три попытки, первым делом проверьте ключ: отклонённый ключ каждый раз отказывает одинаково, а промах почти никогда не случается три раза подряд. Если страница собирает виджет из бандла и ни одно регулярное выражение не срабатывает, откройте вкладку Network и скопируйте оба значения из собственного запроса виджета. Сырой эндпоинт, который стоит за этим методом, описывает справочник API, а все остальные типы капчи, которые решает пакет, собраны на странице сервиса распознавания капчи для C#.

Частые ошибки и что они означают

Что вы видитеПричинаИсправить
Сайт отклоняет отправку, хотя CapSkip вернул все три значенияОтвет был изменён, или до формы дошли только одно или два значенияОтправьте result.Answer без изменений, вместе с двумя другими значениями, одним запросом
Отправка, которая сработала один раз, не проходит при второй попыткеКлюч задания одноразовый и живёт недолгоРешайте заново для каждой отправки и отправляйте сразу
ApiException при каждой попытке для одного ключаCapy API отклонил ключ, или api_server указывает не на тот хостСкопируйте ключ заново, вместе с префиксом, и проверьте хост в теге script
Время от времени ApiException с сообщением, что капча нерешаемаCapSkip не смог найти вырез в этой головоломкеПовторите; следующая попытка получит другую головоломку
Все решения проваливаются после того, как вы скопировали пример от другого сервисаВ примере api_server задан как api.capy.me, а этот хост больше не резолвитсяУберите опцию и используйте хост по умолчанию
ValidationException ещё до какой-либо отправкиПустой ключ или URL страницы, avatar в качестве version либо опция, которую метод не принимаетПроверьте, что регулярное выражение нашло ключ, и уберите опцию, названную в сообщении
По ходу длинного прогона сбоев всё большеВсе головоломки запрашиваются с одного адресаРаспределите решения по пулу прокси, настроенному в CapSkip
TimeoutException при высокой параллельной нагрузкеВ очереди больше задач, чем Max. Threads успевает обработать за 120 секундОграничьте параллельность с помощью SemaphoreSlim или поднимите defaultTimeout
NetworkException на первом решенииCapSkip не запущен либо неверны хост и портЗапустите CapSkip и проверьте, в каком режиме он должен работать: Local или Server
Сборка падает на неоднозначном TimeoutExceptionЭто короткое имя определено и в CapSkip, и в SystemПишите CapSkip.TimeoutException полностью или перехватывайте CapSkipError

FAQ

Почему решение Capy возвращает три значения, а не токен?

Потому что именно это отправляет форма Capy. Токена, выданного сервером, здесь нет ни на одном этапе. Виджет сам генерирует ключ задания, загружает принадлежащую ему головоломку и записывает перетаскивание, а сайт затем отправляет ключ задания и ответ в Capy вместе со своим приватным ключом, чтобы их проверили. CapSkip играет роль виджета, поэтому возвращает то, что виджет записал бы в форму.

Может ли приложение .NET на хостинг-платформе достучаться до решателя?

Да. Переведите CapSkip в режим Server в настройках подключения, чтобы он слушал сетевой адрес, а не локальную петлю, прочитайте этот адрес из CAPSKIP_HOST в своём коде и передайте его клиенту. Хост контейнеров, VPS, агент CI и управляемый сервис приложений подключаются одинаково, через один и тот же HTTP API. Если маршрут идёт через интернет, используйте статический публичный IP с правилом файрвола. Решатель остаётся на вашем собственном железе, поэтому ни лицензия, ни счётчик решений не меняются.

Чем это отличается от решения GeeTest v3 на C#?

Оба представляют собой слайдер-пазлы, которые возвращают несколько значений, отправляемых вместе, но отправная точка у них разная. GeeTest v3 начинается с challenge, который выдаёт сайт, и его нужно загрузить непосредственно перед решением, потому что он истекает примерно за минуту. Capy начинается только с ключа, поскольку ключ задания генерируется в момент решения, так что заранее загружать нечего, а единственный важный отсчёт времени идёт после решения. Сторона GeeTest разобрана в руководстве по GeeTest v3 на C#.

Нужно ли отправлять user agent вместе с формой, как в случае с CaptchaFox?

Нет. Токен CaptchaFox привязан к браузеру, который его выпустил, поэтому отправка должна нести user agent этого браузера, о чём руководство по CaptchaFox на C# подробно рассказывает. Ответ Capy вообще не привязан к браузеру. CapSkip не возвращает для него user agent, а необязательный user agent, который можно передать, влияет только на запрос, загружающий головоломку.

Коротко

Чтобы решить Capy Puzzle на C#, скопируйте ключ PUZZLE_ из URL скрипта виджета или из поля capy_captchakey и запомните хост скрипта на случай, если он не стандартный. Вызовите CapyAsync с ключом и настоящим URL страницы и дайте ему отработать свои несколько секунд. Отправьте CaptchaKey, ChallengeKey и Answer в capy_captchakey, capy_challengekey и capy_answer без изменений, вместе и сразу, и решайте заново для каждой отправки. Повторяйте редкие промахи, добавляйте прокси по мере роста объёма и переходите в режим Server, как только вызывающий код покидает машину решателя.

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