Как решить Capy Puzzle на PHP с помощью Symfony BrowserKit

Чтобы решить Capy Puzzle на PHP, откройте страницу в HttpBrowser из Symfony и прочитайте ключ PUZZLE_ из скрипта виджета. Передайте его вместе с URL страницы в метод capy пакета capskip/capskip. Затем отправьте три возвращённых значения в полях capy_captchakey, capy_challengekey и capy_answer из той же сессии браузера. HttpBrowser (класс, пришедший на смену Goutte) делает за вас большую часть работы: он хранит cookies, следует редиректам и отправляет страницу в качестве Referer. Подвох кроется в его объекте формы. Поля capy_ появляются только после того, как отработал JavaScript виджета, поэтому DomCrawler отказывается их заполнять и выбрасывает Unreachable field. CapSkip добавил Capy Puzzle в версии 1.4.0. В этом руководстве разобраны ключ, вызов, удержание ответа на две секунды, отправка в обход этого отказа, повторы и решатель на другой машине.
Что понадобится
- CapSkip 1.4.0 или новее на машине с Windows. Именно в этом выпуске появился Capy Puzzle, а вместе с ним CaptchaFox и Friendly Captcha.
- PHP 8.4.1 или новее для компонентов Symfony 8. На PHP 8.2 или 8.3 Composer вместо них выберет выпуски 7.4 LTS, и код ниже без изменений работает в обоих вариантах. Пакету capskip также нужны расширения curl и json.
- Пакет capskip/capskip версии 1.3.0 или новее (первый выпуск, в котором есть метод capy), а также четыре компонента Symfony: BrowserKit, HttpClient, Mime и CssSelector.
- URL страницы, на которой показана головоломка. Ключ, хост Capy и форма берутся именно с этой страницы.
- Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства. В режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы скрипт на другой машине мог обращаться к нему через API. Оба режима задаются в разделе Настройки подключения.
# symfony/mime is needed for any POST, css-selector for filter() composer require capskip/capskip symfony/browser-kit symfony/http-client symfony/mime symfony/css-selector
Composer устанавливает DomCrawler вместе с BrowserKit, но остальные три компонента BrowserKit не требует, поэтому укажите их сами. Без HttpClient не получится создать HttpBrowser, а без CssSelector вызовы filter() ниже выбрасывают LogicException. Mime подводит позже всех. Загрузка страницы без него работает, а падает только отправка формы, с исключением LogicException, которое предлагает установить symfony/mime.
Шаг 1: открываем страницу в HttpBrowser и находим ключ
HttpBrowser оборачивает HTTP-клиент Symfony в небольшой браузер: хранилище cookie, история и редиректы, по которым он переходит сам. Этого хватает для сессионного cookie и CSRF-токена, с которыми обычно приходит форма входа с Capy. Однако при статусе ошибки он исключение не выбрасывает. Ответ 403 или 404 возвращается как обычная страница, поэтому проверяйте статус, прежде чем искать ключ, как это делает блок кода. В документации компонента BrowserKit описана остальная часть его API.
<?php
// composer require capskip/capskip symfony/browser-kit symfony/http-client symfony/mime symfony/css-selector
require __DIR__ . '/vendor/autoload.php';
use Symfony\Component\BrowserKit\HttpBrowser;
use Symfony\Component\HttpClient\HttpClient;
const PAGE_URL = 'https://example.com/login';
// HttpBrowser keeps the cookies, follows redirects and sends a Referer.
$browser = new HttpBrowser(HttpClient::create());
$page = $browser->request('GET', PAGE_URL);
$status = $browser->getInternalResponse()->getStatusCode();
if ($status !== 200) {
throw new RuntimeException("The page answered {$status}, not the form.");
}
// The widget script's k= parameter is the key, and everything before
// /puzzle/get_js/ is the Capy host.
$script = $page->filter('script[src*="/puzzle/get_js/"]');
$src = $script->count() ? $script->attr('src') : '';
if (str_starts_with($src, '//')) {
$src = 'https:' . $src;
}
parse_str((string) parse_url($src, PHP_URL_QUERY), $query);
$key = $query['k'] ?? $page->filter('input[name="capy_captchakey"]')->attr('value', '');
$apiServer = strstr($src, '/puzzle/get_js/', true) ?: null;
// The form around the widget, or else the page's first POST form.
$widget = $page->filter('script[src*="/puzzle/get_js/"], input[name="capy_captchakey"]');
$formNode = ($widget->count() ? $widget->closest('form') : null)
?? $page->filter('form')->reduce(fn ($f) => strtolower((string) $f->attr('method')) === 'post')->first();
echo $key, ' ', $apiServer ?? 'default host', ' ', $formNode->count(), PHP_EOL;Ключ публичен и одинаков для всех посетителей, а ключ головоломки начинается с PUZZLE_. На большинстве страниц он стоит в параметре k в URL скрипта виджета, а этот URL входит в HTML, который скачивает HttpBrowser. Некоторые страницы также выводят в форме скрытое поле capy_captchakey, и если скрипта нет, блок кода читает ключ оттуда. Всё, что стоит в URL скрипта до /puzzle/get_js/, указывает на Capy API, за которым стоит ключ, и CapSkip называет его api_server. По умолчанию это https://jp.api.capy.me, где и работает действующий сервис. В примерах, написанных для других решателей, часто указан api.capy.me без регионального префикса, а этот хост больше не резолвится, так что уберите эту настройку, если скопировали такой пример. Если src начинается с двух слэшей, к нему спереди дописывается префикс https: (Capy API работает только по HTTPS).
Последние строки выбирают форму. Вызов closest() поднимается от виджета к форме, в которой он находится. Если скрипт стоит в head или после формы, никакая форма его не содержит, и блок кода берёт первую POST-форму на странице. Если на странице две POST-формы, выбирайте свою по её id через filter().
Шаг 2: вызов capy()
Чтобы решить Capy Puzzle на PHP, нужен один метод. Он принимает ключ, URL страницы и необязательный массив опций, а для страницы на хосте по умолчанию хватает первых двух.
// composer require capskip/capskip
use CapSkip\CapSkip;
$solver = new 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 page's real URL, after any redirect. A null api_server (no widget
// script in the HTML) is dropped, so CapSkip uses its default host.
$pageUrl = $browser->getHistory()->current()->getUri();
$result = $solver->capy($key, $pageUrl, ['api_server' => $apiServer]);
echo $result['captchakey'], PHP_EOL; // goes in capy_captchakey
echo $result['challengekey'], PHP_EOL; // goes in capy_challengekey
echo $result['answer'], PHP_EOL; // goes in capy_answerРезультат представляет собой массив. Читайте три именованных ключа. В ключе code лежит весь ответ сервера, массив со всеми четырьмя полями, что удобно для логирования. respKey содержит пустую строку, которая нужна только для совместимости с другими сервисами. Ответ (answer) представляет собой длинную строку, которая начинается примерно так: 0xax8ex0xax84x. Это траектория перетаскивания, которую виджет записал бы при движении фрагмента, а не координата.
Передавайте URL, на котором браузер в итоге оказался, как это делает блок кода, а не адрес, который вы ввели. Сайт может перенаправить этот адрес, чтобы добавить завершающий слэш или языковой префикс. CapSkip отправляет URL страницы в качестве Referer, когда загружает головоломку, поэтому это должна быть та страница, с которой на самом деле пришла форма.
Опции, которые принимает метод
Помимо api_server, который откликается и на имя apiServer, метод принимает массив proxy с ключами type и uri. Он также принимает useragent, который CapSkip отправляет в единственном запросе, которым загружает головоломку. Любая опция со значением null отбрасывается, поэтому вызов выше может каждый раз передавать разобранный хост. Есть и опция version, но принимается только значение puzzle. Другое семейство Capy, avatar, представляет собой иное задание за другим эндпоинтом, поэтому SDK отклоняет его, а не возвращает ответ, который сайт отверг бы.
Две опции отдельного вызова стоит знать. Параметр timeout в секундах заменяет defaultTimeout клиента для этого одного решения. Потолок интервала опроса тоже можно задать на один вызов, но только как polling_interval. pollingInterval в camelCase относится к конструктору, и если передать его в capy(), метод выбросит ValidationException с названием этой опции. Пустой ключ или URL, неизвестная опция и тип прокси, отличный от HTTP, HTTPS, SOCKS5 или SOCKS5H, тоже вызывают ValidationException ещё до того, как запрос покинет вашу машину.
Почему решение Capy занимает около двух секунд
Найти вырез удаётся быстро. CapSkip загружает изображение головоломки, определяет положение выреза с помощью несложной попиксельной математики и строит траекторию перетаскивания, без всякого браузера. А затем намеренно ждёт.
Capy засекает время между выдачей головоломки и получением ответа и отклоняет ответ, который приходит быстрее, чем человек успел бы перетащить фрагмент. Такой отказ ничем не отличается от отказа при неверном ответе, так что верное решение, доставленное слишком рано, выглядит в точности как сломанный решатель. Поэтому CapSkip придерживает каждый результат Capy, пока с момента получения головоломки не пройдут две секунды. Удержание сделано через sleep: оно добавляет задержку, но не тратит CPU.
С настройками по умолчанию вызов занимает ближе к четырём секундам, и дело в том, как SDK опрашивает результат. Он запрашивает результат сразу после отправки задания, затем ещё раз через четверть секунды и каждый раз удваивает интервал до своего pollingInterval в 5 секунд. Опросы приходятся на 0, 0.25, 0.75, 1.75 и 3.75 секунды, так что ответ, готовый через две секунды, ждёт пятого опроса. В наших тестах клиент с настройками по умолчанию возвращал результат через 3.8 секунды, а клиент, созданный с pollingInterval, равным 0.5, как выше, через 2.3. Не добавляйте собственную задержку перед отправкой формы и не пытайтесь срезать удержание. Именно оно позволяет ответу пройти проверку.
Шаг 3: добавляем три поля capy_ и отправляем форму
Именно здесь код на BrowserKit обычно и ломается. Первое, что приходит в голову: заполнить объект формы и отправить его через setValues(), через аргумент values метода submit() или присвоив значение полю как элементу массива. Все три способа выбрасывают InvalidArgumentException с текстом Unreachable field и именем поля. DomCrawler строит форму по разобранному HTML. Виджет Capy записывает свои три поля в форму с помощью JavaScript, когда работает в настоящем браузере. HttpBrowser не выполняет JavaScript, поэтому, насколько известно DomCrawler, этих полей не существует.
// The form's own fields, the hidden CSRF token included.
$form = $formNode->form();
// $form->setValues() would throw Unreachable field: the widget never
// ran, so the form has no capy_ fields. Send them through request().
$fields = array_replace($form->getPhpValues(), [
// Use the page's own field names, password included.
'username' => 'YOUR_USERNAME',
'capy_captchakey' => $result['captchakey'],
'capy_challengekey' => $result['challengekey'],
'capy_answer' => $result['answer'],
]);
$browser->request($form->getMethod(), $form->getUri(), $fields);
// After a redirect, this is where the login landed.
echo $browser->getInternalResponse()->getStatusCode(), ' ',
$browser->getHistory()->current()->getUri(), PHP_EOL;Решение такое: используйте форму для того, что она знает, а запрос отправьте сами. Метод getPhpValues() возвращает поля формы так, как их отправил бы браузер, без кнопки, но со скрытым CSRF-токеном, во вложенном виде, который PHP использует для имён с квадратными скобками. Затем array_replace() добавляет поверх них ваши поля для входа и три значения. Кроме того, он перезаписывает поле capy_captchakey, которое страница вывела сама, так что ключ никогда не уходит дважды. Наконец, request() отправляет всё это с собственным методом и адресом формы.
Обход объекта формы ничего не стоит вам на стороне браузера. Метод request() и есть то, что submit() вызывает под капотом. Он по-прежнему отправляет cookies из хранилища и текущую страницу в качестве Referer, а именно этот заголовок строгие фреймворки вроде Django проверяют при отправке формы по HTTPS. Он по-прежнему следует редиректу и сохраняет cookie, который устанавливает ответ 302, прежде чем по нему перейти. Поэтому вход, который при успехе обновляет сессию, попадает на страницу аккаунта с новой сессией в хранилище. Чего не хватает форме, построенной из узла form, так это кнопки отправки. Если сервер проверяет имя кнопки, вместо этого стройте форму от самой кнопки: через selectButton() и её подпись.
Отправляйте ответ ровно в том виде, в каком он вернулся. Бэкенд сайта сверяет его с выданной головоломкой, поэтому обрезка или пересборка делают его недействительным. И отправляйте сразу. CapSkip генерирует новый ключ задания для каждого решения, а Capy привязывает выданную головоломку к этому ключу. Поэтому ключ одноразовый и живёт недолго, а одно решение покрывает одну отправку. Некоторые страницы отправляют данные собственным скриптом в теле JSON, а не обычной отправкой формы. Отправьте форму один раз вручную с открытыми DevTools и поставьте три значения туда, куда их ставит страница.
Шаг 4: повторы при неудачном решении
Когда вы решаете Capy Puzzle на PHP в сколько-нибудь заметном объёме, часть решений будет проваливаться. Неудачное решение приходит в виде ApiException, сообщение которого содержит сырой ответ CapSkip, по умолчанию с ERROR_CAPTCHA_UNSOLVABLE. Через res.php, эндпоинт, который опрашивает SDK, любое неудачное решение выглядит одинаково, и здесь важны две причины. Промах мимо выреза случается изредка, а следующая попытка получает совершенно новую головоломку на другой фотографии, поэтому обычно проходит. Ключ, который отклоняет Capy API, не проходит ни при одной попытке, а счётчик Bad Keys на карточке Capy во вкладке Monitor в CapSkip растёт. Первое повторяйте, второе исправляйте. api_server, который не отвечает, тоже даёт сбой каждый раз, но счётчик Bad Keys при этом не меняется.
function solveCapy(\CapSkip\CapSkip $solver, string $key, string $url, array $options = [], int $attempts = 3): array
{
for ($attempt = 1; ; $attempt++) {
try {
return $solver->capy($key, $url, $options);
} catch (\CapSkip\Exceptions\ApiException $e) {
// A missed hole reads as unsolvable, and the next try draws a
// new puzzle. Anything else, such as a wrong API key, is final.
if (!str_contains($e->getMessage(), 'UNSOLVABLE') || $attempt >= $attempts) {
throw $e;
}
}
}
}Блок кода указывает классы CapSkip с полными именами, поэтому функцию можно вставить в любой файл, что бы тот ни импортировал. Отклонённый ключ тоже выглядит как нерешаемый, поэтому и он здесь уходит в повтор, но падает одинаково при каждой попытке: по этому признаку их и можно различить. Другой путь даёт параметр Retries в разделе Capy в настройках CapSkip: он принимает значения от 0 до 3 и по умолчанию равен 0. Тогда CapSkip сам повторяет попытку после промаха мимо выреза, но никогда не повторяет отклонённый ключ, потому что его отклонят точно так же.
Метод capy опрашивает результат с таймаутом клиента defaultTimeout, 120 секунд, потому что решение Capy сводится к одной загрузке и небольшой арифметике, а не к браузерной сессии. У CapSkip есть и собственный отсчёт времени. В его настройках Capy задача может ждать до 250 секунд (Wait Timeout) одного из 10 потоков (Max. Threads), а на одну попытку отводится 60 секунд (Row Timeout). Обычное решение укладывается во всё это с многократным запасом. Кроме того, SDK продолжает опрос сквозь ошибки соединения, поэтому если CapSkip остановится посреди решения, вызов закончится TimeoutException, а не NetworkException.
Полный рабочий пример
Вот весь сценарий в одном файле. Функция logIn() открывает страницу в собственном HttpBrowser, читает ключ и хост, решает головоломку с повторами и отправляет форму с тремя значениями. Она возвращает браузер, так что запросы, которые вы делаете после входа, несут новую сессию.
<?php
// composer require capskip/capskip symfony/browser-kit symfony/http-client symfony/mime symfony/css-selector
// login.php: run with php login.php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use CapSkip\CapSkip;
use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\CapSkipError;
use CapSkip\Exceptions\TimeoutException;
use CapSkip\Exceptions\ValidationException;
use Symfony\Component\BrowserKit\HttpBrowser;
use Symfony\Component\HttpClient\HttpClient;
const PAGE_URL = 'https://example.com/login';
$solver = new CapSkip([
'apiKey' => getenv('CAPSKIP_API_KEY') ?: 'capskip',
'host' => getenv('CAPSKIP_HOST') ?: '127.0.0.1',
'port' => (int) (getenv('CAPSKIP_PORT') ?: 8080),
'pollingInterval' => 0.5,
]);
function solveCapy(CapSkip $solver, string $key, string $url, array $options = [], int $attempts = 3): array
{
for ($attempt = 1; ; $attempt++) {
try {
return $solver->capy($key, $url, $options);
} catch (ApiException $e) {
// A missed hole reads as unsolvable; the next try draws a new puzzle.
if (!str_contains($e->getMessage(), 'UNSOLVABLE') || $attempt >= $attempts) {
throw $e;
}
echo "attempt {$attempt}: {$e->getMessage()}", PHP_EOL;
}
}
}
function logIn(CapSkip $solver, string $pageUrl, array $credentials): HttpBrowser
{
// One browser per login: it owns the cookie jar and the history.
$browser = new HttpBrowser(HttpClient::create());
$page = $browser->request('GET', $pageUrl);
$status = $browser->getInternalResponse()->getStatusCode();
if ($status !== 200) {
throw new RuntimeException("The page answered {$status}, not the form.");
}
$pageUrl = $browser->getHistory()->current()->getUri();
$script = $page->filter('script[src*="/puzzle/get_js/"]');
$src = $script->count() ? $script->attr('src') : '';
if (str_starts_with($src, '//')) {
$src = 'https:' . $src;
}
parse_str((string) parse_url($src, PHP_URL_QUERY), $query);
$key = $query['k'] ?? $page->filter('input[name="capy_captchakey"]')->attr('value', '');
if (!str_starts_with($key, 'PUZZLE_')) {
throw new RuntimeException('No PUZZLE_ key in the HTML; find it in DevTools.');
}
$apiServer = strstr($src, '/puzzle/get_js/', true) ?: null;
$widget = $page->filter('script[src*="/puzzle/get_js/"], input[name="capy_captchakey"]');
$formNode = $widget->closest('form')
?? $page->filter('form')->reduce(fn ($f) => strtolower((string) $f->attr('method')) === 'post')->first();
if (!$formNode->count()) {
throw new RuntimeException('No POST form on the page; select it by id.');
}
$form = $formNode->form();
$result = solveCapy($solver, $key, $pageUrl, ['api_server' => $apiServer]);
// All three together, straight away: the challenge key is single-use.
$browser->request($form->getMethod(), $form->getUri(), array_replace(
$form->getPhpValues(),
$credentials,
[
'capy_captchakey' => $result['captchakey'],
'capy_challengekey' => $result['challengekey'],
'capy_answer' => $result['answer'],
],
));
return $browser;
}
try {
// Use the page's own field names.
$browser = logIn($solver, PAGE_URL, ['username' => 'YOUR_USERNAME', 'password' => 'YOUR_PASSWORD']);
// A redirect to the account page means it worked; $browser keeps that session.
$status = $browser->getInternalResponse()->getStatusCode();
echo $status, ' ', $browser->getHistory()->current()->getUri(), PHP_EOL;
exit($status < 400 ? 0 : 1);
} catch (ValidationException $e) {
fwrite(STDERR, "not sent: {$e->getMessage()}" . PHP_EOL);
exit(1);
} catch (TimeoutException $e) {
fwrite(STDERR, 'gave up after defaultTimeout' . PHP_EOL);
exit(1);
} catch (CapSkipError $e) {
fwrite(STDERR, 'solve failed: ' . get_class($e) . ": {$e->getMessage()}" . PHP_EOL);
exit(1);
} catch (RuntimeException $e) {
// The page itself, or the network on the way to it.
fwrite(STDERR, get_class($e) . ": {$e->getMessage()}" . PHP_EOL);
exit(1);
}Блоки catch построены по двум семействам исключений. Исключения CapSkip не наследуют RuntimeException, а транспортные ошибки HTTP-клиента наследуют. Поэтому недоступный сайт попадает в последний блок. Решатель, который не запущен, попадает в третий, а тот, что остановился посреди решения, оказывается во втором, в виде TimeoutException. Отсутствующий компонент Mime вызывает LogicException из BrowserKit, которое здесь намеренно ничем не перехватывается: это ошибка настройки, которую исправляют один раз, а не то, что нужно обрабатывать во время выполнения. Скрипт завершается с кодом 1, если на отправку пришёл ответ 4xx или 5xx. Однако один лишь ответ 200 не доказывает, что вход удался, потому что многие сайты отвечают на неудачный вход той же формой. Проверьте URL, на который вы попали, или саму страницу. Сырой эндпоинт, который стоит за этим методом, описан в разделе Capy справочника API.
Одновременный запуск множества входов
PHP выполняет один запрос за раз, и каждый вызов capy() блокируется на свои две секунды или около того. Проще всего запускать больше входов одновременно за счёт большего числа процессов: запускайте скрипт по разу на каждый аккаунт, каждый со своим HttpBrowser. Никогда не используйте один браузер для нескольких входов. У него одно хранилище cookie и одна история, поэтому две сессии перемешали бы свои cookies, CSRF-токены и Referer. Держите в работе не больше 10 процессов, потому что по умолчанию CapSkip решает 10 головоломок Capy одновременно, а остальные ставит в очередь ждать свободного потока.
Чтобы уложиться в один процесс, отправьте несколько заданий методом send() из SDK и собирайте их через getResult(): именно по этой схеме руководство по Friendly Captcha на PHP строит свой пул. Для Capy send() принимает метод capy, captchakey и pageurl, а getResult() с включённым JSON возвращает три значения в виде массива. Отправляйте каждый ответ сразу, как только приходит его результат, а не после всей пачки, потому что самые ранние ключи заданий стареют, пока завершаются последние решения. На объёме добавьте ещё и прокси. Каждое решение получает новую головоломку от Capy API, а равномерный поток таких запросов с одного адреса и есть тот шаблон, ради которого существует ограничение частоты запросов. Настройте пул прокси в разделе Capy настроек CapSkip или передавайте массив proxy в каждом вызове; он влияет только на загрузку головоломки, единственный запрос, который делает решение Capy.
Запуск решателя на другой машине
Адрес 127.0.0.1 подходит, пока скрипт и CapSkip работают на одном ПК с Windows. Когда код на PHP переезжает на VPS, на хост контейнеров, на раннер CI или на виртуальный хостинг, локальная петля указывает не на ту машину. Тогда первое же решение выбрасывает NetworkException с Failed to connect в сообщении. Переключите CapSkip в режим Server, и он начнёт слушать ваш сетевой адрес или публичный IP, так что любой из этих вариантов сможет обращаться к нему через тот же API. Если маршрут идёт через интернет, используйте статический публичный IP и включите проверку API-ключа. При запуске режима Server CapSkip открывает свой порт для всех адресов, поэтому, чтобы доступ к нему был только у ожидаемых машин, добавьте в Windows Firewall правило, блокирующее все остальные. Решателем по-прежнему остаётся ваша собственная машина с Windows, и решения по-прежнему не тарифицируются.
Клиент сам не читает переменные окружения. Читайте CAPSKIP_HOST, CAPSKIP_PORT и CAPSKIP_API_KEY в своём коде и передавайте их в конструктор, как это делает полный пример, чтобы один и тот же скрипт работал и у вас на столе, и на сервере.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| InvalidArgumentException: Unreachable field "capy_answer" | setValues(), submit() со значениями или присваивание как элементу массива для поля, которое виджет создаёт в настоящем браузере | Отправьте getPhpValues() вместе с тремя значениями через request(), как в шаге 3 |
| LogicException с предложением установить symfony/mime, только при отправке формы | BrowserKit не может собрать тело запроса без компонента Mime | composer require symfony/mime |
| LogicException с предложением установить компонент CssSelector | filter() вызван с CSS-селектором, а symfony/css-selector не установлен | composer require symfony/css-selector |
| Сайт сообщает, что сессия истекла, хотя capy() вернул все три значения | Отправка ушла из нового браузера или обычным HTTP-вызовом, без cookies страницы | Отправляйте форму из того же HttpBrowser, который загрузил страницу |
| Сайт отклоняет сам ответ | Ответ был изменён, или до формы дошли только одно или два значения | Отправьте ответ без изменений, вместе с двумя другими значениями, одним запросом |
| Отправка, которая сработала один раз, не проходит во второй | Ключ задания одноразовый и живёт недолго | Решайте заново для каждой отправки и отправляйте сразу |
| ApiException при каждой попытке для одного ключа | Capy API отклонил ключ, или api_server указывает не на тот хост | Скопируйте ключ заново, вместе с префиксом, и проверьте хост в теге script |
| Время от времени ApiException с ERROR_CAPTCHA_UNSOLVABLE | CapSkip не смог найти вырез в этой головоломке | Повторите; следующая попытка получит другую головоломку |
| ValidationException ещё до какой-либо отправки | Пустой ключ или URL, опция, которую метод не принимает, например pollingInterval в отдельном вызове, avatar в качестве version или тип прокси, который CapSkip не поддерживает | Проверьте, что ключ найден, и исправьте или уберите опцию, названную в сообщении |
| ApiException с ERROR_KEY_DOES_NOT_EXIST или ERROR_WRONG_USER_KEY при первом вызове | В CapSkip включена проверка API-ключа, а скрипт отправил ключ по умолчанию или не отправил никакого | Задайте в CAPSKIP_API_KEY ключ, настроенный в CapSkip |
| Каждый вызов занимает около четырёх секунд | Интервал между опросами удваивается до потолка по умолчанию в 5 секунд, поэтому ответ, готовый через две секунды, ждёт опроса на отметке 3.75 | Задайте pollingInterval равным 0.5 в конструкторе |
| NetworkException: Failed to connect to 127.0.0.1 port 8080 | CapSkip не запущен либо неверно указаны хост или порт; на удалённой машине соединение может сбрасывать ещё и файрвол | Запустите CapSkip и проверьте, в каком режиме он должен работать: Local или Server |
| RuntimeException: The page answered 403, not the form | Сайт отклонил загрузку страницы ещё до всякой капчи | Откройте страницу в обычном браузере с того же IP-адреса; возможно, сайт блокирует user agent Symfony BrowserKit |
FAQ
Можно ли по-прежнему использовать для этого Goutte?
Можно, но смысла в этом нет. Последний выпуск Goutte, 4.0.3, вышел в апреле 2023 года, и Packagist помечает пакет как заброшенный в пользу symfony/browser-kit. В 4.0.3 его класс Client представляет собой устаревший подкласс HttpBrowser, который ничего не добавляет. Замена на HttpBrowser сводится к изменению одной строки, и всё в этом руководстве применимо как есть. Код на Goutte, который заполняет форму через setValues(), натыкается на ту же ошибку Unreachable field по той же причине.
Не отклонят ли ответ из-за user agent Symfony BrowserKit?
Capy не отклонит. Ответ Capy не привязан к браузеру, CapSkip не возвращает для него user agent, а необязательный user agent, который можно передать, влияет только на запрос, загружающий головоломку. Однако HttpBrowser действительно представляется как Symfony BrowserKit, и сайт может по своим причинам отклонить такой запрос. В таком случае задайте настоящий user agent до первого запроса с помощью метода setServerParameter() браузера и ключа HTTP_USER_AGENT.
Можно ли сделать то же самое на Guzzle?
Да. Вызов capy и три поля остаются теми же. Но Guzzle не хранит cookies, пока вы не дадите ему хранилище cookie, не отправляет Referer и выбрасывает исключение при ответе 403. То, что HttpBrowser делает за вас, придётся настроить вручную. В руководстве по CaptchaFox на PHP пошагово разобрана такая настройка Guzzle, включая хранилище cookie и Referer.
Может ли PHP на виртуальном хостинге достучаться до решателя?
Да, если хостинг разрешает исходящие подключения к вашему порту. Переведите CapSkip в режим Server в настройках подключения, чтобы он слушал сетевой адрес, а не локальную петлю, прочитайте этот адрес из CAPSKIP_HOST в своём скрипте и передайте его в конструктор. VPS, хост контейнеров, раннер CI и виртуальный хостинг подключаются через один и тот же HTTP API. Если маршрут идёт через интернет, используйте статический публичный IP и блокирующее правило Windows Firewall для адресов, у которых не должно быть доступа к порту. Решатель остаётся на вашем собственном железе, поэтому в том, как считаются решения, ничего не меняется.
Коротко
Чтобы решить Capy Puzzle на PHP, откройте страницу в HttpBrowser и возьмите ключ PUZZLE_ из параметра k скрипта виджета. Запомните и хост скрипта на случай, если он не стандартный. Вызовите capy() с ключом и URL, на котором в итоге оказался браузер, из клиента, созданного с pollingInterval, равным 0.5, и дайте ему отработать свои две секунды или около того. Затем обойдите сеттеры объекта формы: отправьте getPhpValues() вместе с captchakey, challengekey и answer через request() без изменений, вместе и сразу, из того же браузера. Повторяйте редкие промахи, держите в работе не больше 10 входов и переходите в режим Server, как только скрипт покидает машину решателя.
- Как работает этот тип и что покрывает решатель: страница решения Capy Puzzle.
- Все типы капчи, которые поддерживает PHP-пакет CapSkip, перечислены на странице сервиса распознавания капчи для PHP.
И последнее о повторах. Каждая попытка получает другую головоломку, поэтому при промахе правильнее всего просто попробовать снова, а безлимитный сервис распознавания капчи на вашей собственной машине означает, что вторая попытка стоит пару секунд, а не ещё одно оплачиваемое решение.
