एक इनलाइन रिक्वेस्ट से Scrapy में ALTCHA कैसे हल करें

solve altcha in scrapy - How to Solve ALTCHA in Scrapy With One Inline Request

Scrapy में ALTCHA हल करने के लिए, altcha-widget element से challenge का पता पढ़ें, उसे उसी callback से एक इनलाइन रिक्वेस्ट के ज़रिए fetch करें, JSON को AsyncCapSkip के altcha method को सौंपें, और फॉर्म को token के साथ सबमिट करें, उस फ़ील्ड में जिसका नाम widget बताता है, जो डिफ़ॉल्ट रूप से altcha है। ALTCHA तस्वीर नहीं, proof of work है, इसलिए इनमें से किसी भी काम के लिए ब्राउज़र की ज़रूरत नहीं, और एक सादा Scrapy spider काफ़ी है। Scrapy से जुड़ा इकलौता जाल आसानी से छूट जाता है: challenge endpoint हर बार उसी URL पर एक नया document लौटाता है, और Scrapy का डुप्लिकेट फ़िल्टर उस पर जाने वाली दूसरी रिक्वेस्ट को चुपचाप हटा देता है। यह गाइड widget, fetch, solve और सबमिट कवर करती है, साथ में एक spider भी, जिसे आप चला सकते हैं।

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

  • Scrapy 2.14 या उसके बाद का वर्ज़न, नीचे इस्तेमाल हुए इनलाइन रिक्वेस्ट method के लिए। 2.13 से Scrapy डिफ़ॉल्ट रूप से asyncio reactor पर चलता है, और इसी वजह से कोई callback async client को await कर पाता है। पुराने वर्ज़न की बात आख़िर के पास एक सेक्शन में है।
  • capskip Python पैकेज का 1.2.0 या उसके बाद का वर्ज़न, यही वह रिलीज़ है जिसमें altcha method जोड़ा गया, और किसी Windows मशीन पर चल रहा CapSkip 1.3.0 या उसके बाद का संस्करण।
  • form2request पैकेज, जिसकी सिफ़ारिश Scrapy अब फॉर्म सबमिशन बनाने के लिए करता है।
  • फॉर्म वाला पेज। बाकी सब कुछ, challenge समेत, उसी पेज से मिलता है, और कहाँ से, यह स्टेप 1 दिखाता है।
  • सॉल्वर के लिए एक पता। Local मोड केवल उसी डिवाइस के लिए 127.0.0.1 पर जवाब देता है; Server मोड आपके नेटवर्क पते या पब्लिक IP पर सुनता है ताकि किसी दूसरी मशीन पर चल रहा crawler, API के ज़रिए उसे कॉल कर सके। दोनों यहाँ मिलते हैं: कनेक्शन सेटिंग्स, और कब स्विच करना है, यह नीचे के एक सेक्शन में बताया गया है।
# pip install scrapy capskip form2request
pip install scrapy capskip form2request

स्टेप 1: widget से challenge पढ़ें

ALTCHA में कोई sitekey नहीं होती। Scrapy में ALTCHA हल करने के लिए आपको challenge चाहिए, या वह पता जहाँ से वह आता है, और widget element में इनमें से कोई एक होता है। वह किस attribute में है, यह widget की पीढ़ी पर निर्भर करता है, इसलिए तीनों पढ़ें।

विजेटएट्रिब्यूटइसमें क्या होता है
v1 और v2challengeurl, या challengejsonchallengeurl उस endpoint का नाम बताता है जो challenge देता है, अक्सर एक relative path; challengejson में खुद challenge document होता है
v3 और उसके बादchallengeया तो वही endpoint, या खुद challenge document
# Inside a spider callback; response is the page with the form.
widget = response.css("altcha-widget")
source = (widget.attrib.get("challenge")
          or widget.attrib.get("challengejson")
          or widget.attrib.get("challengeurl"))

# The token goes in a field named by the widget, altcha by default.
field = widget.attrib.get("name", "altcha")

