Selenium Grid (RemoteWebDriver) पर कैप्चा कैसे हल करें

selenium grid captcha - How to Solve CAPTCHAs on Selenium Grid (RemoteWebDriver)

Selenium Grid पर कैप्चा हल करना ठीक वैसे ही काम करता है जैसे लोकल पर, बस एक अंतर के साथ जो ज़्यादातर लोगों को चकमा दे जाता है। browser node पर चलता है। आपका टेस्ट कोड नहीं। CapSkip की कॉल आपके test process में होती है, इसलिए सॉल्वर तक वहाँ से पहुँच होनी चाहिए जहाँ आप pytest चलाते हैं, उस मशीन से नहीं जिस पर browser होस्ट है। इस एक बात को सही सिरे से पकड़ लीजिए, और बाक़ी सब वही कोड है जो आप किसी लोकल Chrome के लिए लिखते।

आपको क्या चाहिए

  • एक ऐसा Selenium Grid जिस तक आप पहुँच सकें। Standalone हो, hub और node हों, या पूरी तरह distributed हो, client की तरफ़ से सबका बर्ताव एक जैसा है।
  • किसी Windows मशीन पर चलता CapSkip, जिस तक उस मशीन से पहुँचा जा सके जो आपके टेस्ट चलाती है।
  • Grid का पता। Selenium 4 डिफ़ॉल्ट रूप से RemoteWebDriver की requests port 4444 पर सुनता है।
  • जिस साइट का टेस्ट हो रहा है उसका sitekey और page URL।

असल में कौन सी मशीन सॉल्वर से बात करती है

कुछ भी कॉन्फ़िगर करने से पहले तीनों बॉक्स बना लीजिए, क्योंकि उनमें से दो एक जैसे दिखते हैं पर हैं नहीं।

कौन सी मशीनवहाँ क्या चलता हैक्या वह CapSkip से बात करती है?
आपका test runnerpytest, SDK की कॉल, RemoteWebDriverहाँ। सिर्फ़ यही एक करती है
Grid का hubरूटिंग और session की क़तारनहीं। वह सिर्फ़ sessions रूट करती है
browser वाला nodeअसली browserनहीं। वह सॉल्वर को कभी देखती ही नहीं

लोग इसे हैरान करने वाली नियमितता से उल्टा जोड़ देते हैं, आमतौर पर इसलिए कि Docker में Grid के nodes ही चल रहे होते हैं और उन्हें लगता है कि networking की दिक़्क़त वहीं होगी। node पर networking की कोई दिक़्क़त है ही नहीं। token वहाँ साधारण WebDriver protocol से पहुँचता है, किसी script execution command के argument के रूप में, ठीक वैसे ही जैसे कोई भी दूसरी string पहुँचती है।

इसलिए आपको कौन सा कनेक्शन मोड चाहिए, यह इस बात से निकलता है कि आपके टेस्ट कहाँ चलते हैं। CapSkip में Local है, जो 127.0.0.1 से बंधता है और सिर्फ़ उसी डिवाइस को सेवा देता है, और Server है, जो आपके network पते या public IP से बंधता है ताकि कोई दूसरा box, कोई container host या कोई CI runner उसी Windows मशीन तक API के ज़रिए पहुँच सके। दोनों यहाँ रहते हैं: कनेक्शन सेटिंग्स। Server mode सिर्फ़ यह बदलता है कि सॉल्वर कहाँ चलता है, और कुछ नहीं: यह अब भी आपका हार्डवेयर है और अब भी बिना मीटर के है।

आपके टेस्ट कहाँ चलते हैंकौन सा mode, और host का मान
आपकी अपनी Windows मशीन पर, जो किसी दूर के Grid को चला रही होLocal mode। host का मान 127.0.0.1 ही रहता है, भले ही browser कहीं और हो
सॉल्वर वाले उसी network के किसी build agent परServer mode। host का मान सॉल्वर मशीन का LAN पता होता है
आपके network के बाहर किसी होस्टेड CI runner परस्टैटिक public IP के साथ Server mode, key validation चालू, और एक firewall नियम

पहली पंक्ति ही ध्यान देने लायक़ है। जो डेवलपर किसी साझा Grid के ख़िलाफ़ अपने ही कंप्यूटर पर टेस्ट चलाता है, वह loopback पता ही रखता है, क्योंकि solve उसकी मेज़ से बाहर जाता ही नहीं।

स्टेप 1: driver को Grid की तरफ़ मोड़िए

Selenium 4 को Grid का पता और एक options object चाहिए। पुराना hub वाला path suffix अब ज़रूरी नहीं है; base URL ही काफ़ी है।

# pip install selenium capskip
from selenium import webdriver

GRID = "http://grid.internal:4444"

