Symfony BrowserKit के साथ PHP में Capy Puzzle कैसे हल करें

PHP में Capy Puzzle हल करने के लिए, पेज को Symfony के HttpBrowser में खोलें और widget script से PUZZLE_ key पढ़ें। उसे पेज URL के साथ capskip/capskip पैकेज के capy method को पास करें। फिर वह जो तीन वैल्यू लौटाता है, उन्हें उसी ब्राउज़र session से capy_captchakey, capy_challengekey और capy_answer फ़ील्ड में भेजें। HttpBrowser, यानी वह class जिसने Goutte की जगह ली, आपका ज़्यादातर काम खुद कर देता है: वह cookies रखता है, redirects का पीछा करता है और पेज को Referer के रूप में भेजता है। पेच उसके फॉर्म object में है। capy_ फ़ील्ड तभी बनते हैं जब widget का JavaScript चल चुका हो, इसलिए DomCrawler उन्हें सेट करने से मना कर देता है और Unreachable field फेंकता है। CapSkip ने Capy Puzzle को वर्ज़न 1.4.0 में जोड़ा। यह गाइड key, कॉल, दो सेकंड का ठहराव, उस इनकार से बचकर किया जाने वाला सबमिट, दोबारा कोशिशें और दूसरी मशीन पर चलने वाला सॉल्वर कवर करती है।
आपको क्या चाहिए
- किसी Windows मशीन पर चल रहा CapSkip 1.4.0 या उसके बाद का संस्करण। Capy Puzzle उसी रिलीज़ में जोड़ा गया था, CaptchaFox और Friendly Captcha के साथ।
- Symfony 8 components के लिए PHP 8.4.1 या उसके बाद का संस्करण। PHP 8.2 या 8.3 पर Composer इनके बजाय 7.4 LTS रिलीज़ चुनता है, और नीचे का कोड दोनों पर बिना बदलाव के चलता है। capskip पैकेज को curl और json extensions भी चाहिए।
- capskip/capskip का 1.3.0 या उसके बाद का वर्ज़न, जो capy method वाली पहली रिलीज़ है, और साथ में चार Symfony components: BrowserKit, HttpClient, Mime और CssSelector।
- उस पेज का URL जो पज़ल दिखाता है। key, Capy host और फॉर्म, सब उसी पेज से मिलते हैं।
- सॉल्वर के लिए एक पता। Local मोड में CapSkip केवल उसी डिवाइस के लिए 127.0.0.1 पर जवाब देता है। Server मोड में वह आपके नेटवर्क पते या पब्लिक IP पर सुनता है, ताकि किसी दूसरी मशीन पर चल रही script 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, BrowserKit के साथ DomCrawler भी इंस्टॉल कर देता है, लेकिन बाकी तीन BrowserKit की ज़रूरी dependencies नहीं हैं, इसलिए उनके नाम आप खुद लिखें। HttpClient के बिना HttpBrowser बनाया ही नहीं जा सकता, और CssSelector के बिना नीचे की filter() कॉल LogicException फेंकती हैं। Mime वह component है जो देर से फेल होता है। पेज fetch उसके बिना भी चलता है, और सिर्फ़ फॉर्म पोस्ट फेल होती है, एक ऐसे LogicException के साथ जो आपको symfony/mime इंस्टॉल करने को कहता है।
स्टेप 1: पेज को HttpBrowser में खोलें और key खोजें
HttpBrowser, Symfony के HTTP client को एक छोटे ब्राउज़र में लपेट देता है: एक cookie jar, एक history, और redirects जिनका पीछा वह खुद करता है। इससे वह session cookie और CSRF token संभल जाते हैं जो Capy वाले लॉगिन फॉर्म के साथ आमतौर पर आते हैं। हालाँकि, error status पर वह exception नहीं फेंकता। 403 या 404 किसी भी दूसरे पेज की तरह लौट आता है, इसलिए key ढूँढने से पहले status जाँचें, जैसा यह block करता है। इसका बाकी API BrowserKit component के दस्तावेज़ में कवर किया गया है।
<?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 सार्वजनिक होती है और हर विज़िटर के लिए एक जैसी होती है, और पज़ल की key PUZZLE_ से शुरू होती है। ज़्यादातर पेज इसे widget script के URL के k पैरामीटर में रखते हैं, जो उस HTML का हिस्सा है जिसे HttpBrowser डाउनलोड करता है। कुछ पेज फॉर्म में एक hidden capy_captchakey input भी रेंडर करते हैं, और script न हो तो block उसी को पढ़ता है। script URL में /puzzle/get_js/ से पहले का पूरा हिस्सा वह Capy API है जिसके पीछे key रहती है, और CapSkip उसे api_server कहता है। इसका डिफ़ॉल्ट https://jp.api.capy.me है, जहाँ लाइव सर्विस चलती है। दूसरे सॉल्वरों के लिए लिखे गए सैंपल अक्सर क्षेत्रीय prefix के बिना api.capy.me दिखाते हैं, और वह host अब resolve नहीं होता, इसलिए अगर आपने ऐसा कोई सैंपल कॉपी किया है, तो उसमें से यह host हटा दें। जो src दो slash से शुरू होता है, उसके आगे https: लगा दिया जाता है, क्योंकि Capy API सिर्फ़ HTTPS पर ही सर्व होता है।
आख़िरी लाइनें फॉर्म चुनती हैं। closest() कॉल widget से ऊपर की ओर चलते हुए उस फॉर्म तक पहुँचती है जिसके अंदर widget है। जब script head में या फॉर्म के बाद होती है, तो कोई फॉर्म उसे नहीं घेरता, और तब block पेज के पहले POST फॉर्म को चुन लेता है। अगर पेज पर दो POST फॉर्म हों, तो इसके बजाय अपना फॉर्म filter() से उसकी id के ज़रिए चुनें।
स्टेप 2: capy() कॉल
PHP में Capy Puzzle हल करने के लिए आपको एक ही method चाहिए। यह key, पेज URL और options का एक वैकल्पिक array लेता है, और डिफ़ॉल्ट host वाले पेज के लिए पहले दो ही काफ़ी हैं।
// 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नतीजा एक array है। नाम वाली तीनों keys पढ़ें। code key में सर्वर का पूरा जवाब होता है, चारों फ़ील्ड वाला एक array, जो logging के लिए काम आता है। respKey एक खाली स्ट्रिंग है जो सिर्फ़ दूसरी सर्विसों के साथ compatibility के लिए मौजूद है। answer एक लंबी स्ट्रिंग है जो कुछ-कुछ 0xax8ex0xax84x जैसी शुरू होती है। यह वह ड्रैग पाथ है जिसे widget टुकड़ा खिसकते समय रिकॉर्ड करता, कोई coordinate नहीं।
आपने जो पता टाइप किया था, उसके बजाय वह URL पास करें जिस पर HttpBrowser असल में पहुँचा, जैसा यह block करता है। साइट उस पते को redirect करके आख़िर में slash या भाषा का prefix जोड़ सकती है। पज़ल लाते समय CapSkip पेज URL को Referer के रूप में भेजता है, इसलिए यह वही पेज होना चाहिए जिससे फॉर्म असल में आया था।
method जिन ऑप्शन को स्वीकार करता है
api_server के अलावा, जिसे apiServer नाम से भी पास किया जा सकता है, यह method type और uri keys वाला एक proxy array लेता है। यह useragent भी लेता है, जिसे CapSkip उस इकलौती रिक्वेस्ट पर भेजता है जो वह पज़ल लाने के लिए करता है। जिस भी ऑप्शन की वैल्यू null हो, वह हटा दिया जाता है, इसीलिए ऊपर वाली कॉल हर बार पार्स किया गया host पास कर सकती है। एक version ऑप्शन भी है, लेकिन उसमें सिर्फ़ puzzle स्वीकार होता है। Capy का दूसरा परिवार, avatar, एक अलग endpoint के पीछे एक अलग challenge है, इसलिए SDK ऐसा जवाब लौटाने के बजाय, जिसे साइट ठुकरा देगी, उसे मना कर देता है।
हर कॉल के दो ऑप्शन जानने लायक हैं। सेकंड में दिया गया timeout उस एक solve के लिए क्लाइंट के defaultTimeout की जगह ले लेता है। पोलिंग की ऊपरी सीमा भी हर कॉल पर सेट की जा सकती है, लेकिन सिर्फ़ polling_interval के रूप में। camelCase वाला pollingInterval constructor का ऑप्शन है, और capy() को पास करने पर वह ValidationException फेंकता है, जिसमें उस ऑप्शन का नाम होता है। खाली key या URL, कोई अनजान ऑप्शन, और HTTP, HTTPS, SOCKS5 या SOCKS5H के अलावा कोई proxy type भी ValidationException फेंकते हैं, और यह कोई भी रिक्वेस्ट आपकी मशीन से निकलने से पहले ही हो जाता है।
Capy solve में लगभग दो सेकंड क्यों लगते हैं
खाँचा ढूँढना जल्दी हो जाता है। CapSkip पज़ल की तस्वीर लाता है, थोड़े से pixel गणित से खाली जगह ढूँढता है और ड्रैग पाथ बनाता है, और इसमें कोई ब्राउज़र नहीं लगता। फिर वह जानबूझकर इंतज़ार करता है।
Capy पज़ल जारी करने और जवाब मिलने के बीच का समय मापता है, और जो जवाब इतनी जल्दी लौटे कि कोई इंसान उतनी देर में टुकड़े को खींच ही न पाता, उसे ठुकरा देता है। यह इनकार गलत जवाब जैसा ही दिखता है, इसलिए बहुत जल्दी पहुँचाया गया सही solve बिल्कुल खराब सॉल्वर जैसा दिखता है। इसलिए CapSkip हर Capy नतीजे को पज़ल लेने के दो सेकंड बाद तक रोके रखता है। यह ठहराव एक sleep है, इसलिए इसकी कीमत latency है, CPU नहीं।
डिफ़ॉल्ट सेटिंग्स में एक कॉल में चार सेकंड के क़रीब लगते हैं, और इसकी वजह SDK के poll करने का तरीका है। सबमिट करते ही वह नतीजा माँगता है, फिर चौथाई सेकंड बाद दोबारा, और हर बार अंतर को दोगुना करता जाता है, अपने 5 सेकंड वाले pollingInterval तक। poll 0, 0.25, 0.75, 1.75 और 3.75 सेकंड पर होते हैं, इसलिए दो सेकंड पर तैयार जवाब पाँचवें poll का इंतज़ार करता है। हमारे टेस्ट में डिफ़ॉल्ट क्लाइंट 3.8 सेकंड बाद लौटा, और ऊपर की तरह pollingInterval को 0.5 पर सेट करके बनाया गया क्लाइंट 2.3 सेकंड बाद। पोस्ट से पहले अपनी तरफ़ से कोई देरी न जोड़ें, और ठहराव को घटाने की कोशिश न करें। यही ठहराव जवाब को पास करवाता है।
स्टेप 3: तीनों capy_ फ़ील्ड जोड़ें और सबमिट करें
BrowserKit कोड आमतौर पर यहीं टूटता है। स्वाभाविक कदम यह लगता है कि फॉर्म object को भरें और सबमिट कर दें, चाहे setValues() से, submit() के values argument से, या फ़ील्ड को array element की तरह assign करके। ये तीनों तरीके एक InvalidArgumentException फेंकते हैं, जिसमें Unreachable field के बाद फ़ील्ड का नाम लिखा होता है। DomCrawler फॉर्म को उसी HTML से बनाता है जिसे उसने पार्स किया। Capy widget असली ब्राउज़र में चलते समय अपने तीनों inputs 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() method फॉर्म के फ़ील्ड वैसे ही लौटाता है जैसे कोई ब्राउज़र उन्हें सबमिट करता, बटन के बिना और hidden CSRF token समेत, उसी nested रूप में जो PHP brackets वाले नामों के लिए इस्तेमाल करता है। फिर array_replace() आपके लॉगिन फ़ील्ड और तीनों वैल्यू उनके ऊपर जोड़ देता है। यह उस capy_captchakey input को भी overwrite कर देता है जिसे पेज ने खुद रेंडर किया था, ताकि key कभी दो बार न जाए। आख़िर में request() यह सब फॉर्म के अपने method और पते के साथ भेज देता है।
फॉर्म object को दरकिनार करने से HttpBrowser वाली तरफ़ आपका कुछ नहीं बिगड़ता। submit() अंदर से request() method को ही कॉल करता है। यह अब भी jar की cookies भेजता है, और मौजूदा पेज को Referer के रूप में भेजता है, जो वही header है जिसे Django जैसे सख़्त frameworks HTTPS फॉर्म पोस्ट पर जाँचते हैं। यह अब भी redirect का पीछा करता है, और पीछा करने से पहले उस cookie को सहेज लेता है जो 302 सेट करता है। इसलिए जो लॉगिन कामयाबी पर session बदल देता है, वह jar में नए session के साथ अकाउंट पेज पर पहुँचता है। form node से बने फॉर्म में एक चीज़ छूट जाती है: submit बटन। अगर सर्वर बटन का नाम जाँचता है, तो इसके बजाय फॉर्म को बटन से बनाएँ, selectButton() और बटन के label के साथ।
जवाब को ठीक वैसे ही पोस्ट करें जैसा वह लौटा है। साइट का backend इसे उस पज़ल से मिलाकर जाँचता है जो जारी हुआ था, इसलिए इसे काटना या दोबारा बनाना इसे अमान्य कर देता है। फिर तुरंत पोस्ट करें। CapSkip हर solve के लिए एक नई challenge key बनाता है, और Capy जो पज़ल जारी करता है, उसे उसी key से बाँध देता है। इसलिए key एक ही बार इस्तेमाल होती है और कम समय तक टिकती है, और एक solve से एक सबमिशन होता है। कुछ पेज फॉर्म पोस्ट के बजाय अपनी script के ज़रिए JSON बॉडी के साथ सबमिट करते हैं। DevTools खोलकर एक बार हाथ से सबमिट करें, और तीनों वैल्यू वहीं रखें जहाँ पेज उन्हें रखता है।
स्टेप 4: solve फेल होने पर दोबारा कोशिश
जब आप PHP में Capy Puzzle किसी भी मात्रा में हल करते हैं, तो कुछ solve फेल होंगे। फेल हुआ solve एक ApiException के रूप में आता है, जिसका संदेश CapSkip का कच्चा जवाब होता है, और डिफ़ॉल्ट रूप से उसमें ERROR_CAPTCHA_UNSOLVABLE होता है। res.php के ज़रिए, जिस endpoint पर SDK poll करता है, हर फेल solve एक जैसा दिखता है, और यहाँ दो कारण मायने रखते हैं। खाँचा चूक जाना कभी-कभार होता है, और अगली कोशिश में किसी दूसरी फ़ोटो पर बिल्कुल नया पज़ल आता है, इसलिए वह आमतौर पर कामयाब होती है। जिस key को Capy API मना कर देता है, वह हर कोशिश पर फेल होती है, और CapSkip के Monitor टैब में Capy कार्ड पर Bad Keys की गिनती बढ़ जाती है। पहली स्थिति में दोबारा कोशिश करें और दूसरी को ठीक करें। जो 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;
}
}
}
}block, CapSkip की classes के पूरे नाम लिखता है, इसलिए यह function किसी भी फ़ाइल में चिपकाया जा सकता है, चाहे वह फ़ाइल कुछ भी import करती हो। मना की गई key भी unsolvable के रूप में दिखती है, इसलिए यहाँ उस पर भी दोबारा कोशिश होती है, और वह हर कोशिश पर एक ही तरह फेल होती है, और इसी से आप दोनों में फ़र्क कर पाते हैं। दूसरा रास्ता CapSkip की सेटिंग्स के Capy सेक्शन में Retries सेटिंग है, जो 0 से 3 तक की वैल्यू लेती है और डिफ़ॉल्ट रूप से 0 है। तब CapSkip चूके हुए खाँचे पर खुद दोबारा कोशिश करता है, लेकिन मना की गई key पर कभी नहीं, क्योंकि वह उसी तरह फिर मना हो जाती।
capy method क्लाइंट के defaultTimeout पर poll करता है, जो 120 सेकंड है, क्योंकि Capy solve कोई ब्राउज़र session नहीं, बल्कि एक fetch और थोड़ा हिसाब भर है। CapSkip अपनी घड़ी भी चलाता है। उसकी Capy सेटिंग्स में कोई task 10 threads (Max. Threads) में से किसी एक के लिए 250 सेकंड (Wait Timeout) तक इंतज़ार कर सकता है, और एक कोशिश को 60 सेकंड (Row Timeout) मिलते हैं। एक सामान्य solve इन सभी सीमाओं में कई गुना आराम से समा जाता है। SDK connection errors के दौरान भी poll करता रहता है, इसलिए अगर CapSkip solve के बीच में रुक जाए, तो कॉल NetworkException पर नहीं, TimeoutException पर खत्म होती है।
पूरा चलने वाला उदाहरण
यह रहा पूरा run एक ही फ़ाइल में। logIn() function पेज को अपने अलग HttpBrowser में खोलता है, key और host पढ़ता है, दोबारा कोशिशों के साथ हल करता है और फॉर्म को तीनों वैल्यू के साथ भेजता है। यह वही HttpBrowser लौटाता है, ताकि लॉगिन के बाद की आपकी रिक्वेस्ट नया session साथ ले जाएँ।
<?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 blocks दो exception परिवारों के हिसाब से बँटे हैं। CapSkip के exceptions, RuntimeException को extend नहीं करते, जबकि HTTP client की transport errors करती हैं। इसलिए बंद पड़ी साइट आख़िरी block में पहुँचती है। जो सॉल्वर चल ही नहीं रहा, वह तीसरे block में पहुँचता है, और जो solve के बीच में रुक जाए, वह TimeoutException बनकर दूसरे में। Mime component न हो, तो BrowserKit का LogicException आता है, जिसे यहाँ जानबूझकर कोई नहीं पकड़ता: यह setup की गलती है जिसे एक बार ठीक करना है, run time पर संभालने वाली चीज़ नहीं। पोस्ट से 4xx या 5xx मिलने पर script 1 के साथ exit करती है। हालाँकि, अकेला 200 यह साबित नहीं करता कि लॉगिन कामयाब हुआ, क्योंकि बहुत-सी साइटें फेल लॉगिन का जवाब फिर से वही फॉर्म दिखाकर देती हैं। जिस URL पर आप पहुँचे, उसे जाँचें, या खुद पेज को। इस method के पीछे का raw endpoint यहाँ दर्ज है: API रेफ़रेंस का Capy सेक्शन.
एक साथ कई लॉगिन चलाना
PHP एक समय में एक ही रिक्वेस्ट चलाता है, और हर capy() कॉल अपने लगभग दो सेकंड तक रुकी रहती है। एक साथ ज़्यादा लॉगिन चलाने का सीधा तरीका है ज़्यादा प्रोसेस: हर अकाउंट के लिए script एक बार चलाएँ, और हर एक का अपना HttpBrowser हो। एक ही HttpBrowser को कभी कई लॉगिन के बीच साझा न करें। उसमें एक ही cookie jar और एक ही history होती है, इसलिए दो sessions अपनी cookies, CSRF tokens और Referer आपस में मिला देंगे। एक साथ 10 से ज़्यादा न चलाएँ, क्योंकि CapSkip डिफ़ॉल्ट रूप से एक समय में 10 Capy पज़ल हल करता है और बाकी को खाली thread के लिए क़तार में रखता है।
सब कुछ एक ही प्रोसेस में रखने के लिए, SDK के send() method से कई job सबमिट करें और getResult() से उनके नतीजे इकट्ठा करें। यही वह पैटर्न है जिस पर Friendly Captcha PHP गाइड अपना पूल बनाती है। Capy के लिए send() method के रूप में capy, साथ में captchakey और pageurl लेता है, और JSON चालू होने पर getResult() तीनों वैल्यू एक array के रूप में लौटाता है। हर जवाब को पूरे batch के बाद नहीं, बल्कि उसका नतीजा आते ही पोस्ट करें, क्योंकि जब तक आख़िरी solve पूरे होते हैं, सबसे पहली challenge keys पुरानी होती जाती हैं। बड़ी मात्रा में प्रॉक्सी भी जोड़ें। हर solve, Capy API से एक नया पज़ल लेता है, और एक ही पते से ऐसे पज़ल की लगातार धारा ठीक वही पैटर्न है जिसे पकड़ने के लिए rate limiting बनी है। CapSkip के Capy सेक्शन में एक प्रॉक्सी पूल कॉन्फ़िगर करें, या हर कॉल पर एक proxy array पास करें; यह सिर्फ़ पज़ल fetch को छूता है, जो Capy solve की इकलौती रिक्वेस्ट है।
सॉल्वर को किसी दूसरी मशीन पर चलाना
जब तक script और CapSkip एक ही Windows PC पर हों, 127.0.0.1 सही है। जैसे ही PHP कोड किसी VPS, कंटेनर होस्ट, CI runner या shared hosting पर जाता है, loopback गलत मशीन की ओर इशारा करता है। तब पहला ही solve NetworkException फेंकता है, जिसके संदेश में Failed to connect होता है। CapSkip को Server मोड पर कर दें और वह आपके नेटवर्क पते या पब्लिक IP पर सुनने लगता है, ताकि इनमें से कोई भी उसी API के ज़रिए उसे कॉल कर सके। जब रास्ता इंटरनेट से होकर जाए तो स्टैटिक पब्लिक IP इस्तेमाल करें और API key validation चालू करें। Server मोड शुरू होते ही CapSkip अपना पोर्ट हर पते के लिए खोल देता है, इसलिए उसे सिर्फ़ अपेक्षित मशीनों तक सीमित रखने के लिए बाकी पतों के लिए Windows Firewall में एक block नियम जोड़ें। सॉल्वर अब भी आपकी अपनी Windows मशीन है, और solve अब भी बिना मीटर वाले हैं।
क्लाइंट एनवायरनमेंट वेरिएबल खुद नहीं पढ़ता। CAPSKIP_HOST, CAPSKIP_PORT और CAPSKIP_API_KEY को अपने कोड में पढ़ें और उन्हें constructor को पास करें, जैसा पूरा उदाहरण करता है, ताकि वही script आपकी डेस्क पर भी चले और सर्वर पर भी।
आम errors और उनका मतलब
| आप जो देखते हैं | कारण | फिक्स |
|---|---|---|
| InvalidArgumentException: Unreachable field "capy_answer" | setValues(), values के साथ submit(), या ऐसे फ़ील्ड पर array assignment जिसे widget असली ब्राउज़र में बनाता है | स्टेप 3 की तरह, getPhpValues() और तीनों वैल्यू request() से भेजें |
| सिर्फ़ फॉर्म पोस्ट पर LogicException, जो symfony/mime इंस्टॉल करने को कहता है | Mime component के बिना BrowserKit रिक्वेस्ट बॉडी नहीं बना सकता | composer require symfony/mime |
| LogicException, जो CssSelector component इंस्टॉल करने को कहता है | filter() को CSS selector के साथ कॉल किया गया, और symfony/css-selector मौजूद नहीं है | composer require symfony/css-selector |
| capy() के तीनों वैल्यू लौटाने के बावजूद साइट कहती है कि session expire हो गया | पोस्ट किसी नए HttpBrowser या सादी HTTP कॉल से गई, पेज की cookies के बिना | पोस्ट उसी HttpBrowser से भेजें जिसने पेज fetch किया था |
| साइट जवाब को ही ठुकरा देती है | जवाब बदल दिया गया, या वैल्यू में से सिर्फ़ एक या दो ही फॉर्म तक पहुँचीं | answer को हूबहू, बाकी दो वैल्यू के साथ, एक ही रिक्वेस्ट में भेजें |
| एक बार काम कर चुकी पोस्ट दूसरी बार फेल हो जाती है | challenge key एक ही बार इस्तेमाल होती है और कम समय तक टिकती है | हर सबमिशन के लिए दोबारा हल करें, और तुरंत पोस्ट करें |
| एक ही key के लिए हर कोशिश पर एक ApiException | Capy API ने key मना कर दी, या api_server गलत host की ओर इशारा करता है | key को prefix समेत दोबारा कॉपी करें, और script टैग का host जाँचें |
| कभी-कभार आने वाला ApiException, जिसमें ERROR_CAPTCHA_UNSOLVABLE होता है | CapSkip उस पज़ल में खाँचा नहीं ढूँढ पाया | दोबारा कोशिश करें; अगली कोशिश में दूसरा पज़ल आता है |
| कुछ भी भेजे जाने से पहले ही ValidationException | खाली key या URL, ऐसा ऑप्शन जो यह method नहीं लेता, जैसे हर कॉल पर दिया गया pollingInterval, version के रूप में avatar, या ऐसा proxy type जिसे CapSkip स्वीकार नहीं करता | जाँचें कि key मिली थी, और संदेश में बताए गए ऑप्शन को ठीक करें या हटा दें |
| पहली कॉल पर ERROR_KEY_DOES_NOT_EXIST या ERROR_WRONG_USER_KEY वाला ApiException | CapSkip में API key validation चालू है, और script ने डिफ़ॉल्ट key भेजी या कोई key नहीं भेजी | CAPSKIP_API_KEY को CapSkip में कॉन्फ़िगर की गई किसी key पर सेट करें |
| हर कॉल में लगभग चार सेकंड लगते हैं | poll के बीच का अंतर डिफ़ॉल्ट 5 सेकंड की ऊपरी सीमा की ओर दोगुना होता जाता है, इसलिए दो सेकंड पर तैयार जवाब 3.75 सेकंड वाले poll का इंतज़ार करता है | constructor में pollingInterval को 0.5 पर सेट करें |
| NetworkException: Failed to connect to 127.0.0.1 port 8080 | CapSkip चल नहीं रहा, या host या port गलत है; रिमोट मशीन पर कोई फ़ायरवॉल भी कनेक्शन ड्रॉप कर सकता है | CapSkip शुरू करें, फिर देखें कि उसे Local मोड में होना चाहिए या Server मोड में |
| RuntimeException: The page answered 403, not the form | किसी भी कैप्चा से पहले ही साइट ने पेज fetch को ठुकरा दिया | उसी पते से पेज को किसी असली ब्राउज़र में खोलें; हो सकता है साइट Symfony BrowserKit user agent को ब्लॉक कर रही हो |
FAQ
क्या मैं इसके लिए अब भी Goutte इस्तेमाल कर सकता हूँ?
कर सकते हैं, लेकिन इसकी कोई वजह नहीं है। Goutte की आख़िरी रिलीज़, 4.0.3, अप्रैल 2023 में आई थी, और Packagist इस पैकेज को symfony/browser-kit के पक्ष में abandoned दिखाता है। 4.0.3 में इसकी Client class, HttpBrowser की एक deprecated subclass है जो कुछ नया नहीं जोड़ती। इसे HttpBrowser से बदलना एक लाइन का बदलाव है, और इस गाइड में लिखा सब कुछ वैसे ही लागू होता है। जो Goutte कोड फॉर्म को setValues() से भरता है, वह भी इसी वजह से वही Unreachable field error पाता है।
क्या Symfony BrowserKit user agent की वजह से जवाब ठुकरा दिया जाता है?
Capy की तरफ़ से नहीं। Capy का जवाब किसी ब्राउज़र से बँधा नहीं होता, CapSkip इसके लिए कोई user agent नहीं लौटाता, और जो वैकल्पिक user agent आप पास कर सकते हैं, वह सिर्फ़ पज़ल लाने वाली रिक्वेस्ट को बदलता है। हालाँकि HttpBrowser खुद को Symfony BrowserKit के रूप में पेश करता है, और साइट अपनी वजहों से उसे ठुकरा सकती है। अगर ऐसा हो, तो पहली रिक्वेस्ट से पहले HttpBrowser के setServerParameter() method और HTTP_USER_AGENT key से कोई असली user agent सेट करें।
क्या मैं यही काम इसके बजाय Guzzle से कर सकता हूँ?
हाँ। capy कॉल और तीनों फ़ील्ड वही रहते हैं। लेकिन Guzzle तब तक कोई cookie नहीं रखता जब तक आप उसे cookie jar न दें, कोई Referer नहीं भेजता, और 403 पर exception फेंकता है। जो काम HttpBrowser आपके लिए करता है, वह आपको हाथ से सेट करना पड़ता है। CaptchaFox PHP गाइड Guzzle का यही सेटअप, cookie jar और Referer समेत, कदम-दर-कदम समझाती है।
क्या shared hosting पर चल रहा PHP सॉल्वर तक पहुँच सकता है?
हाँ, अगर होस्टिंग प्रोवाइडर आपके पोर्ट तक outbound कनेक्शन की अनुमति देता है। कनेक्शन सेटिंग्स में CapSkip को Server मोड पर कर दें ताकि वह loopback के बजाय किसी नेटवर्क पते पर सुने, वह पता अपनी script में CAPSKIP_HOST से पढ़ें, और उसे constructor को पास करें। VPS, कंटेनर होस्ट, CI runner और shared hosting, सब उसी HTTP API के ज़रिए जुड़ते हैं। जब रास्ता इंटरनेट से होकर जाए, तो स्टैटिक पब्लिक IP इस्तेमाल करें, साथ में उन पतों के लिए Windows Firewall में एक block नियम रखें जिन्हें पोर्ट तक नहीं पहुँचना चाहिए। सॉल्वर आपके अपने हार्डवेयर पर ही रहता है, इसलिए solve गिनने के तरीके में कुछ नहीं बदलता।
संक्षेप में
PHP में Capy Puzzle हल करने के लिए, पेज को एक HttpBrowser में खोलें और widget script के k पैरामीटर से PUZZLE_ key लें। script का host भी नोट कर लें, कहीं वह डिफ़ॉल्ट न हो। pollingInterval को 0.5 पर सेट करके बने क्लाइंट से capy() को key और उस URL के साथ कॉल करें जिस पर HttpBrowser पहुँचा, और उसे अपने लगभग दो सेकंड लेने दें। फिर फॉर्म object के setters को छोड़ दें: getPhpValues() के साथ captchakey, challengekey और answer को request() से भेजें, हूबहू, एक साथ और तुरंत, उसी HttpBrowser से। कभी-कभार की चूक पर दोबारा कोशिश करें, एक साथ 10 से ज़्यादा लॉगिन न चलाएँ, और जैसे ही script सॉल्वर की मशीन से बाहर जाए, Server मोड पर चले जाएँ।
- यह टाइप कैसे काम करता है और सॉल्वर क्या-क्या कवर करता है: Capy Puzzle सॉल्वर पेज.
- कैप्चा के वे सभी टाइप जिन्हें PHP पैकेज संभालता है: PHP कैप्चा सॉल्वर पेज.
दोबारा कोशिशों के बारे में एक आख़िरी बात। हर कोशिश में एक अलग पज़ल आता है, इसलिए चूक का सही इलाज दोबारा कोशिश करना ही है, और अपनी ही मशीन पर चलने वाले असीमित कैप्चा सॉल्वर के साथ दूसरी कोशिश की कीमत बस कुछ सेकंड होती है, एक और बिल किया गया solve नहीं।