# A v3 challenge or a v1/v2 challengejson carries the document inline.
inline = source.lstrip().startswith("{")

name attribute भी नोट करें। widget अपना payload उसी नाम वाले एक hidden input में लिखता है, और यह नाम डिफ़ॉल्ट रूप से altcha होता है, लेकिन साइट इसे बदल सकती है। इसे पढ़ने में बस एक लाइन लगती है, और इससे ऐसे सबमिट से बचाव होता है जो चुपचाप असली फ़ील्ड को खाली छोड़ दे। कुछ डिप्लॉयमेंट challenge को किसी endpoint से सर्व करने के बजाय पेज की अपनी script में बनाते हैं, और तब पढ़ने को कोई attribute होता ही नहीं। आपके पास कौन सी किस्म है, यह Network टैब दिखाता है।

स्टेप 2: callback के भीतर challenge fetch करें

अगर widget किसी endpoint की ओर इशारा करता है, तो उसे उसी callback से fetch करें जिसमें आप पहले से हैं, engine के download_async method से, जिसे Scrapy का डॉक्यूमेंटेशन इनलाइन रिक्वेस्ट्स शीर्षक के अंतर्गत बताता है। यह रिक्वेस्ट downloader middlewares से होकर जाती है, इसलिए spider का cookie jar, user agent और प्रॉक्सी सेटिंग्स उस पर भी लागू होती हैं, जैसे पेज पर हुई थीं। यह scheduler से होकर नहीं जाती, और असली बात यही है।

# The page's jar and proxy; no scheduler, no dupefilter.
meta = {"dont_cache": True, "allow_offsite": True}
for key in ("cookiejar", "proxy"):
    if key in response.meta:
        meta[key] = response.meta[key]
reply = await self.crawler.engine.download_async(
    scrapy.Request(response.urljoin(source), meta=meta,
                   headers={"Referer": response.url})
)
if reply.status != 200:
    raise ValueError(f"challenge endpoint answered {reply.status}")
challenge_json = reply.text

scheduler को छोड़ना क्यों मायने रखता है, इसकी वजह यह है। सीधा विकल्प, यानी challenge के लिए अपने अलग callback वाली एक Request yield करना, पहले फॉर्म के लिए काम करता है और दूसरे को खो देता है। endpoint का हर fetch एक नया challenge लौटाता है, लेकिन URL कभी नहीं बदलता, इसलिए Scrapy का डुप्लिकेट फ़िल्टर दूसरी रिक्वेस्ट को पहले ही की जा चुकी रिक्वेस्ट मानकर हटा देता है। कोई error नहीं आता। फ़िल्टर अपने पहले discard को debug लेवल पर एक बार लॉग करता है, crawl stats में एक counter बढ़ा देता है, और उसके पीछे वाला फॉर्म बस कभी सबमिट ही नहीं होता। इनलाइन रिक्वेस्ट कभी फ़िल्टर तक पहुँचती ही नहीं।

