Python स्क्रैपर में कैप्चा और ब्लॉक का फ़र्क कैसे पहचानें

captcha or block - How to Tell CAPTCHA or Block Apart in Python Scrapers

जब Python स्क्रैपर को कोई खराब response मिले, तो पहला सवाल यही होता है कि यह कैप्चा है या ब्लॉक, क्योंकि जवाब तय करता है कि आगे क्या करना है। कैप्चा हल किया जा सकता है। रेट लिमिट के लिए इंतज़ार चाहिए। ब्लॉक पेज का मतलब है कि आपका पता या fingerprint ठुकरा दिया गया है, और कोई टोकन इसे नहीं बदलेगा। यह गाइड करीब 40 लाइन का एक classifier बनाती है जो status, headers और markup पढ़कर छह लेबल में से एक लौटाता है, फिर एक fetch loop बनाती है जो CapSkip को सिर्फ़ उस लेबल पर कॉल करता है जहाँ हल से मदद मिल सकती है। CapSkip आपके अपने Windows PC पर चलता है और प्रति-हल कोई शुल्क नहीं लगता, इसलिए जो हल काम नहीं कर सकता उसे छोड़ने से पैसे नहीं, समय बचता है।

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

  • किसी Windows PC पर चल रहा CapSkip, जिसमें reCAPTCHA सॉल्वर चालू हो। पहली बार शुरू करने के चरण सेटअप गाइड में समझाए गए हैं।
  • Python 3.10 या बाद का। नीचे का कोड Python 3.14.0 पर, requests 2.33.0 और capskip 1.3.0 के साथ चलाया गया।
  • एक PowerShell prompt। कमांड PowerShell syntax में लिखी गई हैं।
  • ऐसे पेज जो आपके अपने हों या जिन्हें ऑटोमेट करने की आपको अनुमति हो।
  • सॉल्वर के लिए एक पता। Local मोड में CapSkip केवल उसी डिवाइस के लिए 127.0.0.1 पर जवाब देता है। Server मोड में वह आपके नेटवर्क पते या पब्लिक IP पर सुनता है, ताकि किसी दूसरे PC, VPS या होस्टेड प्लेटफ़ॉर्म पर चल रहा स्क्रैपर API के ज़रिए उसे कॉल कर सके। दोनों यहाँ सेट किए जाते हैं: कनेक्शन सेटिंग्स.

यह कैसे टेस्ट किया गया: classifier और fetch loop ने Python 3.14.0 पर 17 जाँचें पास कीं, हर एक एक ऐसा response थी जिसे लोकल fixture साइट ने परोसा। हल असली capskip पैकेज से एक mock CapSkip तक गए, ऐप तक नहीं। Cloudflare की पूरे पेज वाली जाँच के पीछे के दो सार्वजनिक पेज भी 29 अक्टूबर 2026 को लाए गए, और दोनों ने cf-mitigated header के साथ 403 लौटाया। नीचे कोई भी संख्या हल का समय नहीं है।

कैप्चा या ब्लॉक पहला सवाल क्यों है

दोनों गलतियों की कीमत अलग-अलग है। ब्लॉक पेज को सॉल्वर के पास भेजने से हल बेकार जाता है और आप ब्लॉक ही रहते हैं। हर बार कैप्चा आने पर नए प्रॉक्सी पर rotate करने से ऐसे पते खर्च होते हैं जिन्हें खर्च करने की ज़रूरत नहीं थी। जो पेज कैप्चा परोसता है वह आमतौर पर आपसे कुछ साबित करने को कह रहा होता है, और हल किया हुआ टोकन आपको अंदर जाने देता है। जो पेज ब्लॉक परोसता है वह आपको सीधे मना कर रहा होता है। HTTP 429 गाइड एक status code के लिए इसका इंतज़ार वाला पक्ष कवर करती है। यह गाइड उस फ़ैसले को कवर करती है जो उससे पहले आता है।