options = webdriver.ChromeOptions()
options.add_argument("--no-sandbox")

# command_executor is the Grid, not the browser. Everything you
# call on this driver is a request over the wire to the node.
driver = webdriver.Remote(command_executor=GRID, options=options)

कैप्चा के बारे में यहाँ कुछ नहीं बदलता। जो बदलता है वह यह है कि अब driver की हर कॉल एक राउंड ट्रिप है, और यह बात नीचे timeout वाले हिस्से में मायने रखने लगती है।

स्टेप 2: हल कीजिए, फिर token इंजेक्ट कीजिए

solve आपके test process में एक लोकल function कॉल है। injection node पर चलने वाला script execution है। दोनों को दिमाग़ में अलग-अलग रखिए और कोड ख़ुद लिख जाएगा।

# pip install capskip
import os
from capskip import CapSkip

# The host is 127.0.0.1 when the tests run on the solver machine,
# and the solver's address when they do not. It is never the node.
solver = CapSkip(
    host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
    port=8080,
)

result = solver.recaptcha(
    sitekey="YOUR_SITEKEY",
    url="https://example.com/page-with-recaptcha",
)

# This runs on the node. The token travels as a script argument.
driver.execute_script(
    "document.getElementById('g-recaptcha-response')"
    ".value = arguments[0];",
    result["code"],
)

token को script की string में जोड़ने के बजाय argument के रूप में भेजना Grid पर लोकल के मुक़ाबले और भी ज़्यादा मायने रखता है, क्योंकि script serialise होकर node तक भेजा जाता है। string जोड़ने में ही quoting की ग़लतियाँ चुपचाप ख़ाली रह गए field में बदल जाती हैं।

reCAPTCHA का हर variant वही method है, बस एक अतिरिक्त keyword के साथ: invisible को 1, enterprise को 1, या version को v3 पर सेट कीजिए और साथ में एक action का नाम दीजिए। Turnstile और GeeTest अपने अलग methods हैं, बनावट वही है, और parameters की सूची यहाँ है: CapSkip API डॉक्यूमेंटेशन.

स्टेप 3: वह session timeout जो धीमे solve को मार देता है

यह रही वह नाकामी जो ख़ास तौर पर Grid की है, और है बड़ी दिलचस्प। node किसी भी ऐसे session को मार देता है जिसमें उसके session timeout जितनी देर तक कोई हलचल न हुई हो, और वह डिफ़ॉल्ट रूप से 300 सेकंड है। जब SDK जवाब के लिए पोल कर रहा होता है, तब आपका test process driver को बिल्कुल भी कॉल नहीं कर रहा होता। browser वहाँ कुछ किए बिना बैठा रहता है, और node की नज़र से वह session छोड़ा हुआ लगता है।

reCAPTCHA v2 के checkbox पर यह कभी सामने नहीं आता, क्योंकि जवाब आमतौर पर एक मिनट से भी काफ़ी कम में आ जाता है। यह Turnstile के challenge पेजों और GeeTest पर सामने आता है, व्यस्त सॉल्वर पर, और किसी भी ऐसे रन पर जहाँ आप SDK की अपनी ऊपरी सीमा तक जा पहुँचें। वह सीमा 300 सेकंड है, जो recaptchaTimeout से तय होती है, और यह ठीक node का डिफ़ॉल्ट भी है। डिफ़ॉल्ट मानों पर आप यह मुक़ाबला जीत ही नहीं सकते। node की idle घड़ी SDK के पोल करना शुरू करने से पहले ही चल पड़ती है, इसलिए session पहले समेट दिया जाता है और driver की अगली कॉल कैप्चा से जुड़ी किसी बात के बजाय invalid session id के साथ नाकाम होती है।

तीन उपाय, उसी क्रम में जिसमें उन्हें आज़माना चाहिए।

  • node पर session timeout बढ़ाइए, उसके session timeout विकल्प से, 300 सेकंड से आराम से ऊपर। यही ईमानदार उपाय है और इसमें कुछ ख़र्च नहीं होता।
  • solve चलने के दौरान session को व्यस्त रखिए। synchronous client ब्लॉक कर देता है, इसलिए इसका मतलब है solve को किसी thread पर ले जाना या asynchronous client इस्तेमाल करना, और फिर थोड़ी-थोड़ी देर में driver की कोई सस्ती कॉल करके idle घड़ी को फिर से शून्य पर लौटाते रहना। यह चल जाता है, बस क़ीमत यह है कि चलते-फिरते हिस्से बढ़ जाते हैं।
  • SDK की अपनी ऊपरी सीमा घटा दीजिए ताकि वह पहले हार माने, और अपने टेस्ट को retry करने दीजिए। आपकी अपनी चुनी हुई सीमा से आया TimeoutException किसी रिपोर्ट में मरे हुए session के मुक़ाबले कहीं आसानी से पढ़ा जाता है।