उस ब्लॉक का बाकी हिस्सा उन बारीकियों को सँभालता है जिन्हें आमतौर पर scheduler आपके लिए सँभालता। हर रिक्वेस्ट की state अपने आप आगे नहीं जाती, इसलिए अगर पेज cookiejar या proxy key के साथ fetch हुआ था, तो loop वह key पेज के meta से कॉपी करता है। Referer header हाथ से सेट किया जाता है, क्योंकि उसे जोड़ने वाला middleware spider की तरफ़ चलता है। अगर challenge endpoint किसी दूसरे डोमेन पर है, उदाहरण के लिए कोई होस्टेड ALTCHA सर्विस, और spider में allowed_domains सेट है, तो offsite फ़िल्टर fetch को रोक देगा, और allow_offsite इस एक रिक्वेस्ट को जाने देता है। status की जाँच इसलिए है क्योंकि यहाँ endpoint से आया 403 एक सामान्य response की तरह लौटता है: error status को नाकामी में बदलने वाला middleware spider की तरफ़ चलता है, जिसे इनलाइन रिक्वेस्ट छोड़ देती है। और अगर आपने Scrapy का HTTP cache चालू रखा है, तो dont_cache उसे challenge से दूर रखता है। cache किया गया challenge, expire हो चुका challenge होता है, और CapSkip ऐसे token पर CPU ख़र्च करने के बजाय, जिसे साइट ठुकरा देगी, expire हो चुके इनलाइन challenge को तुरंत मना कर देता है। इस flag के बिना, दूसरा रन उस endpoint को साझा करने वाले हर पेज को वही सहेजा हुआ challenge थमा देता है। यही तर्क फॉर्म वाले पेज पर भी लागू होता है। cache किया गया पेज पुराना anti-forgery token और session cookie दोहराता है, और इनलाइन challenge हो तो पुराना challenge भी, इसलिए cache चालू हो तो फॉर्म वाले पेजों पर भी dont_cache लगाएँ, या इस spider के लिए cache बंद ही रखें।

इसके बजाय आप endpoint को challenge_url के रूप में पास करके CapSkip को challenge खुद fetch करने दे सकते हैं। सार्वजनिक endpoint के लिए यह ठीक है। लेकिन CapSkip के fetch में आपके spider की कोई cookie नहीं जाती, और कुछ साइटें challenge सिर्फ़ उसी session को देती हैं जिसने फॉर्म लोड किया था। Scrapy के ज़रिए fetch करने से शुरू से आख़िर तक एक ही session बना रहता है।

स्टेप 3: crawl को रोके बिना इसे हल करें

AsyncCapSkip इस्तेमाल करें और callback को async def बनाएँ। Python पैकेज में यह class कोई alias नहीं, बल्कि असली asyncio client है, इसलिए इसे await करने पर solve चलते समय नियंत्रण Scrapy के पास लौट जाता है और बाकी हर रिक्वेस्ट चलती रहती है। synchronous CapSkip class हर solve की पूरी अवधि तक पूरे crawl को रोक देती, और यह समस्या यहाँ विस्तार से समझाई गई है: Scrapy कैप्चा middleware गाइड.

from capskip import AsyncCapSkip

solver = AsyncCapSkip(host="127.0.0.1", port=8080)

# Pass the document exactly as the endpoint sent it.
result = await solver.altcha(url=response.url, challenge_json=challenge_json)

token = result["token"]   # base64 payload for the form field

क्योंकि challenge इनलाइन जाता है, इसलिए CapSkip कोई नेटवर्क रिक्वेस्ट करता ही नहीं। वह तब तक hash करता है जब तक जवाब न मिल जाए, जिसमें आमतौर पर कुछ milliseconds लगते हैं, और client तुरंत poll करता है और फिर चौथाई सेकंड बाद दोबारा, इसलिए एक सामान्य solve लगभग चौथाई सेकंड में लौट आता है। कॉल defaultTimeout तक, यानी 120 सेकंड तक, इंतज़ार करती है, क्योंकि यह ब्राउज़र session नहीं, CPU का काम है। दो challenge algorithm, Argon2id और scrypt, आज़माए जाने के बजाय मना कर दिए जाते हैं, और यह इनकार एक सेकंड से कम में ApiException के रूप में आता है। ज़्यादा आम PBKDF2 और SHA स्कीमें, ALTCHA के सुझाए डिफ़ॉल्ट समेत, कवर हैं।

reply का text जैसा आया वैसा ही पास करें; उसे parse करने की ज़रूरत नहीं। जो नहीं बदलना चाहिए, वह है token: यह एक JSON document का base64 है जिसके फ़ील्ड पर साइट के सर्वर ने हस्ताक्षर किए हैं, इसलिए काटा गया, decode किया गया या दोबारा encode किया गया token, verification में फेल हो जाता है।

स्टेप 4: token के साथ फॉर्म सबमिट करें