छह लेबल इसे पूरा ढक लेते हैं: ok, rate_limited, captcha, cloudflare_challenge, blocked और unknown। unknown लेबल मायने रखता है। जिस classifier को बाकी पाँच में से ही चुनना पड़े, वह कभी-कभी गलत चुनेगा, और unknown पर exception उठाने वाला loop आपको वह मामला दिखा देता है जिसे आपने अभी संभाला नहीं है।

चरण 1: इंस्टॉल करें

एक खाली फ़ोल्डर में virtual environment बनाएँ और दोनों पैकेज इंस्टॉल करें। कमांड एनवायरनमेंट के अपने Python को पाथ से बुलाती हैं, इसलिए उसे activate करने की ज़रूरत नहीं।

py -m venv .venv
.\.venv\Scripts\python.exe -m pip install capskip requests

चरण 2: संकेत

इस ब्लॉक और अगले को classify.py के रूप में सेव करें। पहले ब्लॉक में patterns हैं। विजेट वाला pattern ऐसा tag खोजता है जिसकी class attribute किसी विजेट का नाम लेती हो, या altcha-widget tag। escaped कोड के अंदर markup दिखाने वाली ब्लॉग पोस्ट, ऐसा लिंक जिसके पते में विजेट का नाम हो, और छिपा हुआ g-recaptcha-response input, इन सब पर मैच नहीं होता।

आख़िरी लाइन में वे phrases हैं जो soft block पहचानते हैं, यानी ऐसा पेज जो सामान्य status लौटाता है पर कहता है कि आपको ठुकराया गया है। इसमें सावधान रहें। Cloudflare के error दस्तावेज़ में Error 1020: Access denied दिया है, जहाँ से पहले दो phrases आते हैं। बाकी दो असली ब्लॉक पेजों से लगाए अनुमान हैं, और आपके टारगेट के अपने शब्द होंगे। कुछ असली पेज देखने के बाद इन्हें बदल दें।

import re

# A tag whose class attribute names a widget, or an altcha-widget tag. A link, a hidden
# input or a sentence that merely contains the name does not match.
CLS = r"<[^>]*\bclass\s*=\s*[\"'][^\"']*(?<![\w-])%s(?![\w-])"
WIDGET = re.compile(CLS % "(?:g-recaptcha|h-captcha|cf-turnstile|frc-captcha|captchafox)"
                    + r"|<altcha-widget\b", re.I)
TITLE = re.compile(r"<title[^>]*>(.*?)</title>", re.I | re.S)
BLOCK = ("access denied", "error 1020", "unusual traffic", "you have been blocked")

चरण 3: कैप्चा या ब्लॉक तय करें

जाँचों का क्रम ही तर्क है। हर नियम तभी चलता है जब उसके ऊपर के नियम मैच न हुए हों।

  1. cf-mitigated header सबसे पहले आता है। Cloudflare के दस्तावेज़ के अनुसार वह challenge पेजों पर मान challenge भेजता है, और यह फ़ंक्शन का सबसे मज़बूत संकेत है।
  2. रीडायरेक्ट की जाँच अगली होती है, उन कॉलर के लिए जो redirect follow करना बंद रखते हैं। उसे unknown कहा जाता है, क्योंकि अकेला पता यह नहीं बता सकता कि वह किसी कैप्चा तक ले जाता है या ऐसे लेख तक जिसके नाम में वह शब्द है।
  3. _cf_chl_opt script variable और Just a moment title उसके बाद आते हैं। जब किसी proxy या CDN ने header हटा दिया हो, तब ये challenge पेज को पकड़ लेते हैं।
  4. expect argument आपको वह string बताने देता है जो सिर्फ़ असली पेज में होती है। जब वह मौजूद हो, तो status चाहे जो हो, पेज को ok कहा जाता है। उसके बिना, footer में कैप्चा विजेट वाला कोई कंटेंट पेज कैप्चा कहलाएगा।
  5. markup में विजेट का मतलब captcha है, 429 या 403 पर भी। यह status नियमों से पहले चलता है, क्योंकि कुछ साइटें अपना कैप्चा error status के साथ परोसती हैं। विजेट सूची में reCAPTCHA, hCaptcha, Turnstile, Friendly Captcha, CaptchaFox और ALTCHA आते हैं।
  6. 429, या Retry-After वाला 503, का मतलब rate_limited है। HTML body वाला 403 blocked है, जब तक पेज के टेक्स्ट में csrf न हो। JSON 403 को unknown रहने दिया जाता है, क्योंकि जो API कहती है कि आपके टोकन में scope नहीं है वह आपको rotate करने को नहीं कह रही।
  7. 400 से नीचे status वाला response soft block है जब उसके title में इनमें से कोई phrase हो, या जब पूरा पेज 3,000 characters से छोटा हो और उसमें कोई हो।