जब आप वहाँ हैं ही तो Grid की दो और सीमाएँ जान लेना ठीक रहेगा। हर node पर अधिकतम sessions डिफ़ॉल्ट रूप से उस node के प्रोसेसरों की संख्या जितने होते हैं, और असल में यही आपकी समांतरता की हद तय करता है। और नए session की जो request क़तार में session request timeout से ज़्यादा देर पड़ी रहती है, जो डिफ़ॉल्ट रूप से 300 सेकंड ही है, वह browser शुरू होने से पहले ही ठुकरा दी जाती है।

पूरा चलने वाला उदाहरण

एक पूरा टेस्ट जो पेज खोलता है, हल करता है, इंजेक्ट करता है और submit करता है। submit जान-बूझकर injection के तुरंत बाद होता है: reCAPTCHA token लगभग दो मिनट तक चलता है, और Grid हर step के बीच राउंड ट्रिप जोड़ देता है। इस पर और बातें इस गाइड में हैं: reCAPTCHA token की समय समाप्ति.

# pip install selenium capskip
import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from capskip import CapSkip
from capskip.exceptions import NetworkException, TimeoutException

GRID = "http://grid.internal:4444"
PAGE = "https://example.com/page-with-recaptcha"

def test_login_through_recaptcha():
    options = webdriver.ChromeOptions()
    driver = webdriver.Remote(command_executor=GRID, options=options)

    try:
        driver.get(PAGE)

        # Read the sitekey off the rendered page rather than
        # hardcoding it. It is on the node, so this is a round trip.
        sitekey = driver.find_element(
            By.CSS_SELECTOR, ".g-recaptcha"
        ).get_attribute("data-sitekey")

        solver = CapSkip(host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"))
        result = solver.recaptcha(sitekey=sitekey, url=PAGE)

        driver.execute_script(
            "document.getElementById('g-recaptcha-response')"
            ".value = arguments[0];",
            result["code"],
        )
        driver.find_element(By.CSS_SELECTOR, "form").submit()

    except NetworkException:
        raise AssertionError("CapSkip unreachable from the test runner")
    except TimeoutException:
        raise AssertionError("Solve did not finish before the ceiling")
    finally:
        driver.quit()

Grid पर driver को हमेशा finally block में quit कीजिए। लोकल Chrome अगर लीक हो जाए तो वह आपकी अपनी मशीन पर एक भटकी हुई process भर है। लीक हुआ Grid session उस node के session slots में से एक को तब तक रोके रखता है जब तक timeout उसे समेट न ले, और चार प्रोसेसर वाले node पर वह तब तक आपकी एक चौथाई क्षमता का चले जाना है।

समांतर sessions पर solves चलाना

Grid का मक़सद ही एक साथ कई browsers चलाना है, और उनमें से हर session को अपना token चाहिए। सॉल्वर एक साथ कई submissions लेता है, इसलिए जो शक्ल काम करती है वह यह है कि आप अपने पूरे suite को एक blocking कॉल के पीछे क़तार में लगाने के बजाय solve को asynchronous रखें।

इसी के लिए Python SDK एक असली asynchronous client देता है, और यह साफ़ कहने लायक़ है क्योंकि Node.js, PHP और .NET SDKs में वैसी ही classes अलग implementations नहीं बल्कि aliases हैं। batching का तरीक़ा यहाँ लिखा गया है: Python के साथ समानांतर में कैप्चा हल करने की गाइड, और जब browsers दूर की मशीनों पर हों तब भी वह बिना किसी बदलाव के लागू होता है।

इनमें से ज़्यादा चलाने पर कोई अतिरिक्त ख़र्च नहीं होता। जिसकी हद है वह Grid है: हर node पर अधिकतम sessions, और उसके आगे लगी क़तार का timeout। टेस्ट की समांतरता Grid के हिसाब से तय कीजिए, सॉल्वर के हिसाब से नहीं।

आम errors और उनका मतलब

आप जो देखते हैंकारणफिक्स
NetworkException, port 8080 पर connection refusedसॉल्वर Local mode में है और टेस्ट किसी दूसरी मशीन पर चलते हैंServer mode पर स्विच करें और host को सॉल्वर के पते पर सेट करें
आपने node को सॉल्वर तक पहुँचने के लिए कॉन्फ़िगर किया और कुछ नहीं बदलाnode कभी CapSkip को कॉल करता ही नहीं। आपका test process करता हैइसके बजाय host के मान को test runner से सॉल्वर की तरफ़ मोड़ें
किसी लंबे solve के ठीक बाद invalid session idSDK के पोल करते समय node ने ख़ाली पड़े session को समेट दियाnode का session timeout 300 सेकंड से ऊपर बढ़ाएँ
browser शुरू होने से पहले ही नए session की request timeout हो जाती हैक़तार भरी हुई है और request बहुत पुरानी होकर बाहर हो गईऔर nodes जोड़ें, या session request timeout बढ़ाएँ
script चलने के बाद response वाला field ख़ाली हैtoken को script की string में जोड़ दिया गया था और quoting टूट गईउसे script के argument के रूप में भेजें, जैसा ऊपर के नमूनों में है
साइट किसी वैध token को ठुकरा देती हैवह solve और submit के बीच expire हो गयाअगले ही statement में submit करें, बीच में कोई इंतज़ार न रखें
submit पर ERROR_GOOGLEKEYपेज से पढ़ा गया sitekey ख़ाली था या ग़लत element से आया थाselector जाँचें, और यह भी कि पढ़ने से पहले widget रेंडर हो चुका था
एक ही node पर sessions जमा होते जाते हैंdriver को quit किए बिना ही कोई टेस्ट क्रैश कर गयाfinally block में quit करें, जैसा पूरे उदाहरण में है

FAQ

क्या मुझे Grid के nodes पर कुछ install करना पड़ेगा?

नहीं। nodes सिर्फ़ browsers चलाते हैं और कुछ नहीं। SDK आपके टेस्ट प्रोजेक्ट की dependency है, solve आपके test process में होता है, और node तक सिर्फ़ token पहुँचता है, किसी script execution के argument के रूप में। इसीलिए Linux वाला Docker node ऐसे सॉल्वर के साथ भी ठीक चलता है जो सिर्फ़ Windows पर चलता है: दोनों आपस में बात करते ही नहीं।

मेरे टेस्ट CI में चलते हैं। क्या खोलना पड़ेगा?

सॉल्वर का port, उस मशीन के लिए जो टेस्ट चलाती है। अगर runner होस्टेड है तो इसका मतलब है स्टैटिक public IP के साथ Server mode, साथ में API key validation चालू और पूरे इंटरनेट से कहीं सँकरा एक firewall नियम। अगर आपके CI runners आपके अपने network पर सेल्फ-होस्टेड हैं, तो LAN पता ही काफ़ी है और कुछ भी बाहर जाने की ज़रूरत नहीं। दोनों ही हालात में Grid पर कोई असर नहीं पड़ता, क्योंकि वह इस रास्ते का हिस्सा ही नहीं है।

Turnstile के solve के बीच में ही मेरा session क्यों मर गया?

क्योंकि node को उसके session timeout जितनी देर तक कोई हलचल नहीं दिखी और उसने सफ़ाई कर दी। डिफ़ॉल्ट 300 सेकंड है और Turnstile के लिए SDK की अपनी ऊपरी सीमा भी 300 सेकंड ही है, और node की idle घड़ी पहले ही चल पड़ती है, इसलिए इन डिफ़ॉल्ट मानों पर node हमेशा SDK के हार मानने से पहले ही session समेट देता है। node का session timeout बढ़ाइए, और SDK की सीमा घटाने पर भी सोचिए ताकि नाकामी मरे हुए session के बजाय एक साफ़ exception के रूप में लौटे।

क्या यह किसी लोकल driver से हल करने से अलग है?

सिर्फ़ इस मायने में कि चीज़ें कहाँ चलती हैं। solve की कॉल और injection एक जैसे ही हैं, और बात ही यही है। अंतर संचालन के हैं: node का idle session timeout, driver की हर कॉल पर राउंड ट्रिप, और यह तथ्य कि सॉल्वर का पता आपके test runner की जगह से तय होता है। एक ही browser वाले रूप यहाँ बताए गए हैं: SeleniumBase गाइड और undetected-chromedriver गाइड.

संक्षेप में

Grid पर browser जगह बदलता है और आपका कोड नहीं, इसलिए सॉल्वर का पता इस बात से तय होता है कि आपके टेस्ट कहाँ चलते हैं। RemoteWebDriver को port 4444 की तरफ़ मोड़िए, SDK को test process से कॉल कीजिए, और token को string बनाने के बजाय script के argument के रूप में node तक भेजिए। node का session timeout 300 सेकंड से ऊपर बढ़ाइए ताकि कोई धीमा solve अपना session न गँवा दे, इंजेक्ट करने के तुरंत बाद submit कीजिए, और driver को हमेशा finally block में quit कीजिए।

suite को बड़ा करने से पहले यह जान लेना ठीक रहेगा: CapSkip एक कैप्चा सॉल्वर है, जो उसी हार्डवेयर पर चलता है जो पहले से आपका है, इसलिए सौ समांतर sessions और एक session, दोनों की लागत बिल्कुल एक जैसी है।