widget जिस hidden input को भरता है, वह Scrapy के डाउनलोड किए HTML में मौजूद ही नहीं होता। widget उसे ब्राउज़र में, अपने चलने के बाद बनाता है, इसलिए सर्व किया गया HTML पढ़ने वाली कोई भी चीज़ उसे नहीं ढूँढ सकती। उसे खुद जोड़ें, स्टेप 1 वाले नाम के साथ। उदाहरण सबमिट को form2request से बनाता है, जिसकी सिफ़ारिश Scrapy, FormRequest.from_response की जगह करता है: Scrapy 2.16 से वह पुराना method हर कॉल पर एक deprecation warning लॉग करता है।

from form2request import form2request

form = response.xpath("//form[.//altcha-widget]")
data = {"email": "[email protected]", field: token}
yield form2request(form, data).to_scrapy(
    callback=self.after_submit,
    priority=10,
    meta={"handle_httpstatus_all": True},
)

form2request वह सब रखता है जो पेज ने फॉर्म में पहले से डाला है, जैसे anti-forgery token, और पहला submit बटन दबाता है, इसलिए रिक्वेस्ट वैसी ही दिखती है जैसी कोई ब्राउज़र भेजता। XPath वह फॉर्म चुनता है जिसमें widget है, जो उन पेजों पर मायने रखता है जिनमें एक सर्च बॉक्स भी होता है। handle_httpstatus_all से callback ठुकराया गया सबमिट देख पाता है, जिसे वरना Scrapy callback तक पहुँचने से पहले ही हटा देता। बढ़ी हुई priority सबमिट को scheduler में अब भी इंतज़ार कर रही हर चीज़ से आगे भेज देती है, क्योंकि challenges expire होते हैं। कुछ समय-सीमाएँ मुश्किल से दो मिनट की होती हैं, और लंबे crawl के पीछे इंतज़ार करता token भेजे जाने से पहले ही expire हो सकता है।

हर token को सिर्फ़ एक सबमिट के लायक मानें। अगर साइट फॉर्म ठुकरा दे, तो वही रिक्वेस्ट दोबारा भेजने के बजाय नए challenge के साथ स्टेप 2 से फिर शुरू करें। कुछ इंटीग्रेशन payload को फॉर्म फ़ील्ड के बजाय JSON बॉडी या cookie में भेजते हैं, इसलिए DevTools खोलकर फॉर्म को एक बार हाथ से सबमिट करें और पेज जो भेजता है, वही दोहराएँ।

solver को कहीं और चलाना

सैंपल 127.0.0.1 इस्तेमाल करते हैं, क्योंकि जब तक spider और CapSkip एक ही मशीन पर हों, यही सही है। किसी दूसरी मशीन पर Scrapyd में, किसी VPS पर या किसी होस्टेड Scrapy प्लेटफ़ॉर्म पर डिप्लॉय किए गए crawler के लिए loopback उसकी अपनी ही मशीन की ओर इशारा करता है, और पहला ही solve एक NetworkException उठाता है। CapSkip को Server मोड पर कर दें और वह आपके नेटवर्क पते या पब्लिक IP पर सुनने लगता है, ताकि spider आपकी अनुमति वाली किसी भी जगह से API के ज़रिए उस तक पहुँच सके। अगर रास्ता इंटरनेट से होकर जाता है तो स्टैटिक पब्लिक IP इस्तेमाल करें, API key validation चालू करें, और Windows Firewall नियम से पोर्ट को सिर्फ़ अपेक्षित पतों तक सीमित रखें। यह अब भी आपकी अपनी Windows मशीन है, और हल करना अब भी बिना मीटर वाला है।

क्लाइंट एनवायरनमेंट वेरिएबल खुद नहीं पढ़ता। CAPSKIP_HOST और CAPSKIP_API_KEY को spider में पढ़ें और उन्हें constructor को पास करें, जैसा पूरा उदाहरण करता है, ताकि वही कोड आपकी डेस्क पर भी चले और सर्वर पर भी।

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