def classify(resp, expect=None):
    """expect: a string only the real page contains, for pages that carry a widget anyway."""
    code, h = resp.status_code, resp.headers
    if h.get("cf-mitigated", "").lower() == "challenge":
        return "cloudflare_challenge"                 # Cloudflare docs
    if 300 <= code < 400:                             # allow_redirects=False callers
        return "unknown"
    html = "html" in h.get("content-type", "").lower()
    text = resp.text[:200000] if html else ""
    low = text.lower()
    if "_cf_chl_opt" in text or "<title>just a moment" in low:
        return "cloudflare_challenge"                 # header stripped by a proxy
    if expect and expect in text:
        return "ok"
    if WIDGET.search(text):
        return "captcha"                              # even on a 429 or a 403
    if code == 429 or (code == 503 and "retry-after" in h):
        return "rate_limited"
    if code == 403 and html and "csrf" not in low:
        return "blocked"                              # JSON 403s fall through
    m = TITLE.search(text)
    head = m.group(1).lower() if m else ""
    small = len(text) < 3000                          # block pages are short
    if code < 400 and html and any(b in head or (small and b in low) for b in BLOCK):
        return "blocked"                              # soft block: 200 with a notice
    return "ok" if code < 400 else "unknown"

चरण 4: Retry-After पढ़ें

इस ब्लॉक, अगले और उसके बाद वाले को policy.py के रूप में सेव करें। 429 के साथ Retry-After header आ सकता है, और header में सेकंड या HTTP तारीख हो सकती है। MDN दोनों रूप बताता है, और कहता है कि 429 पर header वैकल्पिक है। helper दोनों संभालता है और जब header न हो या पढ़ा न जा सके तो आपके चुने हुए नंबर पर लौट आता है।

import re
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from urllib.parse import urljoin

from classify import classify


def retry_delay(resp, fallback):
    raw = resp.headers.get("Retry-After")
    if not raw:
        return fallback
    try:
        return max(1.0, float(raw))                        # delay-seconds
    except ValueError:
        pass
    try:
        when = parsedate_to_datetime(raw)                  # HTTP-date
        return max(1.0, (when - datetime.now(timezone.utc)).total_seconds())
    except (TypeError, ValueError):
        return fallback

चरण 5: कैप्चा हल करें

CapSkip को सिर्फ़ यहीं कॉल किया जाता है। यह पेज से sitekey और फ़ॉर्म की action पढ़ता है, CapSkip से टोकन माँगता है और उसे g-recaptcha-response फ़ील्ड में वापस पोस्ट करता है। यह जानबूझकर सिर्फ़ reCAPTCHA v2 संभालता है, और सिर्फ़ वही एक फ़ील्ड पोस्ट करता है, कुछ और नहीं, इसलिए जिस फ़ॉर्म में और ज़रूरी फ़ील्ड हों उनमें data में उन्हें जोड़ना पड़ेगा। सूची के हर विजेट को captcha लेबल मिलता है, इसलिए Turnstile, hCaptcha, Friendly Captcha, CaptchaFox या ALTCHA पेज भी इस फ़ंक्शन तक पहुँचता है। यह उन्हें एक RuntimeError से रोकता है जो कहता है कि पहले कैप्चा टाइप डिटेक्टर चलाएँ। इसके बजाय reCAPTCHA method को Turnstile key भेजने का अंजाम ERROR_GOOGLEKEY होगा: CapSkip ऐसी key को ठुकरा देता है जो reCAPTCHA key जैसी नहीं दिखती, जिसे मैंने ऐप की key जाँच की नकल करने वाले mock पर पक्का किया, ऐप पर नहीं। reCAPTCHA कॉल के बारे में खुद जानने के लिए यह पेज देखें: reCAPTCHA v2 सॉल्वर पेज.

