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

Чтобы решить 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, как только вызывающий код покидает машину решателя.
- Как работает этот тип и что покрывает решатель: страница решения Capy Puzzle.
- Все остальные типы капчи, которые решает пакет .NET: страница решения капчи для C# и .NET.
И последнее о повторной попытке. Поскольку каждая попытка получает другую головоломку, при промахе правильнее всего просто попробовать снова, а локальный сервис распознавания капчи на вашей собственной машине означает, что вторая попытка стоит несколько секунд, а не ещё одно оплачиваемое решение.