# pip install scrapy capskip form2request
import os

import scrapy
from capskip import AsyncCapSkip, CapSkipError
from form2request import form2request


class SignupSpider(scrapy.Spider):
    name = "signup"
    start_urls = ["https://example.com/signup"]

    solver = AsyncCapSkip(
        apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
        host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
        port=8080,
    )

    async def parse(self, response):
        widget = response.css("altcha-widget")
        source = (widget.attrib.get("challenge")
                  or widget.attrib.get("challengejson")
                  or widget.attrib.get("challengeurl"))
        if not source:
            self.logger.warning("no ALTCHA challenge on %s", response.url)
            return
        field = widget.attrib.get("name", "altcha")

        if source.lstrip().startswith("{"):
            challenge_json = source
        else:
            # Inline request: the page's jar and proxy, no dupefilter.
            meta = {"dont_cache": True, "allow_offsite": True}
            for key in ("cookiejar", "proxy"):
                if key in response.meta:
                    meta[key] = response.meta[key]
            reply = await self.crawler.engine.download_async(
                scrapy.Request(response.urljoin(source), meta=meta,
                               headers={"Referer": response.url})
            )
            if reply.status != 200:
                self.logger.error("challenge endpoint answered %s", reply.status)
                return
            challenge_json = reply.text

        try:
            result = await self.solver.altcha(
                url=response.url, challenge_json=challenge_json)
        except CapSkipError as exc:
            # Expired challenge, unsupported algorithm, or CapSkip unreachable.
            self.logger.error("ALTCHA not solved on %s: %r", response.url, exc)
            return

        form = response.xpath("//form[.//altcha-widget]")
        data = {"email": "[email protected]", field: result["token"]}
        yield form2request(form, data).to_scrapy(
            callback=self.after_submit,
            priority=10,
            meta={"handle_httpstatus_all": True},
        )

    def after_submit(self, response):
        yield {"url": response.url, "status": response.status}

इसे scrapy runspider और फ़ाइल के नाम के साथ चलाएँ, या class को किसी प्रोजेक्ट में डाल दें। spider, widget की दोनों पीढ़ियाँ सँभालता है, challenge को पेज के session के भीतर fetch करता है, और हर सबमिट किए गए फॉर्म के लिए एक item yield करता है, जिसमें साइट का लौटाया status होता है, ठुकराए गए फॉर्म समेत। क्योंकि challenge fetch, scheduler से होकर नहीं जाता, इसलिए एक ही challenge endpoint साझा करने वाले पेजों में से हर एक को अपना अलग challenge मिलता है। altcha method के पीछे का raw endpoint यहाँ दर्ज है: API रेफ़रेंस, और Scrapy के बिना सादा Python कॉल यहाँ कवर की गई है: Python ALTCHA गाइड.

2.14 से पुराने Scrapy पर

2.14 से पहले download_async नहीं है, लेकिन Scrapy 2.6 से वही इनलाइन fetch, engine.download के ज़रिए काम करता है, जिसे एक helper के साथ await किया जाता है जो उसके नतीजे को ऐसी चीज़ में बदल देता है जिसका कोई coroutine इंतज़ार कर सके।

from scrapy.utils.defer import maybe_deferred_to_future

# Scrapy 2.6 to 2.13: the same inline fetch, through engine.download.
reply = await maybe_deferred_to_future(self.crawler.engine.download(
    scrapy.Request(response.urljoin(source),
                   meta={"dont_cache": True, "allow_offsite": True})
))

उन वर्ज़न पर FormRequest.from_response बिना warning के सबमिट बनाता है, इसलिए आप form2request की जगह उसका इस्तेमाल कर सकते हैं: वही XPath, formxpath के रूप में पास करें और token को formdata में रखें। अगर आप इसके बजाय challenge को scheduler से होकर भेजते हैं, अपने अलग callback वाली एक सामान्य रिक्वेस्ट के रूप में, तो उस पर dont_filter सेट करें, वरना डुप्लिकेट फ़िल्टर पहले के बाद हर fetch हटा देता है।