def solve_recaptcha_form(session, resp, solver):
    """reCAPTCHA v2 only. For other types, run the detector first."""
    key = re.search(r"data-sitekey=[\"']([^\"']+)", resp.text)
    form = re.search(r"<form[^>]*action=[\"']([^\"']*)", resp.text)
    if "g-recaptcha" not in resp.text or not (key and form):
        raise RuntimeError("not a plain reCAPTCHA v2 form: run the detector first")
    token = solver.recaptcha(sitekey=key.group(1), url=resp.url)["code"]
    return session.post(urljoin(resp.url, form.group(1)),
                        data={"g-recaptcha-response": token}, timeout=30)

चरण 6: fetch loop

लूप कैप्चा या ब्लॉक वाले फ़ैसले पर अमल करता है। वह हर लेबल को एक कार्रवाई में बदलता है और ज़्यादा से ज़्यादा चार बार कोशिश करता है। rate limit में वह header के मान जितना सोता है, न्यूनतम 1 सेकंड और अधिकतम 60। अधिकतम का मतलब है कि एक दिन का Retry-After एक मिनट बाद दोबारा आज़माया जाता है, जिससे सीमा लंबी खिंच सकती है। bare 429, यानी बिना Retry-After वाला, दो बार 5 सेकंड इंतज़ार पाता है और फिर rotation, और गिनती नए पते के लिए फिर शुरू होती है। चार कोशिशों के साथ इसका मतलब है दो इंतज़ार, एक rotation और एक और इंतज़ार। Retry-After वाला 429 कभी rotate नहीं करता। ब्लॉक आपके rotate फ़ंक्शन को कॉल करता है और सॉल्वर तक कभी नहीं पहुँचता। आपका rotate फ़ंक्शन अगले request की शक्ल बदले, जैसे session.proxies सेट करके, या रन रोकने के लिए exception उठाए। challenge पेज आपके on_challenge फ़ंक्शन को कॉल करता है, जो session.cookies पर एक वैध cf_clearance cookie छोड़ दे, क्योंकि पूरे पेज के challenge के लिए Turnstile method को जो मान चाहिए वे सादे request को मिलने वाले HTML में नहीं होते। दोनों में से कोई फ़ंक्शन लौटे तो loop बस URL फिर माँग लेता है। सफल होने पर फ़ंक्शन फ़ॉर्म पोस्ट का response लौटाता है, इसलिए अगर आपका टारगेट पेज कोई और है, तो उसे फिर माँगें। हर कैप्चा कोशिश में एक हल लगता है और वह पाँच मिनट तक इंतज़ार कर सकती है, इसलिए चार नाकाम कोशिशें चार हल इस्तेमाल कर सकती हैं। NetworkException का मतलब है CapSkip तक पहुँचा नहीं जा सकता, और उसे यहाँ पकड़ा नहीं जाता। cf_clearance गाइड उस cookie को कवर करती है जो इसके बाद आती है।

def fetch(session, url, solver, rotate, on_challenge,
          tries=4, sleep=time.sleep, expect=None):
    limited = 0
    for _ in range(tries):
        resp = session.get(url, timeout=30)
        kind = classify(resp, expect)
        if kind == "ok":
            return resp
        if kind == "rate_limited":
            limited += 1
            if limited > 2 and "Retry-After" not in resp.headers:
                rotate(session)                    # a bare 429 that outlasts two waits
                limited = 0                        # the new address starts fresh
            else:
                sleep(min(retry_delay(resp, 5.0), 60.0))
        elif kind == "captcha":
            resp = solve_recaptcha_form(session, resp, solver)
            if classify(resp, expect) == "ok":
                return resp
        elif kind == "cloudflare_challenge":
            on_challenge(session, resp)            # yours: a browser, or the Turnstile guide
        elif kind == "blocked":
            rotate(session)                        # never solve a block
        else:
            raise RuntimeError(f"unknown response {resp.status_code} from {url}")
    raise RuntimeError(f"gave up on {url} after {tries} tries")

