Cara Memecahkan Capy Puzzle di PHP dengan Symfony BrowserKit

Untuk memecahkan Capy Puzzle di PHP, buka halaman di HttpBrowser milik Symfony dan baca key PUZZLE_ dari script widget. Berikan key itu bersama URL halaman ke metode capy dari paket capskip/capskip. Lalu kirim tiga nilai yang dikembalikannya di field capy_captchakey, capy_challengekey dan capy_answer, dari sesi browser yang sama. HttpBrowser, kelas yang menggantikan Goutte, melakukan sebagian besar pekerjaan untuk Anda: ia menyimpan cookie, mengikuti redirect, dan mengirim halaman itu sebagai Referer. Masalahnya ada di objek form-nya. Field capy_ baru muncul setelah JavaScript widget berjalan, jadi DomCrawler menolak mengisinya dan melempar Unreachable field. CapSkip menambahkan Capy Puzzle pada versi 1.4.0. Panduan ini membahas key, pemanggilan, jeda dua detik, pengiriman form yang mengakali penolakan itu, percobaan ulang, dan pemecah CAPTCHA di mesin lain.
Apa yang Anda butuhkan
- CapSkip 1.4.0 atau lebih baru yang berjalan di komputer Windows. Rilis itu menambahkan Capy Puzzle, bersama CaptchaFox dan Friendly Captcha.
- PHP 8.4.1 atau lebih baru untuk komponen Symfony 8. Di PHP 8.2 atau 8.3, Composer memilih rilis 7.4 LTS sebagai gantinya, dan kode di bawah berjalan tanpa perubahan di keduanya. Paket capskip juga membutuhkan ekstensi curl dan json.
- Paket capskip/capskip versi 1.3.0 atau lebih baru, rilis pertama yang memiliki metode capy, ditambah empat komponen Symfony: BrowserKit, HttpClient, Mime dan CssSelector.
- URL halaman yang menampilkan puzzle. Key, host Capy, dan form semuanya diambil dari halaman itu.
- Sebuah alamat untuk pemecah CAPTCHA. Dalam mode Local, CapSkip menjawab di 127.0.0.1 hanya untuk perangkat itu. Dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga skrip di mesin lain dapat memanggilnya melalui API. Keduanya diatur di pengaturan koneksi.
# 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 menginstal DomCrawler bersama BrowserKit, tetapi BrowserKit tidak mewajibkan tiga komponen lainnya, jadi sebutkan sendiri. Tanpa HttpClient, tidak ada HttpBrowser yang bisa dibuat, dan tanpa CssSelector, pemanggilan filter() di bawah melempar LogicException. Mime adalah komponen yang kegagalannya baru muncul belakangan. Pengambilan halaman tetap berjalan tanpanya, dan hanya kiriman form yang gagal, dengan LogicException yang meminta Anda menginstal symfony/mime.
Langkah 1: buka halaman di HttpBrowser dan temukan key
HttpBrowser membungkus klien HTTP milik Symfony menjadi browser kecil: sebuah cookie jar, sebuah riwayat (history), dan redirect yang diikuti dengan sendirinya. Itu sudah mencakup cookie sesi dan token CSRF yang biasanya menyertai form login Capy. Namun, HttpBrowser tidak melempar exception saat menerima status error. Respons 403 atau 404 kembali sebagai halaman biasa, sama seperti halaman lain, jadi periksa status sebelum Anda mencari key, seperti yang dilakukan blok kode ini. Dokumentasi komponen BrowserKit membahas sisa API-nya.
<?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;Key-nya bersifat publik dan sama untuk setiap pengunjung, dan key puzzle diawali PUZZLE_. Sebagian besar halaman menyimpannya di parameter k pada URL script widget, yang merupakan bagian dari HTML yang diunduh HttpBrowser. Sebagian halaman juga merender input tersembunyi capy_captchakey di dalam form, dan blok kode ini membacanya jika tidak ada script. Semua bagian URL script sebelum /puzzle/get_js/ adalah Capy API yang menaungi key tersebut, yang oleh CapSkip disebut api_server. Nilai default-nya https://jp.api.capy.me, tempat layanan aktifnya berjalan. Contoh kode yang ditulis untuk pemecah CAPTCHA lain sering menampilkan api.capy.me tanpa awalan regional, dan host itu sudah tidak bisa di-resolve lagi, jadi hapus opsi itu jika Anda menyalin salah satu contoh tersebut. Atribut src yang diawali dua garis miring diberi https: di depannya, karena Capy API hanya disajikan melalui HTTPS.
Beberapa baris terakhir memilih form. Pemanggilan closest() menelusuri ke atas dari widget sampai ke form yang memuatnya. Jika script berada di head atau setelah form, tidak ada form yang memuatnya, dan blok kode ini beralih ke form POST pertama di halaman. Jika halaman memiliki dua form POST, pilih form Anda berdasarkan id-nya dengan filter() sebagai gantinya.
Langkah 2: pemanggilan capy()
Untuk memecahkan Capy Puzzle di PHP, Anda cukup memakai satu metode. Metode ini menerima key, URL halaman, dan array opsi yang bersifat opsional, dan untuk halaman di host default, dua yang pertama sudah cukup.
// 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_answerHasilnya berupa array. Baca ketiga key bernama di dalamnya. Key code berisi seluruh jawaban dari server, sebuah array dengan keempat field-nya, yang berguna untuk logging. respKey adalah string kosong yang hanya ada demi kompatibilitas dengan layanan lain. Jawabannya berupa string panjang yang awalnya kira-kira seperti 0xax8ex0xax84x. Itu adalah jalur seretan yang akan direkam widget saat potongan puzzle digeser, bukan sebuah koordinat.
Berikan URL tempat browser benar-benar berakhir, seperti yang dilakukan blok kode ini, bukan alamat yang Anda ketik. Situs bisa mengalihkan alamat itu untuk menambahkan garis miring di akhir atau awalan bahasa. CapSkip mengirim URL halaman sebagai Referer saat mengambil puzzle, jadi URL itu harus berupa halaman asal form yang sebenarnya.
Opsi yang diterima metode ini
Selain api_server, yang juga bisa ditulis sebagai apiServer, metode ini menerima array proxy dengan key type dan uri. Metode ini juga menerima useragent, yang dikirim CapSkip pada satu-satunya permintaan yang dibuatnya untuk mengambil puzzle. Opsi apa pun yang bernilai null dibuang, itulah sebabnya pemanggilan di atas bisa memberikan host hasil parsing setiap kali. Ada juga opsi version, tetapi hanya puzzle yang diterima. Keluarga Capy yang lain, avatar, adalah challenge berbeda di balik endpoint yang berbeda, jadi SDK menolaknya alih-alih mengembalikan jawaban yang akan ditolak situs.
Ada dua opsi per panggilan yang perlu diketahui. Opsi timeout, dalam detik, menggantikan defaultTimeout milik klien untuk satu pemecahan itu. Batas atas interval polling juga bisa disetel per panggilan, tetapi hanya sebagai polling_interval. pollingInterval dalam camelCase adalah milik konstruktor, dan jika diberikan ke capy(), opsi itu melempar ValidationException yang menyebut nama opsinya. Key atau URL yang kosong, opsi yang tidak dikenal, dan tipe proxy selain HTTP, HTTPS, SOCKS5 atau SOCKS5H juga melempar ValidationException, sebelum permintaan apa pun meninggalkan mesin Anda.
Mengapa pemecahan Capy memakan waktu sekitar dua detik
Proses menemukan lubangnya berlangsung cepat. CapSkip mengambil gambar puzzle, menemukan celahnya dengan sedikit perhitungan piksel, lalu menyusun jalur seretan, tanpa melibatkan browser. Setelah itu ia sengaja menunggu.
Capy mengukur selang waktu antara puzzle diambil dan jawaban diterima, dan menolak jawaban yang kembali lebih cepat daripada waktu yang dibutuhkan seseorang untuk menyeret potongannya. Penolakan itu terbaca seperti jawaban yang salah, sehingga pemecahan yang benar tetapi dikirim terlalu awal terlihat persis seperti pemecah yang rusak. Karena itu CapSkip menahan setiap hasil Capy sampai dua detik setelah ia mengambil puzzle. Jeda ini berupa sleep, jadi yang dikorbankan hanya latensi, bukan CPU.
Dengan pengaturan default, sebuah pemanggilan memakan waktu mendekati empat detik, karena cara SDK melakukan polling. SDK meminta hasil segera setelah mengirim tugas, lalu sekali lagi setelah seperempat detik, dan menggandakan jaraknya setiap kali hingga pollingInterval-nya, yaitu 5 detik. Polling terjadi pada 0, 0.25, 0.75, 1.75 dan 3.75 detik, sehingga jawaban yang siap pada detik kedua menunggu polling kelima. Dalam pengujian kami, klien default kembali setelah 3.8 detik, dan klien yang dibuat dengan pollingInterval disetel ke 0.5, seperti di atas, setelah 2.3 detik. Jangan menambahkan penundaan sendiri sebelum mengirim form, dan jangan mencoba memangkas jedanya. Jeda itulah yang membuat jawabannya lolos.
Langkah 3: tambahkan ketiga field capy_ lalu kirim
Di sinilah kode BrowserKit biasanya gagal. Langkah yang terasa wajar adalah mengisi objek form lalu mengirimnya, dengan setValues(), dengan argumen values pada submit(), atau dengan meng-assign field seperti elemen array. Ketiganya melempar InvalidArgumentException yang berbunyi Unreachable field diikuti nama field-nya. DomCrawler membangun form dari HTML yang di-parse-nya. Widget Capy menulis tiga input-nya ke dalam form dengan JavaScript saat berjalan di browser sungguhan. HttpBrowser tidak menjalankan JavaScript, jadi sejauh yang diketahui DomCrawler, field tersebut tidak ada.
// 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;Solusinya adalah memakai form hanya untuk hal yang memang diketahuinya, lalu Anda sendiri yang mengirim permintaannya. Metode getPhpValues() mengembalikan field form seperti yang akan dikirim browser, tanpa tombol, termasuk token CSRF tersembunyi, dalam bentuk bersarang yang dipakai PHP untuk nama dengan tanda kurung siku. Lalu array_replace() menambahkan field login Anda dan ketiga nilai di atasnya. Fungsi itu juga menimpa input capy_captchakey yang dirender sendiri oleh halaman, sehingga key tidak pernah terkirim dua kali. Terakhir, request() mengirim semuanya dengan method dan alamat milik form itu sendiri.
Memutari objek form tidak mengorbankan apa pun di sisi browser. Metode request() adalah metode yang dipanggil submit() di balik layar. Metode ini tetap mengirim cookie dari jar dan halaman saat ini sebagai Referer, yaitu header yang diperiksa framework ketat seperti Django pada form post HTTPS. Metode ini tetap mengikuti redirect, dan menyimpan cookie yang disetel respons 302 sebelum mengikutinya. Karena itu, login yang merotasi sesi saat berhasil akan mendarat di halaman akun dengan sesi baru di dalam jar. Satu hal yang tidak disertakan form yang dibangun dari node form adalah tombol submit. Jika server memeriksa nama tombol, bangun form dari tombol itu dengan selectButton() dan label-nya sebagai gantinya.
Kirim jawaban persis seperti yang dikembalikan. Backend situs memeriksanya terhadap puzzle yang diambil, jadi memotong atau menyusunnya ulang akan membuatnya tidak valid. Lalu kirim secepatnya. CapSkip membuat challenge key baru untuk setiap pemecahan, dan Capy mengikat puzzle yang diambilnya ke key tersebut. Jadi key itu hanya sekali pakai dan berumur pendek, dan satu pemecahan hanya untuk satu pengiriman. Sebagian halaman mengirim lewat script miliknya sendiri dengan body JSON, bukan form post. Kirim sekali secara manual dengan DevTools terbuka, lalu taruh ketiga nilai di tempat halaman itu menaruhnya.
Langkah 4: percobaan ulang saat pemecahan gagal
Saat Anda memecahkan Capy Puzzle di PHP dalam volume berapa pun, beberapa pemecahan akan gagal. Pemecahan yang gagal datang sebagai ApiException yang pesannya berupa balasan mentah dari CapSkip, dengan ERROR_CAPTCHA_UNSOLVABLE di dalamnya secara default. Lewat res.php, endpoint yang di-polling SDK, setiap pemecahan yang gagal terbaca sama, dan ada dua penyebab yang penting di sini. Kegagalan menemukan lubang hanya terjadi sesekali, dan percobaan berikutnya memunculkan puzzle baru di atas foto yang berbeda, sehingga biasanya berhasil. Key yang ditolak Capy API gagal di setiap percobaan, dan hitungan Bad Keys pada kartu Capy di tab Monitor CapSkip bertambah. Coba ulang yang pertama dan perbaiki yang kedua. Server api_server yang tidak merespons juga gagal setiap kali, tanpa menambah hitungan 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;
}
}
}
}Blok kode ini menulis nama kelas CapSkip secara lengkap, sehingga fungsinya bisa ditempel ke file mana pun, apa pun yang diimpor file itu. Key yang ditolak juga terbaca sebagai unsolvable, sehingga key itu ikut dicoba ulang di sini, dan kegagalannya selalu sama di setiap percobaan: dari situlah Anda bisa membedakan keduanya. Jalan lainnya adalah pengaturan Retries di bagian Capy pada pengaturan CapSkip, yang menerima nilai 0 sampai 3 dan bernilai 0 secara default. CapSkip lalu mencoba ulang pemecahan yang meleset dengan sendirinya, tetapi tidak pernah mencoba ulang key yang ditolak, karena key itu akan ditolak dengan cara yang sama.
Metode capy melakukan polling dengan defaultTimeout milik klien, 120 detik, karena pemecahan Capy hanyalah satu pengambilan dan sedikit aritmetika, bukan sesi browser. CapSkip juga menjalankan hitungan waktunya sendiri. Di pengaturan Capy-nya, sebuah tugas bisa menunggu hingga 250 detik (Wait Timeout) untuk mendapatkan salah satu dari 10 thread (Max. Threads), dan satu percobaan mendapat 60 detik (Row Timeout). Pemecahan normal muat di dalam semua batas itu berkali-kali lipat. SDK juga terus melakukan polling menembus error koneksi, jadi jika CapSkip berhenti di tengah pemecahan, pemanggilan berakhir dengan TimeoutException, bukan NetworkException.
Contoh lengkap yang berfungsi
Berikut seluruh alurnya dalam satu file. Fungsi logIn() membuka halaman di HttpBrowser miliknya sendiri, membaca key dan host, memecahkan dengan percobaan ulang, lalu mengirim form beserta ketiga nilai. Fungsi ini mengembalikan browser tersebut, sehingga permintaan yang Anda buat setelah login membawa sesi yang baru.
<?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);
}Blok catch mengikuti dua keluarga exception. Kelas exception milik CapSkip tidak meng-extend RuntimeException, sedangkan error transport milik klien HTTP meng-extend-nya. Jadi situs yang sedang down masuk ke blok terakhir. Pemecah CAPTCHA yang tidak berjalan masuk ke blok ketiga, dan yang berhenti di tengah pemecahan masuk ke blok kedua, sebagai TimeoutException. Komponen Mime yang tidak terinstal melempar LogicException milik BrowserKit, yang sengaja tidak ditangkap di sini: itu error penyiapan yang cukup diperbaiki sekali, bukan sesuatu yang ditangani saat runtime. Skrip keluar dengan kode 1 jika kiriman form menghasilkan 4xx atau 5xx. Namun, 200 saja tidak membuktikan login berhasil, karena banyak situs menjawab login yang gagal dengan menampilkan form lagi. Periksa URL tempat login berakhir atau halamannya sendiri. Endpoint mentah di balik metode ini didokumentasikan di bagian Capy pada referensi API.
Menjalankan banyak login sekaligus
PHP menjalankan satu permintaan pada satu waktu, dan setiap pemanggilan capy() memblokir selama sekitar dua detiknya. Cara sederhana untuk menjalankan lebih banyak login sekaligus adalah dengan lebih banyak proses: jalankan skrip sekali per akun, masing-masing dengan HttpBrowser miliknya sendiri. Jangan pernah memakai satu browser bersama untuk beberapa login. Browser itu menyimpan satu cookie jar dan satu riwayat, sehingga dua sesi akan mencampuradukkan cookie, token CSRF, dan Referer mereka. Jangan jalankan lebih dari 10 proses, karena CapSkip memecahkan 10 puzzle Capy sekaligus secara default dan memasukkan sisanya ke antrean sampai ada thread yang kosong.
Untuk tetap dalam satu proses, kirim beberapa tugas dengan metode send() milik SDK dan kumpulkan hasilnya dengan getResult(), pola yang juga dipakai panduan Friendly Captcha PHP untuk membangun pool-nya. Untuk Capy, send() menerima method capy, captchakey, dan pageurl, dan getResult() dengan JSON aktif mengembalikan ketiga nilai sebagai array. Kirim setiap jawaban segera setelah hasilnya tiba, bukan setelah seluruh batch selesai, karena challenge key yang paling awal menua selagi pemecahan terakhir selesai. Pada volume besar, tambahkan juga proxy. Setiap pemecahan mengambil puzzle baru dari Capy API, dan aliran permintaan seperti itu dari satu alamat persis pola yang ingin ditangkap oleh rate limiting. Konfigurasikan pool proxy di bagian Capy pada CapSkip, atau berikan array proxy di setiap pemanggilan; proxy hanya menyentuh pengambilan puzzle, satu-satunya permintaan yang dibuat oleh pemecahan Capy.
Menjalankan solver di mesin lain
127.0.0.1 sudah benar selama skrip dan CapSkip berada di PC Windows yang sama. Begitu kode PHP dipindahkan ke VPS, host container, CI runner, atau shared hosting, loopback menunjuk ke mesin yang salah. Pemecahan pertama lalu melempar NetworkException dengan Failed to connect di pesannya. Alihkan CapSkip ke mode Server dan ia akan mendengarkan di alamat jaringan atau IP publik Anda, sehingga semua lingkungan itu dapat memanggilnya melalui API yang sama. Gunakan IP publik statis jika rutenya melewati internet dan aktifkan validasi API key. CapSkip membuka port-nya untuk semua alamat saat mode Server dimulai, jadi untuk membatasinya hanya pada mesin yang Anda harapkan, tambahkan aturan blokir Windows Firewall untuk alamat lainnya. Pemecah CAPTCHA tetap berupa mesin Windows milik Anda sendiri, dan pemecahannya tetap tanpa kuota.
Klien tidak membaca variabel lingkungan dengan sendirinya. Baca CAPSKIP_HOST, CAPSKIP_PORT dan CAPSKIP_API_KEY di kode Anda dan berikan ke konstruktor, seperti pada contoh lengkap, agar skrip yang sama bisa berjalan di komputer Anda maupun di server.
Kesalahan umum dan artinya
| Apa yang Anda lihat | Penyebab | Perbaiki |
|---|---|---|
| InvalidArgumentException: Unreachable field "capy_answer" | setValues(), submit() dengan values, atau assignment array pada field yang dibuat widget di browser sungguhan | Kirim getPhpValues() ditambah ketiga nilai lewat request(), seperti di Langkah 3 |
| LogicException yang meminta Anda menginstal symfony/mime, hanya saat mengirim form | BrowserKit tidak bisa membangun body permintaan tanpa komponen Mime | composer require symfony/mime |
| LogicException yang meminta Anda menginstal komponen CssSelector | filter() dipanggil dengan selector CSS sementara symfony/css-selector tidak terinstal | composer require symfony/css-selector |
| Situs menyatakan sesi telah kedaluwarsa, padahal capy() mengembalikan ketiga nilai | Form dikirim dari browser baru atau panggilan HTTP biasa, tanpa cookie halaman | Kirim form dari HttpBrowser yang mengambil halaman tersebut |
| Situs menolak jawaban itu sendiri | Jawaban diubah, atau hanya satu atau dua nilai yang sampai ke form | Kirim jawaban apa adanya, bersama dua nilai lainnya, dalam satu permintaan |
| Kiriman yang pernah berhasil sekali gagal pada kali kedua | Challenge key hanya sekali pakai dan berumur pendek | Pecahkan ulang untuk setiap pengiriman, dan langsung kirim |
| ApiException pada setiap percobaan, untuk satu key | Capy API menolak key tersebut, atau api_server menunjuk ke host yang salah | Salin ulang key, termasuk awalannya, dan periksa host pada tag script |
| ApiException sesekali yang berisi ERROR_CAPTCHA_UNSOLVABLE | CapSkip tidak dapat menemukan lubang pada puzzle itu | Coba ulang; percobaan berikutnya memunculkan puzzle yang berbeda |
| ValidationException sebelum apa pun terkirim | Key atau URL kosong, opsi yang tidak diterima metode ini, seperti pollingInterval per panggilan, avatar sebagai version, atau tipe proxy yang tidak diterima CapSkip | Pastikan key berhasil ditemukan, lalu perbaiki atau buang opsi yang disebut dalam pesan |
| ApiException dengan ERROR_KEY_DOES_NOT_EXIST atau ERROR_WRONG_USER_KEY pada pemanggilan pertama | Validasi API key aktif di CapSkip, dan skrip mengirim key default atau tidak mengirim key sama sekali | Setel CAPSKIP_API_KEY ke key yang dikonfigurasi di CapSkip |
| Setiap pemanggilan memakan waktu sekitar empat detik | Jarak polling terus digandakan menuju batas atas default 5 detik, sehingga jawaban yang siap pada detik kedua menunggu polling pada detik 3.75 | Setel pollingInterval ke 0.5 di konstruktor |
| NetworkException: Failed to connect to 127.0.0.1 port 8080 | CapSkip tidak berjalan, atau host atau port-nya salah; di mesin jarak jauh, firewall juga bisa memblokir koneksi | Jalankan CapSkip, lalu periksa apakah ia harus berada di mode Local atau mode Server |
| RuntimeException: The page answered 403, not the form | Situs menolak pengambilan halaman, sebelum ada CAPTCHA apa pun | Buka halaman di browser sungguhan dari alamat yang sama; situs mungkin memblokir user agent Symfony BrowserKit |
FAQ
Apakah saya masih bisa memakai Goutte untuk ini?
Bisa, tetapi tidak ada alasan untuk melakukannya. Rilis terakhir Goutte, 4.0.3, keluar pada April 2023, dan Packagist menandai paket itu sebagai abandoned dengan symfony/browser-kit sebagai penggantinya. Di versi 4.0.3, kelas Client-nya adalah subclass HttpBrowser yang sudah deprecated dan tidak menambahkan apa pun. Menggantinya dengan HttpBrowser cukup dengan mengubah satu baris, dan semua isi panduan ini berlaku apa adanya. Kode Goutte yang mengisi form dengan setValues() mengalami error Unreachable field yang sama, dengan alasan yang sama.
Apakah user agent Symfony BrowserKit membuat jawaban ditolak?
Tidak oleh Capy. Jawaban Capy tidak terikat pada browser, CapSkip tidak mengembalikan user agent untuknya, dan user agent opsional yang bisa Anda berikan hanya mengubah permintaan yang mengambil puzzle. Namun, HttpBrowser memang memperkenalkan dirinya sebagai Symfony BrowserKit, dan situs mungkin menolaknya dengan alasannya sendiri. Jika begitu, setel user agent sungguhan sebelum permintaan pertama, dengan metode setServerParameter() milik browser itu dan key HTTP_USER_AGENT.
Bisakah saya melakukan hal yang sama dengan Guzzle?
Bisa. Pemanggilan capy dan ketiga field-nya sama. Namun, Guzzle tidak menyimpan cookie kecuali Anda memberinya cookie jar, tidak mengirim Referer, dan melempar exception saat menerima 403. Anda harus menyiapkan secara manual apa yang dikerjakan HttpBrowser untuk Anda. Panduan CaptchaFox PHP membahas penyiapan Guzzle tersebut langkah demi langkah, termasuk cookie jar dan Referer.
Bisakah PHP di shared hosting menjangkau pemecah CAPTCHA?
Bisa, jika penyedia hosting mengizinkan koneksi keluar ke port Anda. Alihkan CapSkip ke mode Server di pengaturan koneksi agar ia mendengarkan di alamat jaringan, bukan di loopback, baca alamat itu dari CAPSKIP_HOST di skrip Anda, lalu berikan ke konstruktor. VPS, host container, CI runner, dan shared hosting semuanya terhubung melalui API HTTP yang sama. Gunakan IP publik statis jika rutenya melewati internet, dengan aturan blokir Windows Firewall untuk alamat yang seharusnya tidak menjangkau port tersebut. Pemecah CAPTCHA tetap berada di perangkat milik Anda, jadi tidak ada yang berubah soal cara pemecahan dihitung.
Versi singkatnya
Untuk memecahkan Capy Puzzle di PHP, buka halaman di HttpBrowser dan ambil key PUZZLE_ dari parameter k pada script widget. Catat juga host script itu, kalau-kalau bukan host default. Panggil capy() dengan key dan URL tempat browser berakhir, dari klien yang dibuat dengan pollingInterval disetel ke 0.5, lalu biarkan ia memakan sekitar dua detiknya. Setelah itu, lewati setter objek form: kirim getPhpValues() ditambah captchakey, challengekey dan answer lewat request(), apa adanya, bersamaan dan seketika, dari browser yang sama. Coba ulang pemecahan yang sesekali meleset, jangan jalankan lebih dari 10 login sekaligus, dan beralihlah ke mode Server begitu skrip tidak lagi berada di mesin pemecah CAPTCHA.
- Cara kerja tipe ini dan apa saja yang dicakup pemecah CAPTCHA: halaman pemecah Capy Puzzle.
- Setiap tipe CAPTCHA yang ditangani paket PHP: halaman pemecah CAPTCHA PHP.
Satu hal terakhir tentang percobaan ulang. Setiap percobaan memunculkan puzzle yang berbeda, jadi mencoba lagi adalah solusi yang tepat untuk pemecahan yang meleset, dan dengan pemecah captcha tanpa batas di mesin Anda sendiri, percobaan kedua hanya memakan beberapa detik, bukan satu pemecahan berbayar lagi.