2.13 से पुराने Scrapy पर, AsyncCapSkip के लिए settings.py में TWISTED_REACTOR को asyncio reactor पर सेट करना भी ज़रूरी है। Scrapy 2.7 से बनाए गए प्रोजेक्ट में यह लाइन पहले से होती है।

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

आप जो देखते हैंकारणफिक्स
पहला फॉर्म सबमिट होता है और बाद वाले कभी नहीं होते, कोई error भी नहीं आताchallenge को सामान्य रिक्वेस्ट के रूप में yield किया गया और डुप्लिकेट फ़िल्टर ने दोहराई गई रिक्वेस्ट हटा दींउसे download_async से fetch करें, या रिक्वेस्ट पर dont_filter सेट करें
पल भर में ApiException, लेकिन सिर्फ़ पहले रन के बादHTTP cache ने एक पुराना, expire हो चुका challenge दोहरा दियाchallenge रिक्वेस्ट में dont_cache जोड़ें
solve कामयाब होने के बावजूद साइट कहती है कि verification फेल हुआtoken ने बहुत देर इंतज़ार किया, दोबारा इस्तेमाल हुआ, या गलत फ़ील्ड में गयाबढ़ी हुई priority के साथ तुरंत सबमिट करें, हर token एक ही बार, और widget के name attribute वाले नाम के तहत
challenge endpoint से 403endpoint को वही session चाहिए जिसने फॉर्म लोड कियाइसे ऊपर की तरह Scrapy के ज़रिए fetch करें, challenge_url के ज़रिए नहीं
हर बार, एक सेकंड से कम में ApiExceptionसाइट Argon2id या scrypt इस्तेमाल करती है, जिन्हें आज़माने के बजाय मना कर दिया जाता हैदोबारा कोशिश करने जैसा कुछ नहीं; उस साइट के लिए कोई दूसरा रास्ता चाहिए
हर फॉर्म हल होते समय crawl रुक जाता हैsynchronous CapSkip class, event loop पर चल रही हैAsyncCapSkip और async def callback पर जाएँ
NoEventLoopError, not currently running on any asynchronous event loop (पुराने इंस्टॉलेशन पर AsyncLibraryNotFoundError)प्रोजेक्ट asyncio वाले के अलावा कोई दूसरा reactor तय करता हैवह TWISTED_REACTOR लाइन हटा दें, या उसे asyncio reactor पर सेट करें
AttributeError: ‘ExecutionEngine’ object has no attribute ‘download_async’Scrapy, 2.14 से पुराना हैअपग्रेड करें, या ऊपर दिखाए अनुसार engine.download इस्तेमाल करें
challenge fetch पर IgnoreRequest, filtered offsite requestchallenge endpoint किसी दूसरे डोमेन पर है और spider में allowed_domains सेट हैchallenge रिक्वेस्ट के meta में allow_offsite जोड़ें
हर सबमिट पर from_response के बारे में एक ScrapyDeprecationWarningScrapy 2.16 और उसके बाद के वर्ज़न FormRequest.from_response को deprecate करते हैंसबमिट को form2request से बनाएँ
पहले solve पर एक NetworkExceptionCapSkip चल नहीं रहा, या host और port गलत हैंCapSkip शुरू करें, फिर देखें कि उसे Local मोड में होना चाहिए या Server मोड में

FAQ

क्या ALTCHA के लिए मुझे scrapy-playwright या headless ब्राउज़र चाहिए?