इसे चलाएँ

इसे बाकी दो फ़ाइलों के बगल में use.py के रूप में सेव करें। पता बदलें, और rotate में अपना proxy लॉजिक डालें। नमूना rotate exception उठाता है, ताकि ब्लॉक हुआ रन उसी पते को चार बार पूछने की जगह रुक जाए। इसे एनवायरनमेंट के अपने Python से चलाएँ, जैसे .\.venv\Scripts\python.exe use.py। पोर्ट 8080 default है; अगर आपका अलग हो तो CapSkip की कनेक्शन सेटिंग्स में दिखा पोर्ट इस्तेमाल करें। placeholder पते के साथ script एक unknown response error पर खत्म होती है, जो अपेक्षित नतीजा है।

import requests
from capskip import CapSkip
from policy import fetch

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


def rotate(session):
    raise RuntimeError("blocked: change the proxy here, or stop the run")


def on_challenge(session, resp):
    raise RuntimeError("Cloudflare challenge page: needs a browser")


session = requests.Session()
page = fetch(session, "https://example.com/data", solver, rotate, on_challenge)
print(page.status_code, len(page.text))

भरोसा करने से पहले यह देखने के लिए कि classifier असली responses का क्या करता है, अपने कुछ URLs पर यह जाँच चलाएँ। 29 अक्टूबर 2026 के रन में उसने सादे पेज के लिए ok, 429 के लिए rate_limited और एक टेस्ट सर्विस के 403 के लिए blocked प्रिंट किया।

import requests
from classify import classify

URLS = [
    "https://example.com/",
    "https://httpbin.org/status/429",
    "https://httpbin.org/status/403",
]

for url in URLS:
    resp = requests.get(url, timeout=30)
    print(classify(resp), resp.status_code, url)

जहाँ classifier गलत है

यह heuristics का एक समूह है, और ये मामले ज्ञात हैं।

मामलानतीजाफिक्स
footer में कैप्चा विजेट वाला कंटेंट पेजcaptchaclassify या fetch को expect ऐसी string के साथ दें जो सिर्फ़ असली पेज में हो
title में unusual traffic वाली ब्लॉग पोस्टblockedBLOCK के phrases को अपने टारगेट के अपने phrases से बदलें
ऐप से ही आया HTML 403, जैसे लॉगिन पेजblocked403 नियम से पहले उस पेज के लिए एक नियम जोड़ें
किसी /captcha पेज पर सीधा विज़िट, या रीडायरेक्ट, जिसमें सिर्फ़ इमेज होokउस साइट के लिए एक नियम जोड़ें
g-recaptcha class और data-action attribute वाला button (reCAPTCHA v3 binding)captchaवह विजेट पेज को कभी गेट नहीं करता: expect दें, या डिटेक्टर चलाएँ
लोड के बाद JavaScript से जोड़ा गया कैप्चाokclassifier सिर्फ़ पहला response देखता है
पूरी साइट पर reCAPTCHA v3 scriptokइरादतन: v3 पेज को कभी गेट नहीं करता

आम errors

त्रुटिकारणफिक्स
RuntimeError: unknown responseऐसा status जिसे नियम कवर नहीं करते, जैसे JSON 403 या 404body प्रिंट करें और नियम जोड़ें, या रन को रुकने दें
RuntimeError: gave upok के बिना चार कोशिशेंदेखें कौन सा लेबल दोहराया, और tries तभी बढ़ाएँ जब वह rate_limited था
RuntimeError: not a plain reCAPTCHA v2 formcaptcha लेबल किसी और विजेट से आया, या पेज में sitekey या फ़ॉर्म नहीं हैडिटेक्टर चलाएँ और मेल खाता method कॉल करें
ERROR_GOOGLEKEYkey reCAPTCHA key जैसी नहीं दिखती, जैसे Turnstile key या शब्द explicitkey सीधे विजेट से पढ़ें, या डिटेक्टर चलाएँ
ApiException या TimeoutExceptionहल फेल हुआ या SDK की सीमा से आगे निकल गयाउससे पहले वाला लेबल देखें, और tries कम रखें ताकि फेल होता कैप्चा बहुत से हल इस्तेमाल न करे
NetworkExceptionCapSkip चल नहीं रहा, या host या port गलत हैCapSkip शुरू करें और कनेक्शन सेटिंग्स जाँचें, फिर यह देखें: API दस्तावेज़

