PHP’de Symfony BrowserKit ile Capy Puzzle Nasıl Çözülür

PHP ile Capy Puzzle çözmek için sayfayı Symfony’nin HttpBrowser sınıfıyla açın ve PUZZLE_ anahtarını widget script’inden okuyun. Anahtarı sayfa URL’siyle birlikte capskip/capskip paketinin capy metoduna verin. Ardından döndürdüğü üç değeri capy_captchakey, capy_challengekey ve capy_answer alanlarında, aynı tarayıcı oturumundan gönderin. Goutte’un yerini alan sınıf olan HttpBrowser, işin çoğunu sizin yerinize yapar: çerezleri saklar, yönlendirmeleri izler ve sayfayı Referer olarak gönderir. Sorun ise form nesnesindedir. capy_ alanları ancak widget’ın JavaScript’i çalıştıktan sonra ortaya çıkar; bu yüzden DomCrawler onlara değer atamayı reddeder ve Unreachable field hatası fırlatır. CapSkip, Capy Puzzle desteğini 1.4.0 sürümünde ekledi. Bu rehber anahtarı, çağrıyı, iki saniyelik bekletmeyi, bu reddi aşan gönderimi, yeniden denemeleri ve başka bir makinedeki çözücüyü ele alıyor.
Neye ihtiyacınız var
- Bir Windows makinesinde çalışan CapSkip 1.4.0 veya üzeri. Capy Puzzle desteği, CaptchaFox ve Friendly Captcha ile birlikte bu sürümle geldi.
- Symfony 8 bileşenleri için PHP 8.4.1 veya üzeri. PHP 8.2 ya da 8.3 üzerinde Composer bunun yerine 7.4 LTS sürümlerini seçer ve aşağıdaki kod ikisinde de hiçbir değişiklik yapılmadan çalışır. capskip paketi ayrıca curl ve json eklentilerine ihtiyaç duyar.
- capskip/capskip paketinin 1.3.0 veya üzeri bir sürümü (capy metodunu içeren ilk sürüm) ve dört Symfony bileşeni: BrowserKit, HttpClient, Mime ve CssSelector.
- Bulmacayı gösteren sayfanın URL’si. Anahtar, Capy host’u ve form hep bu sayfadan elde edilir.
- Çözücü için bir adres. Local modunda CapSkip 127.0.0.1 üzerinden yalnızca o cihaza yanıt verir. Server modunda ise ağ adresinizi veya genel IP’nizi dinler, böylece başka bir makinedeki bir betik onu API üzerinden çağırabilir. İkisi de şurada ayarlanır: bağlantı ayarları.
# 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 ile birlikte kurar, ancak BrowserKit diğer üçünü gerektirmez; bu yüzden onları komuta kendiniz ekleyin. HttpClient olmadan oluşturulacak bir HttpBrowser yoktur, CssSelector olmadan da aşağıdaki filter() çağrıları bir LogicException fırlatır. Geç başarısız olan bileşen Mime’dır. Sayfa isteği onsuz da çalışır; yalnızca form gönderimi, symfony/mime kurmanızı söyleyen bir LogicException ile başarısız olur.
1. Adım: sayfayı HttpBrowser ile açın ve anahtarı bulun
HttpBrowser, Symfony’nin HTTP istemcisini küçük bir tarayıcının içine sarar: bir cookie jar, bir geçmiş ve kendiliğinden izlenen yönlendirmeler. Bu, bir Capy giriş formunun genellikle beraberinde getirdiği oturum çerezini ve CSRF token’ını karşılar. Ancak bir hata durum kodu aldığında istisna fırlatmaz. 403 ya da 404 de diğer sayfalar gibi bir sayfa olarak geri gelir; bu yüzden, koddaki blokta olduğu gibi, anahtar aramadan önce durum kodunu kontrol edin. HttpBrowser API’sinin geri kalanı BrowserKit bileşeni belgelerinde anlatılıyor.
<?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;Anahtar herkese açıktır ve her ziyaretçi için aynıdır; puzzle türündeki bir anahtar PUZZLE_ ile başlar. Çoğu sayfa onu widget script’inin URL’sindeki k parametresinde taşır ve bu URL, HttpBrowser’ın indirdiği HTML’in bir parçasıdır. Bazı sayfalar formda ayrıca gizli bir capy_captchakey input alanı da oluşturur; script olmadığında blok bu alanı okur. Script URL’sinde /puzzle/get_js/ öncesindeki her şey, anahtarın arkasında durduğu Capy API’sidir ve CapSkip buna api_server der. Varsayılan değeri, canlı servisin çalıştığı https://jp.api.capy.me adresidir. Diğer çözücüler için yazılmış örnekler çoğu zaman bölgesel ön ek olmadan api.capy.me adresini gösterir ve bu host artık çözümlenmiyor; böyle bir örneği kopyaladıysanız bu değeri kaldırın. İki eğik çizgiyle başlayan bir src değerinin önüne https: eklenir, çünkü Capy API’si yalnızca HTTPS üzerinden sunulur.
Son satırlar formu seçer. closest() çağrısı widget’tan yukarı doğru çıkarak onu içeren forma ulaşır. Script head bölümünde ya da formdan sonra duruyorsa onu içeren bir form yoktur ve blok yedek olarak sayfanın ilk POST formunu alır. Sayfada iki POST formu varsa kendi formunuzu bunun yerine filter() ile id’sine göre seçin.
2. Adım: capy() çağrısı
PHP ile Capy Puzzle çözmek için tek bir metoda ihtiyacınız var. Metot anahtarı, sayfa URL’sini ve isteğe bağlı bir seçenekler dizisini alır; varsayılan host üzerindeki bir sayfa için ilk ikisi yeterlidir.
// 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_answerSonuç bir dizidir. Adlandırılmış üç anahtarı okuyun. code anahtarı, sunucunun cevabının tamamını, yani dört alanın hepsini içeren ve loglama için kullanışlı olan bir diziyi tutar. respKey ise yalnızca diğer servislerle uyumluluk için var olan boş bir dizedir. Cevap, 0xax8ex0xax84x benzeri bir şeyle başlayan uzun bir dizedir. Bir koordinat değil, parça hareket ederken widget’ın kaydedeceği sürükleme izidir.
Yazdığınız adresi değil, bloğun yaptığı gibi tarayıcının gerçekte vardığı URL’yi verin. Bir site, sona bir eğik çizgi ya da bir dil ön eki eklemek için o adresi yönlendirebilir. CapSkip bulmacayı çekerken sayfa URL’sini Referer olarak gönderir; bu yüzden bu URL, formun gerçekten geldiği sayfa olmalıdır.
Metodun kabul ettiği seçenekler
Metot, apiServer adıyla da kullanılabilen api_server seçeneğinin yanı sıra type ve uri anahtarlarına sahip bir proxy dizisi alır. Ayrıca useragent seçeneğini de alır; CapSkip bunu bulmacayı çekmek için yaptığı tek istekte gönderir. Değeri null olan her seçenek çıkarılır; yukarıdaki çağrının ayrıştırılan host’u her seferinde verebilmesinin nedeni budur. Bir version seçeneği de vardır, ancak yalnızca puzzle kabul edilir. Capy’nin diğer ailesi olan avatar, farklı bir endpoint’in arkasında duran başka bir challenge türüdür; bu yüzden SDK, sitenin reddedeceği bir cevap döndürmek yerine onu geri çevirir.
Çağrı başına verilen iki seçeneği bilmekte fayda var. Saniye cinsinden verilen bir timeout, yalnızca o çözüm için istemcinin defaultTimeout değerinin yerini alır. Yoklama aralığının üst sınırı da çağrı başına ayarlanabilir, ama yalnızca polling_interval olarak. camelCase yazılan pollingInterval constructor’a aittir; capy() çağrısına verildiğinde seçeneğin adını belirten bir ValidationException fırlatır. Boş bir anahtar veya URL, bilinmeyen bir seçenek ve HTTP, HTTPS, SOCKS5 ya da SOCKS5H dışında bir proxy türü de, makinenizden daha hiçbir istek çıkmadan ValidationException fırlatır.
Bir Capy çözümü neden yaklaşık iki saniye sürer
Boşluğu bulmak hızlıdır. CapSkip bulmaca görselini çeker, biraz piksel hesabıyla boşluğun yerini belirler ve sürükleme izini oluşturur; işin içinde hiçbir tarayıcı yoktur. Sonra bilerek bekler.
Capy, bir bulmacanın çekilmesiyle cevabın alınması arasındaki süreyi ölçer ve bir insanın parçayı sürükleyebileceğinden daha hızlı gelen bir cevabı reddeder. Ret, yanlış bir cevapla aynı şekilde okunur; dolayısıyla fazla erken teslim edilen doğru bir çözüm tıpatıp bozuk bir çözücü gibi görünür. Bu yüzden CapSkip her Capy sonucunu, bulmacayı çektikten iki saniye sonrasına kadar bekletir. Bekletme bir sleep çağrısıdır, yani gecikme ekler ama CPU harcamaz.
Varsayılan ayarlarla bir çağrı, SDK’nın yoklama biçimi yüzünden dört saniyeye yakın sürer. SDK, görevi gönderir göndermez sonucu ister, ardından çeyrek saniye sonra yeniden sorar ve aralığı her seferinde ikiye katlayarak 5 saniyelik pollingInterval değerine kadar açar. Yoklamalar 0, 0.25, 0.75, 1.75 ve 3.75 saniyelerde gerçekleşir; dolayısıyla iki saniyede hazır olan bir cevap beşinci yoklamayı bekler. Testlerimizde varsayılan bir istemci 3.8 saniye sonra, yukarıdaki gibi pollingInterval değeri 0.5 olarak ayarlanmış bir istemci ise 2.3 saniye sonra döndü. Gönderimden önce kendi gecikmenizi eklemeyin ve bekletmeyi kısaltmaya da çalışmayın. Cevabın kabul edilmesini sağlayan şey bu bekletmedir.
3. Adım: üç capy_ alanını ekleyin ve gönderin
BrowserKit kodunun genellikle bozulduğu yer burasıdır. Akla gelen ilk hamle form nesnesini doldurup göndermektir: setValues() ile, submit() metodunun values argümanıyla ya da alana bir dizi öğesi gibi değer atayarak. Üçü de, Unreachable field ifadesinin ardından alan adını içeren bir InvalidArgumentException fırlatır. DomCrawler formu, ayrıştırdığı HTML’den oluşturur. Capy widget’ı ise gerçek bir tarayıcıda çalıştığında üç input alanını forma JavaScript ile yazar. HttpBrowser hiç JavaScript çalıştırmaz; bu yüzden DomCrawler’ın bildiği kadarıyla bu alanlar yoktur.
// 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;Çözüm, formu yalnızca bildiği şeyler için kullanmak ve isteği kendiniz göndermektir. getPhpValues() metodu, formun alanlarını bir tarayıcının göndereceği şekilde, düğme hariç ve gizli CSRF token’ı dahil olarak, PHP’nin köşeli parantezli adlar için kullandığı iç içe yapıda döndürür. Ardından array_replace() giriş alanlarınızı ve üç değeri bunların üzerine ekler. Sayfanın kendisinin oluşturduğu bir capy_captchakey input alanının da üzerine yazar; böylece anahtar hiçbir zaman iki kez gönderilmez. Son olarak request() hepsini formun kendi HTTP metodu ve adresiyle gönderir.
Form nesnesini atlayıp geçmek, tarayıcı tarafında size hiçbir şey kaybettirmez. request() metodu, submit() metodunun arka planda çağırdığı metottur. Yine cookie jar’daki çerezleri ve Referer olarak geçerli sayfayı gönderir; Django gibi katı framework’lerin bir HTTPS form gönderiminde kontrol ettiği başlık da budur. Yine yönlendirmeleri izler ve bir 302 yanıtının ayarladığı çerezi, yönlendirmeyi izlemeden önce saklar. Bu yüzden başarılı girişte oturumu yenileyen bir giriş işlemi, yeni oturum cookie jar’dayken hesap sayfasına ulaşır. Bir form düğümünden oluşturulan formun dışarıda bıraktığı tek şey gönder düğmesidir. Sunucu düğmenin adını kontrol ediyorsa formu bunun yerine düğmeden, selectButton() ve düğmenin etiketiyle oluşturun.
Cevabı tam olarak döndüğü gibi gönderin. Sitenin backend’i onu çekilen bulmacaya göre kontrol eder; dolayısıyla kırpmak ya da yeniden kurmak onu geçersiz kılar. Ardından hemen gönderin. CapSkip her çözüm için yeni bir challenge anahtarı üretir ve Capy, çektiği bulmacayı bu anahtara bağlar. Dolayısıyla anahtar tek kullanımlıktır ve ömrü kısadır; bir çözüm de yalnızca bir gönderimi karşılar. Bazı sayfalar form gönderimi yerine kendi script’leri üzerinden bir JSON gövdesiyle gönderir. DevTools açıkken formu bir kez elle gönderin ve üç değeri sayfanın koyduğu yere koyun.
4. Adım: bir çözüm başarısız olduğunda yeniden denemeler
PHP ile kayda değer bir hacimde Capy Puzzle çözdüğünüzde bazı çözümler başarısız olur. Başarısız bir çözüm, mesajı CapSkip’in ham yanıtı olan ve varsayılan olarak ERROR_CAPTCHA_UNSOLVABLE içeren bir ApiException olarak gelir. SDK’nın yokladığı endpoint olan res.php üzerinden her başarısız çözüm aynı görünür ve burada iki neden önemlidir. Bulunamayan bir boşluk ara sıra yaşanır ve bir sonraki deneme farklı bir fotoğraf üzerinde yepyeni bir bulmaca çeker, bu yüzden genellikle başarılı olur. Capy API’sinin reddettiği bir anahtar ise her denemede başarısız olur ve CapSkip’in Monitor sekmesindeki Capy kartında Bad Keys sayacı artar. İlkini yeniden deneyin, ikincisini düzeltin. Yanıt vermeyen bir api_server da her seferinde başarısız olur, ama Bad Keys sayacını artırmaz.
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, CapSkip sınıflarını tam adlarıyla yazar; böylece fonksiyon, o dosya neyi import ederse etsin, herhangi bir dosyaya yapıştırılabilir. Reddedilen bir anahtar da çözülemez olarak görünür, dolayısıyla burada o da yeniden denenir ve her denemede aynı şekilde başarısız olur; ikisini birbirinden böyle ayırt edersiniz. Diğer yol, CapSkip’in ayarlarındaki Capy bölümünde bulunan Retries ayarıdır; 0 ile 3 arasında bir değer alır ve varsayılan olarak 0’dır. Bu durumda CapSkip bulunamayan bir boşluğu kendiliğinden yeniden dener, ama reddedilen bir anahtarı asla yeniden denemez, çünkü aynı şekilde yine reddedilirdi.
capy metodu, istemcinin 120 saniye olan defaultTimeout değeriyle yoklama yapar, çünkü bir Capy çözümü bir tarayıcı oturumu değil, tek bir çekme işlemi ve biraz aritmetiktir. CapSkip de kendi saatini işletir. Capy ayarlarında bir görev, 10 thread’den (Max. Threads) birini en fazla 250 saniye (Wait Timeout) bekleyebilir ve tek bir deneme 60 saniye (Row Timeout) alır. Normal bir çözüm bunların hepsine kat kat sığar. SDK ayrıca bağlantı hatalarına rağmen yoklamayı sürdürür; bu yüzden CapSkip çözümün ortasında durursa çağrı NetworkException ile değil, TimeoutException ile sona erer.
Tam çalışan örnek
İşte tüm akış tek bir dosyada. logIn() fonksiyonu sayfayı kendi HttpBrowser nesnesinde açar, anahtarı ve host’u okur, yeniden denemelerle çözer ve formu üç değerle birlikte gönderir. Tarayıcı nesnesini döndürür; böylece girişten sonra yaptığınız istekler yeni oturumu taşır.
<?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 blokları iki istisna ailesini izler. CapSkip’in istisnaları RuntimeException sınıfından türemez, HTTP istemcisinin aktarım hataları ise türer. Bu yüzden çalışmayan bir site son bloğa düşer. Çalışmayan bir çözücü üçüncü bloğa, çözümün ortasında duran bir çözücü ise TimeoutException olarak ikinci bloğa düşer. Eksik bir Mime bileşeni BrowserKit’in LogicException istisnasını fırlatır ve buradaki hiçbir blok onu bilerek yakalamaz: bu, çalışma zamanında ele alınacak bir şey değil, bir kez düzeltilecek bir kurulum hatasıdır. Betik, gönderim 4xx veya 5xx döndürürse 1 koduyla çıkar. Ancak tek başına 200 yanıtı girişin başarılı olduğunu kanıtlamaz, çünkü pek çok site başarısız bir girişe formu yeniden göstererek yanıt verir. Girişin vardığı URL’yi ya da sayfanın kendisini kontrol edin. Metodun arkasındaki ham endpoint şurada belgelenmiştir: API referansının Capy bölümü.
Aynı anda çok sayıda giriş işlemi çalıştırmak
PHP bir seferde tek bir istek çalıştırır ve her capy() çağrısı yaklaşık iki saniye boyunca bloklanır. Aynı anda daha fazla giriş çalıştırmanın basit yolu daha fazla süreçtir: betiği her hesap için bir kez, her biri kendi HttpBrowser nesnesiyle çalıştırın. Tek bir tarayıcı nesnesini asla girişler arasında paylaşmayın. Bu nesne tek bir cookie jar ve tek bir geçmiş tutar; bu yüzden iki oturum çerezlerini, CSRF token’larını ve Referer değerlerini birbirine karıştırır. Aynı anda en fazla 10 tane çalıştırın, çünkü CapSkip varsayılan olarak aynı anda 10 Capy bulmacası çözer ve geri kalanını boş bir thread için kuyruğa alır.
Hepsini tek bir süreçte tutmak için SDK’nın send() metoduyla birkaç iş gönderin ve onları getResult() ile toplayın; bu, Friendly Captcha PHP rehberinin havuzunu üzerine kurduğu kalıptır. Capy için send(), method olarak capy değerini, ayrıca captchakey ve pageurl değerlerini alır; JSON açıkken getResult() üç değeri bir dizi olarak döndürür. Her cevabı tüm toplu işin sonunda değil, sonucu gelir gelmez gönderin, çünkü son çözümler biterken ilk challenge anahtarları eskir. Hacim arttığında proxy de ekleyin. Her çözüm Capy API’sinden yeni bir bulmaca çeker ve tek bir adresten gelen sürekli bir bulmaca akışı, hız sınırlamanın yakalamak için var olduğu örüntünün ta kendisidir. CapSkip’in Capy bölümünde bir proxy havuzu yapılandırın ya da her çağrıda bir proxy dizisi verin; proxy yalnızca bulmacanın çekilmesini etkiler, bu da bir Capy çözümünün yaptığı tek istektir.
Çözücüyü başka bir makinede çalıştırmak
Betik ve CapSkip aynı Windows bilgisayarı paylaştığı sürece 127.0.0.1 doğru adrestir. PHP kodu bir VPS’e, bir konteyner sunucusuna, bir CI runner’a ya da paylaşımlı barındırmaya taşındığında loopback yanlış makineyi gösterir. Bu durumda ilk çözüm, mesajında Failed to connect bulunan bir NetworkException fırlatır. CapSkip’i Server moduna alın; o zaman ağ adresinizi veya genel IP’nizi dinler, böylece bunların hepsi onu aynı API üzerinden çağırabilir. Rota internetten geçiyorsa statik bir genel IP kullanın ve API anahtarı doğrulamasını açın. CapSkip, Server modu başladığında portunu her adrese açar; portu yalnızca beklediğiniz makinelere açık tutmak için diğerlerine yönelik bir Windows Firewall engelleme kuralı ekleyin. Çözücü hâlâ sizin kendi Windows makinenizdir ve çözüm başına ücretlendirme hâlâ yoktur.
İstemci ortam değişkenlerini kendiliğinden okumaz. Tam örnekte olduğu gibi CAPSKIP_HOST, CAPSKIP_PORT ve CAPSKIP_API_KEY değerlerini kodunuzda okuyup constructor’a verin; böylece aynı betik hem masanızda hem de bir sunucuda çalışır.
Sık görülen hatalar ve anlamları
| Gördüğünüz | Neden | Düzeltme |
|---|---|---|
| InvalidArgumentException: Unreachable field "capy_answer" | setValues(), değerlerle çağrılan submit() ya da widget’ın tarayıcıda oluşturduğu bir alana dizi ataması | 3. adımdaki gibi getPhpValues() çıktısını ve üç değeri request() ile gönderin |
| Yalnızca form gönderiminde, symfony/mime kurmanızı isteyen bir LogicException | BrowserKit, Mime bileşeni olmadan bir istek gövdesi oluşturamaz | composer require symfony/mime |
| CssSelector bileşenini kurmanızı isteyen bir LogicException | filter() bir CSS seçiciyle çağrıldı ve symfony/css-selector eksik | composer require symfony/css-selector |
| capy() üç değerin hepsini döndürdüğü hâlde site oturumun süresinin dolduğunu söylüyor | Gönderim, sayfanın çerezleri olmadan, yeni bir tarayıcı nesnesinden ya da düz bir HTTP çağrısından yapıldı | Gönderimi, sayfayı çeken HttpBrowser nesnesinden yapın |
| Site cevabın kendisini reddediyor | Cevap değiştirildi ya da değerlerden yalnızca biri veya ikisi forma ulaştı | Cevabı diğer iki değerle birlikte, birebir ve tek istekte gönderin |
| Bir kez çalışan gönderim ikinci seferde başarısız oluyor | Challenge anahtarı tek kullanımlıktır ve ömrü kısadır | Her gönderim için yeniden çözün ve hemen gönderin |
| Tek bir anahtar için her denemede bir ApiException | Capy API’si anahtarı reddetti ya da api_server yanlış host’u gösteriyor | Anahtarı ön eki dahil yeniden kopyalayın ve script etiketinin host’unu kontrol edin |
| Ara sıra gelen, ERROR_CAPTCHA_UNSOLVABLE içeren bir ApiException | CapSkip o bulmacadaki boşluğu bulamadı | Yeniden deneyin; bir sonraki deneme farklı bir bulmaca çeker |
| Daha hiçbir şey gönderilmeden ValidationException | Boş bir anahtar veya URL, metodun kabul etmediği bir seçenek (örneğin çağrı başına verilen bir pollingInterval), version olarak avatar ya da CapSkip’in kabul etmediği bir proxy türü | Anahtarın bulunduğunu kontrol edin ve mesajda adı geçen seçeneği düzeltin ya da çıkarın |
| İlk çağrıda ERROR_KEY_DOES_NOT_EXIST veya ERROR_WRONG_USER_KEY içeren bir ApiException | CapSkip’te API anahtarı doğrulaması açık ve betik varsayılan anahtarı gönderdi ya da hiç anahtar göndermedi | CAPSKIP_API_KEY değerini CapSkip’te yapılandırılmış bir anahtara ayarlayın |
| Her çağrı yaklaşık dört saniye sürüyor | Yoklamalar arasındaki aralık, varsayılan 5 saniyelik üst sınıra doğru ikiye katlanmaya devam ediyor, bu yüzden iki saniyede hazır olan bir cevap 3.75 saniyedeki yoklamayı bekliyor | Constructor’da pollingInterval değerini 0.5 olarak ayarlayın |
| NetworkException: Failed to connect to 127.0.0.1 port 8080 | CapSkip çalışmıyor ya da host veya port yanlış; uzak bir makinede bir güvenlik duvarı da bağlantıyı düşürebilir | CapSkip’i başlatın, ardından Local modunda mı yoksa Server modunda mı olması gerektiğini kontrol edin |
| RuntimeException: The page answered 403, not the form | Site, daha herhangi bir CAPTCHA gelmeden sayfa isteğini geri çevirdi | Sayfayı aynı adresten gerçek bir tarayıcıda açın; site Symfony BrowserKit user agent’ını engelliyor olabilir |
FAQ
Bunun için hâlâ Goutte kullanabilir miyim?
Kullanabilirsiniz ama bunun için bir neden yok. Goutte’un son sürümü olan 4.0.3 Nisan 2023’te çıktı ve Packagist paketi symfony/browser-kit lehine terk edilmiş (abandoned) olarak işaretliyor. 4.0.3 sürümünde Client sınıfı, HttpBrowser’ın hiçbir şey eklemeyen, kullanımdan kaldırılmış (deprecated) bir alt sınıfıdır. Onu HttpBrowser ile değiştirmek tek satırlık bir değişikliktir ve bu rehberdeki her şey yazıldığı gibi geçerlidir. Formu setValues() ile dolduran Goutte kodu da aynı nedenle aynı Unreachable field hatasına takılır.
Symfony BrowserKit user agent’ı cevabın reddedilmesine yol açar mı?
Capy tarafında hayır. Capy cevabı bir tarayıcıya bağlı değildir, CapSkip onun için bir user agent döndürmez ve geçirebileceğiniz isteğe bağlı user agent da yalnızca bulmacayı çeken isteği değiştirir. Ancak HttpBrowser kendini gerçekten Symfony BrowserKit olarak tanıtır ve bir site, kendine ait nedenlerle bunu geri çevirebilir. Böyle olursa ilk istekten önce, tarayıcı nesnesinin setServerParameter() metodu ve HTTP_USER_AGENT anahtarıyla gerçek bir user agent ayarlayın.
Aynı şeyi bunun yerine Guzzle ile yapabilir miyim?
Evet. capy çağrısı ve üç alan aynıdır. Ancak Guzzle, siz ona bir cookie jar vermedikçe hiçbir çerez saklamaz, Referer göndermez ve 403 aldığında istisna fırlatır. HttpBrowser’ın sizin yerinize yaptığı şeyleri elle kurarsınız. Cookie jar ve Referer dahil bu Guzzle kurulumunu CaptchaFox PHP rehberi adım adım anlatıyor.
Paylaşımlı barındırmadaki PHP çözücüye erişebilir mi?
Evet, barındırma sağlayıcınız portunuza giden bağlantılara izin veriyorsa. Bağlantı ayarlarından CapSkip’i Server moduna alın; böylece loopback yerine bir ağ adresini dinler. O adresi betiğinizde CAPSKIP_HOST değişkeninden okuyun ve constructor’a verin. Bir VPS, bir konteyner sunucusu, bir CI runner ve paylaşımlı barındırma; hepsi aynı HTTP API üzerinden bağlanır. Rota internetten geçiyorsa statik bir genel IP kullanın ve porta ulaşmaması gereken adresler için bir Windows Firewall engelleme kuralı ekleyin. Çözücü sizin sahip olduğunuz donanımda kalır, dolayısıyla çözümlerin nasıl sayıldığı konusunda hiçbir şey değişmez.
Kısa özet
PHP ile Capy Puzzle çözmek için sayfayı bir HttpBrowser nesnesinde açın ve PUZZLE_ anahtarını widget script’inin k parametresinden alın. Varsayılan olmayabileceği için script’in host’unu da not edin. capy() metodunu, pollingInterval değeri 0.5 olarak ayarlanmış bir istemciden, anahtar ve tarayıcının vardığı URL ile çağırın ve iki saniye kadar sürmesine izin verin. Ardından form nesnesinin setter metotlarını atlayın: getPhpValues() çıktısını ve captchakey, challengekey ve answer değerlerini aynı tarayıcı nesnesinden, request() ile, birebir, birlikte ve hemen gönderin. Ara sıra yaşanan ıskalarda yeniden deneyin, aynı anda en fazla 10 giriş çalıştırın ve betik çözücünün makinesinden ayrıldığında Server moduna geçin.
- Bu türün nasıl çalıştığı ve çözücünün neleri kapsadığı: Capy Puzzle çözücü sayfası.
- PHP paketinin desteklediği tüm CAPTCHA türleri: PHP CAPTCHA çözücü sayfası.
Yeniden denemeler hakkında son bir nokta. Her deneme farklı bir bulmaca çektiği için bir ıskanın doğru çaresi yeniden denemektir ve kendi makinenizde bir sınırsız captcha çözücü çalıştırdığınızda ikinci bir deneme, faturalanan yeni bir çözüme değil, yalnızca bir iki saniyeye mal olur.