नहीं, आप Scrapy में ALTCHA को सादी रिक्वेस्ट से हल कर सकते हैं। widget का पूरा काम बस इतना है: एक challenge fetch करना, एक संख्या खोजने में थोड़ा CPU ख़र्च करना, और नतीजा एक फ़ील्ड में लिखना। Scrapy challenge fetch कर सकता है, CapSkip संख्या खोजता है, और form2request फ़ील्ड लिखता है, इसलिए कहीं भी कोई JavaScript चलाने की ज़रूरत नहीं। इससे spider उतना ही तेज़ और चलाने में उतना ही सस्ता रहता है जितना कोई भी दूसरा Scrapy crawl।

क्या Scrapyd या किसी होस्टेड Scrapy प्लेटफ़ॉर्म पर चल रहा spider सॉल्वर तक पहुँच सकता है?

हाँ। कनेक्शन सेटिंग्स में CapSkip को Server मोड पर कर दें ताकि वह loopback के बजाय किसी नेटवर्क पते पर सुने, spider के एनवायरनमेंट में CAPSKIP_HOST सेट करें, और उसे क्लाइंट को पास करें, जैसा पूरा उदाहरण करता है। होस्टेड प्लेटफ़ॉर्म उसी HTTP API से जुड़ता है जिससे लोकल spider जुड़ता है। जब रास्ता इंटरनेट से होकर जाए, तो फ़ायरवॉल नियम के साथ स्टैटिक पब्लिक IP इस्तेमाल करें। सॉल्वर आपके अपने हार्डवेयर पर ही रहता है, इसलिए हल की गिनती से आपका ख़र्च कभी नहीं बदलता।

यह downloader middleware में नहीं, callback में क्यों है?

क्योंकि ALTCHA ऐसे फॉर्म पर होता है जिसे आप खुद सबमिट करना चुनते हैं, किसी block पेज पर नहीं जो crawl को बीच में रोक दे। middleware ऐसे challenge के लिए सही जगह है जो किसी भी response पर आ सकता है, और reCAPTCHA middleware गाइड उसे इसी तरह सँभालती है। ALTCHA, flow के एक स्टेप का हिस्सा है, और उस फॉर्म को बनाने वाले callback के पास पहले से वह सब है जिसकी solve को ज़रूरत है।

क्या मैं कई फॉर्म parallel में हल कर सकता हूँ?

हाँ, और Scrapy यह आपके लिए पहले से करता है। सॉल्वर को await करने वाला हर async callback नियंत्रण छोड़ देता है, इसलिए Scrapy के डाउनलोड किए हर पेज का solve एक ही समय पर चल सकता है। CONCURRENT_REQUESTS इस पर सीमा नहीं लगाता, क्योंकि वह downloads को सीमित करता है, callbacks को नहीं। CapSkip की तरफ़, ALTCHA सेटिंग्स में Max. Threads तय करता है कि एक साथ कितने challenges hash हों, और चूँकि यह CPU का काम है, cores से ज़्यादा threads रखने से कुछ हासिल नहीं होता। क्योंकि हर callback हल करने से ठीक पहले अपना challenge fetch करता है, इसलिए उनमें से कोई भी queue में पड़े-पड़े बासी नहीं होता। इसी वजह से, बाद में हल करने के लिए challenges का बैच पहले से fetch न करें।

संक्षेप में

Scrapy में ALTCHA हल करने के लिए, altcha-widget element से challenge, challengejson या challengeurl attribute और name attribute पढ़ें। challenge को download_async और dont_cache के साथ fetch करें, ताकि वह spider के session में ही जाए और डुप्लिकेट फ़िल्टर से कभी न टकराए। AsyncCapSkip के altcha method को JSON जैसा आया वैसा ही देकर await करें, और form2request वाला सबमिट yield करें, जिसके data में token हो और priority बढ़ी हुई हो। एक challenge, एक token, एक सबमिट, और जब spider कहीं और चले तो Server मोड।

volume के बारे में एक आख़िरी बात। हज़ारों फॉर्म सबमिट करने वाला crawler हज़ारों challenges हल करता है, और अपनी ही मशीन पर चल रहे कैप्चा सॉल्वर के साथ इसकी कीमत बस CPU time है, कोई प्रति-हल शुल्क नहीं।