nodriver में Async SDK से कैप्चा कैसे हल करें

nodriver में कैप्चा वाला step वही आम तीन चालें हैं: पेज से sitekey पढ़ें, उसे सॉल्वर को भेजें, token को JavaScript से वापस लिख दें। फ़र्क़ यह है कि nodriver जड़ से लेकर ऊपर तक asynchronous है। यह Chrome को एक asyncio websocket पर चलाता है, इसलिए कोई blocking solve सिर्फ़ आपकी script को इंतज़ार नहीं कराता, वह उस socket को भी अटका देता है जो हर DevTools संदेश ढोता है। इसे async client के साथ जोड़ें और पूरी चीज़ जवाब देती रहती है।
आपको क्या चाहिए
- Python 3.10 या उससे नया, साथ में nodriver 0.50 और CapSkip SDK इंस्टॉल किए हुए।
- जहाँ script चलती है वहाँ Chrome, Chromium, Edge या Brave इंस्टॉल हो। nodriver उसे सीधे launch करता है।
- सुरक्षित form का पेज URL। sitekey runtime पर पढ़ी जाती है।
- CapSkip, Local mode में अगर script और सॉल्वर एक ही मशीन साझा करते हैं, या Server mode में अगर नहीं करते। दोनों की जानकारी यहाँ है: कनेक्शन सेटिंग्स.
# Both packages, one line. pip install nodriver capskip
nodriver इस काम की शक्ल क्यों बदल देता है
nodriver, undetected-chromedriver का आधिकारिक उत्तराधिकारी है, उसी लेखक का लिखा हुआ, और इसकी सबसे बड़ी बात यह है कि पूरे stack में कहीं कोई webdriver नहीं है और कोई Selenium नहीं है। यह ख़ुद अपने launch किए हुए ब्राउज़र से DevTools Protocol बोलता है। न कोई chromedriver binary patch करनी है, न कोई driver वर्ज़न Chrome के साथ मिलाकर रखना है।
कैप्चा वाले काम के लिए उस वाक्य का दूसरा आधा हिस्सा मायने रखता है: यह पूरी तरह asynchronous है। connection एक websocket है जिसे asyncio सँभालता है, और एक background task उसमें से protocol संदेश पढ़ता रहता है। हर element lookup, हर navigation और हर event handler इसी बात पर टिका है कि उस task को चलने का मौक़ा मिले। बीच में कोई synchronous सॉल्वर कॉल कर दें और पूरे solve के दौरान process में और कुछ नहीं चलता, जो reCAPTCHA v2 के लिए आम तौर पर पंद्रह से पैंतालीस सेकंड होता है।
तो इस framework के लिए नियम छोटा है। async client इस्तेमाल करें, और उसे await करें।
एक बात साफ़ कह देना ठीक रहेगा: webdriver न होने का मतलब detection न होना नहीं है। driver हटाने से एक signal हट जाता है, बाकी पूरा fingerprint जहाँ था वहीं रहता है। challenge हल करना और ब्राउज़र जैसा दिखना, ये दो अलग काम हैं, और यह पोस्ट पहले वाले को कवर करती है।
Step 1: page से sitekey पढ़ें
sitekey host document पर रहती है, widget के iframe के अंदर नहीं। Google का markup उसे एक container पर data-sitekey attribute के रूप में रखता है, और nodriver का select method उस container को CSS selector से ढूँढ लेता है। आख़िरी लाइन वाले bracket access पर ध्यान दें, क्योंकि लोग यहीं फँसते हैं।
# pip install nodriver
import nodriver as uc
async def main():
browser = await uc.start()
page = await browser.get("https://example.com/page-with-recaptcha")
# select() retries for 10 seconds by default, so it doubles
# as a wait condition for a widget that renders late.
holder = await page.select("div.g-recaptcha")
sitekey = holder.attrs["data-sitekey"]
print(sitekey) # 6Lxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
uc.loop().run_until_complete(main())attribute के नाम element पर ठीक वैसे ही रखे जाते हैं जैसे HTML ने उन्हें लिखा था, hyphens समेत, इसलिए किसी attribute को पढ़ने का भरोसेमंद तरीका सिर्फ़ bracket access है। dotted shortcut देखने में लगता है कि काम करना चाहिए, पर चुपचाप नहीं करता: किसी element से data_sitekey माँगने पर वह error उठाने के बजाय None लौटा देता है, क्योंकि वह lookup एक default पर जा गिरता है। फिर वही None एक ख़ाली key बनकर सॉल्वर तक पहुँच जाता है और बहुत बाद में ERROR_GOOGLEKEYबनकर सामने आता है, उस लाइन से बहुत दूर जिसने उसे पैदा किया था। जानने लायक असली rename सिर्फ़ एक है: class attribute को class_ के नीचे रखा जाता है ताकि वह Python के keyword से न टकराए।
कुछ साइटें sitekey को host पेज पर दिखाती ही नहीं और उसे सिर्फ़ widget के iframe URL में भेजती हैं। ऐसे में उसे query string से पढ़ें।
# Fallback: the k= parameter on the anchor iframe.
from urllib.parse import urlparse, parse_qs
frame = await page.select("iframe[src*='recaptcha/api2/anchor']")
sitekey = parse_qs(urlparse(frame.attrs["src"]).query)["k"][0]Step 2: उसे socket को अटकाए बिना हल करें
Python SDK दो clients के साथ आता है। CapSkip synchronous है और AsyncCapSkip कोई alias नहीं, बल्कि सचमुच का asyncio implementation है, और इस framework को ठीक यही चाहिए। दोनों आपकी अपनी मशीन पर port 8080 पर चल रहे सॉल्वर से बात करते हैं, और दोनों में से कोई प्रति-हल बिल नहीं बनाता।
# pip install capskip from capskip import AsyncCapSkip solver = AsyncCapSkip(host="127.0.0.1", port=8080) # Same call shape for v3 (version="v3") and Enterprise # (enterprise=1). Invisible v2 takes invisible=1. result = await solver.recaptcha(sitekey=sitekey, url=PAGE_URL) token = result["code"] # the g-recaptcha-response value
call awaitable है, इसलिए कई tabs बिना किसी threading के एक साथ solve कर सकते हैं। पेज खोलें, solves को gather करें, फिर हर token को उसी tab में inject करें जिसका वह है। व्यापक pattern, जिसमें यह भी शामिल है कि SDK सपाट interval पर sleep करने के बजाय अपनी polling को कैसे पीछे खिसकाता है, यहाँ कवर किया गया है: कैप्चा को समानांतर में हल करना.
# Three tabs, three solves, one wait.
import asyncio
results = await asyncio.gather(*[
solver.recaptcha(sitekey=k, url=u) for k, u in targets
])Turnstile और GeeTest के अपने methods हैं, और दोनों का आकार ऊपर वाली call जैसा ही है। हर एक की पूरी parameter सूची यहाँ है: CapSkip API डॉक्यूमेंटेशन.
Step 3: token inject करें और submit करें
response वाला textarea display:none से छिपा होता है, इसलिए उसमें टाइप करना किसी भी ऑटोमेशन टूल में मुमकिन नहीं है। उसे आप JavaScript से लिखते हैं। nodriver का evaluate method एक expression string लेता है और उसके साथ arguments भेजने का कोई रास्ता नहीं देता, इसलिए token को उसी string में embed करना पड़ता है, और ऐसा करने का सुरक्षित तरीका f-string नहीं बल्कि json.dumps है। JSON की एक string literal, JavaScript की भी वैध string literal होती है, quoting और escaping समेत।
# json.dumps gives a correctly quoted JS string literal.
import json
await page.evaluate(
"document.getElementById('g-recaptcha-response').value = "
+ json.dumps(token)
)
# Then submit the form the way the page expects.
button = await page.select("button[type=submit]")
await button.click()value सही जगह पहुँची या नहीं, यह जाँचने में अपना एक अलग जाल है, और वह आपके दस सेकंड के ध्यान के लायक है। return_by_value लगा हो, तब भी evaluate सादा Python value सिर्फ़ तभी लौटाता है जब वह value truthy हो। ख़ाली string या शून्य निकल जाता है और आपके हाथ में एक protocol object आ जाता है। इसलिए length पढ़कर उसकी जाँच न करें, क्योंकि शून्य length तो ठीक वही हालत है जिसे आप पकड़ना चाहते हैं। ऐसा कुछ लौटाएँ जो कभी falsy हो ही न सके।
# String() keeps a zero-length answer truthy, so the check
# reports the real number instead of a protocol object.
length = await page.evaluate(
"String(document.getElementById('g-recaptcha-response').value.length)",
return_by_value=True,
)
print(length) # "0" means the injection did not landअगर साइट submit के समय textarea पढ़ने के बजाय कोई callback तय करती है, तो value सेट करने के बाद उसे कॉल करें। function का नाम हर साइट का अलग होता है, इसलिए अंदाज़ा लगाने के बजाय उसे पेज के अपने markup से पढ़ें। दोनों ही हालत में यह सामान्य reCAPTCHA v2 ही है: callback सिर्फ़ यह बदलता है कि आप token कैसे सौंपते हैं, यह नहीं कि वह हल कैसे होता है। reCAPTCHA v2 सॉल्वर पेज दोनों submission शैलियों को कवर करता है।
पूरा चलने वाला उदाहरण
ऊपर की सारी बातें एक ही script में। सॉल्वर एक बार बनाया जाता है और दोबारा इस्तेमाल होता है, और ब्राउज़र को finally block में बंद किया जाता है ताकि कोई नाकाम solve पीछे कोई Chrome process छोड़कर न जाए।
# pip install nodriver capskip
import json
import nodriver as uc
from capskip import AsyncCapSkip
PAGE_URL = "https://example.com/page-with-recaptcha"
async def main():
solver = AsyncCapSkip(host="127.0.0.1", port=8080)
browser = await uc.start()
try:
page = await browser.get(PAGE_URL)
holder = await page.select("div.g-recaptcha")
sitekey = holder.attrs["data-sitekey"]
if not sitekey:
raise RuntimeError("Widget found but data-sitekey was empty.")
result = await solver.recaptcha(sitekey=sitekey, url=PAGE_URL)
await page.evaluate(
"document.getElementById('g-recaptcha-response').value = "
+ json.dumps(result["code"])
)
button = await page.select("button[type=submit]")
await button.click()
await page.sleep(2)
print(page.target.url) # the page you land on after submitting
finally:
browser.stop()
uc.loop().run_until_complete(main())सॉल्वर को किसी दूसरी मशीन पर चलाना
nodriver देर सबेर किसी सर्वर पर पहुँच ही जाता है, और वहाँ उसे दो चीज़ें चाहिए: एक Chromium binary, और एक desktop session जिस पर Chrome draw कर सके। headless mode डिफ़ॉल्ट रूप से बंद है, इसलिए जिस सर्वर पर desktop session न हो, वहाँ headless को साफ़ तौर पर चालू करना पड़ता है। सॉल्वर को उसके साथ यह सफ़र करना ज़रूरी नहीं है।
CapSkip में दो connection modes हैं। Local, 127.0.0.1 से bind होता है और सिर्फ़ उसी डिवाइस को जवाब देता है, जो script लिखते समय सही सेटिंग है। Server, आपके नेटवर्क या public IP से bind होता है, जिससे कोई scraping VM, कोई container host या कोई दूसरा workstation उसी सॉल्वर को API के ज़रिए कॉल कर लेता है। एक static public IP उस पते को स्थिर रखता है। कोड में और कुछ नहीं बदलता, सिर्फ़ वह host बदलता है जो आप पास करते हैं, और लागत में भी कुछ नहीं बदलता, क्योंकि हार्डवेयर तब भी आपका ही है।
# Same SDK, same call. Only the host moves. solver = AsyncCapSkip(host="10.0.0.12", port=8080, apiKey="YOUR_API_KEY")
जैसे ही सॉल्वर किसी नेटवर्क पते पर सुनना शुरू करे, key validation चालू कर दें, और हर मशीन को उसकी अपनी key दें ताकि एक को बाकियों को छुए बिना रद्द किया जा सके। सेटअप गाइड दोनों modes को विस्तार से समझाती है।
यह करते समय एक नाम की टकराहट ध्यान में रखें। nodriver का अपना start function भी एक host और एक port लेता है, और वे किसी Chrome debugging endpoint का पता बताते हैं जिससे आप attach होना चाहते हैं, सॉल्वर का नहीं। दोनों दे दिए तो nodriver कोई ब्राउज़र launch करेगा ही नहीं। सॉल्वर का पता client के constructor में जाता है, और कहीं नहीं।
आम errors और उनका मतलब
| आप जो देखते हैं | कारण | फिक्स |
|---|---|---|
| attrs lookup पर AttributeError | select() को कुछ नहीं मिला और उसने None लौटा दिया | selector को चौड़ा करें, या select का timeout बढ़ाएँ |
| sitekey None है और कोई error तक नहीं | dotted access hyphen वाले attribute तक नहीं पहुँच सकता | उसे attrs से bracket access के साथ पढ़ें |
| ERROR_GOOGLEKEY | एक ख़ाली sitekey सॉल्वर तक पहुँच गई | उस पर एक solve ख़र्च करने से पहले value जाँच लें |
| पूरे solve के दौरान script अटकी रहती है | किसी synchronous client ने event loop को block कर दिया | AsyncCapSkip इस्तेमाल करें और call को await करें |
| NetworkException | CapSkip चल नहीं रहा, या host ग़लत है | ऐप शुरू करें, या host को सर्वर पते पर point करें |
| TimeoutException | solve recaptchaTimeout से ज़्यादा लंबा चला | उसे डिफ़ॉल्ट 300 सेकंड से ऊपर बढ़ाएँ |
| evaluate एक object लौटाता है, string नहीं | value falsy थी, इसलिए सादा return छूट गया | String() में length लपेटें, value को नहीं |
FAQ
अगर मैं सिर्फ़ एक बार solve करता हूँ तो क्या synchronous client रख सकता हूँ?
रख सकते हैं, और छोटी script पर शायद आपको कभी पता भी न चले। आप जो चीज़ छोड़ रहे हैं वह है solve के दौरान आने वाला हर protocol संदेश: navigation events, load events, वह सब जिसका किसी event handler को इंतज़ार था। लंबे run पर यह इस तरह सामने आता है कि lookups बिना किसी दिखने वाली वजह के timeout होने लगते हैं। async client की कीमत एक import और एक await है, इसलिए यह सौदा करने की कोई ख़ास वजह नहीं है।
क्या मुझे reCAPTCHA iframe के अंदर जाना पड़ेगा?
नहीं, और लोग इसी हिस्से को ज़रूरत से ज़्यादा उलझा देते हैं। checkbox एक iframe में रहता है, पर sitekey attribute और छिपा हुआ response textarea, दोनों host document के हैं। frame को आप सिर्फ़ तब छूते हैं जब साइट sitekey को पेज पर देती ही नहीं और आपको उसे frame के अपने URL से पढ़ना पड़ता है।
मैं undetected-chromedriver से migrate कर रहा हूँ। इसमें से क्या क्या साथ चलता है?
तीनों चालें ज्यों की त्यों साथ चलती हैं, क्योंकि वे कभी driver पर टिकी थीं ही नहीं: sitekey पढ़ें, उसे हल करें, token को textarea में लिख दें। जो साथ नहीं चलता वह है उनके इर्द-गिर्द की API, क्योंकि अब हर call awaitable है और कोई driver object है ही नहीं। nodriver एक helper भी देता है जो चल रहे undetected-chromedriver instance को एक browser object में बदल देता है, जिससे आप script को टुकड़ों में हिला सकते हैं। पुराना तरीका यहाँ लिखा गया है: undetected-chromedriver कैप्चा गाइड.
मेरा स्क्रैपर एक VPS पर चलता है। सॉल्वर कहाँ रखूँ?
जहाँ आपको ठीक लगे, बस दोनों एक दूसरे तक पहुँच पाने चाहिए। Server mode सॉल्वर को loopback पते के बजाय किसी नेटवर्क पते पर सुनने लगाता है, इसलिए VPS उसे API के ज़रिए ठीक वैसे ही कॉल करता है जैसे किसी भी internal service को करता। host argument को उसी पते पर लगाएँ, key validation चालू करें, और VPS को उसकी अपनी key दें। सॉल्वर को किसी display की ज़रूरत नहीं होती, जो सुविधाजनक है, क्योंकि ब्राउज़र को तो होती है।
संक्षेप में
sitekey को attrs पर bracket access से पढ़ें, उसे 127.0.0.1:8080 पर AsyncCapSkip से हल करें, token को json.dumps के साथ evaluate के ज़रिए inject करें, फिर submit करें। हर चीज़ को await करें, क्योंकि synchronous solve उसी socket को पकड़े रखता है जो ब्राउज़र को चलाता है। Selenium और Playwright समेत Python की व्यापक तस्वीर के लिए देखें: Python कैप्चा सॉल्वर पेज.
crawl को बड़ा करने से पहले एक नतीजा साफ़ कह देना ठीक रहेगा। चूँकि CapSkip आपके अपने हार्डवेयर पर चलने वाला एक असीमित कैप्चा सॉल्वर है, इसलिए एक हज़ार पेज दोबारा आज़माने वाले run की लागत ठीक उतनी ही है जितनी दस पेज दोबारा आज़माने वाले की।