सॉल्वर को किसी server पर चलाना

लूप को CapSkip के लिए सिर्फ़ host और पोर्ट चाहिए। Local मोड में वह उसी PC पर 127.0.0.1 है। Server मोड में CapSkip आपके नेटवर्क पते या पब्लिक IP पर सुनता है, इसलिए किसी दूसरे PC, VPS या होस्टेड प्लेटफ़ॉर्म पर चल रहा स्क्रैपर वही API कॉल करता है। CapSkip constructor में host और पोर्ट बदलें और कुछ और नहीं बदलता। स्थिर रिमोट एक्सेस के लिए static पब्लिक IP सुझाई जाती है। Server मोड फिर भी आपका अपना हार्डवेयर है और फिर भी बिना मीटर वाला है। यह पोर्ट को किसी भी पते के लिए खोल देता है, इसलिए जिन पतों को वहाँ पहुँचना नहीं चाहिए उनके लिए Windows Firewall में block rule जोड़ें। मोड बदलने का तरीका कनेक्शन सेटिंग्स में समझाया गया है।

FAQ

मुझे कैसे पता चले कि मैं ब्लॉक हूँ या बस rate limited?

429, खासकर Retry-After header के साथ, rate limit है, और इंतज़ार काम करता है। HTML body वाला 403, या ऐसा 200 पेज जो कहता है कि access denied है, ब्लॉक है। जो bare 429 दो इंतज़ार के बाद भी बना रहे, उसे अंदाज़े के नियम के तौर पर ब्लॉक माना जाता है, इसीलिए loop उस पर rotate करता है।

क्या हल किया हुआ कैप्चा ब्लॉक ठीक कर सकता है?

नहीं। ब्लॉक आपके पते या आपके fingerprint को ठुकराता है, और टोकन इनमें से किसी के बारे में कुछ साबित नहीं करता। ब्लॉक पर loop सॉल्वर को छोड़ देता है और उसकी जगह rotate करता है।

loop Cloudflare challenge पेज को हल क्यों नहीं करता?

Turnstile method को challenge से दो मान चाहिए, और सादा request उन्हें नहीं पाता। loop वह मामला आपके दिए फ़ंक्शन को सौंप देता है, जैसे कोई ऐसा जो असली ब्राउज़र चलाए, और classifier को इस बारे में ईमानदार रखता है कि वह क्या देख सकता है।

क्या यह किसी दूसरी मशीन पर चल रहे CapSkip के सामने चल सकता है?

हाँ। Server मोड इस्तेमाल करें, CapSkip constructor को उसी host और पोर्ट पर पॉइंट करें, और loop वैसे ही काम करता है जैसे एक PC पर। सॉल्वर आपके हार्डवेयर पर ही रहता है।

संक्षेप में

पहले कैप्चा या ब्लॉक तय करें, फिर कार्रवाई करें: कैप्चा हल करें, rate limit का इंतज़ार करें, ब्लॉक से rotate करके दूर हो जाएँ और जो कुछ unknown हो उस पर रुक जाएँ। अपनी मशीन पर CapSkip के साथ, बेकार जाने वाला हल छोड़ने से समय बचता है, और बेवजह rotate न करने से पते बचते हैं। कैप्चा पेजों को पार करने का व्यापक नज़रिया चाहिए, तो कैप्चा बायपास का तरीका यह है कि जो हल हो सकता है उसे हल करें और बाकी से बचकर निकल जाएँ। Python SDK के बारे में खुद जानना हो, तो यह पेज देखें: Python कैप्चा सॉल्वर पेज.