So erkennen Sie den Captcha-Typ einer Seite und lösen ihn (Python)

detect captcha type - How to Detect CAPTCHA Type on a Page and Solve It (Python)

Um den Captcha-Typ auf einer Seite zu erkennen, lesen Sie das HTML der Seite nach dem Skript und dem Container des Widgets, ziehen den sitekey heraus und ordnen die Familie der CapSkip-Methode zu, die sie löst. Ein reCAPTCHA-v2-Kasten, ein v3-Skript und ein Turnstile-Widget brauchen drei verschiedene Aufrufe mit unterschiedlichen Optionen, und ein falscher Tipp verschwendet einen ganzen Lösevorgang und liefert Ihnen ein Token, das Sie nicht verwenden können. Dieser Leitfaden baut in Python einen Detektor von etwa 175 Zeilen, der den Typ, die Methode und die Argumente zurückgibt oder offen sagt, dass er es nicht erkennen kann. CapSkip läuft auf Ihrem eigenen Windows-PC ohne Gebühr pro Lösung, daher kostet ein Skript, das hundert Seiten prüft, nichts extra.

Was Sie brauchen

  • CapSkip läuft auf einem Windows-PC, mit den Lösern, die Sie brauchen, eingeschaltet. Die Einrichtungsanleitung führt durch den ersten Start.
  • Python 3.10 oder neuer. Der Code unten lief auf Python 3.14.0 und 3.11.7 mit requests 2.33.0 und 2.34.2 sowie capskip 1.3.0.
  • Eine PowerShell-Eingabeaufforderung. Die Befehle verwenden PowerShell-Syntax.
  • Eine Seite, die Ihnen gehört oder die Sie automatisieren dürfen, mit einem Captcha darauf.
  • Eine Adresse für den Solver. Im Local-Modus antwortet CapSkip auf 127.0.0.1 nur für dieses Gerät. Im Server-Modus lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, damit ein Skript auf einem anderen PC, einem VPS oder einer gehosteten Plattform es über die API aufrufen kann. Eingestellt werden beide Modi in den Verbindungseinstellungen.

So wurde getestet: Der Detektor lief gegen 66 gespeicherte Seiten, und jede Seite lieferte das erwartete Ergebnis. Davon wurden 59 von Hand gebaut (etwa 40 folgen der Dokumentation der Anbieter, der Rest sind Randfälle, die ich mir ausgedacht habe) und 7 sind Schnappschüsse öffentlicher Seiten vom 29. Oktober 2026. Die Lösevorgänge liefen über das echte capskip-Paket gegen einen Mock-CapSkip (einen nachgebauten Testersatz), der die Eingabeprüfungen der App nachbildet. Die echte App lief nicht, daher ist keine Zahl unten eine Lösezeit.

Warum Sie den Captcha-Typ erkennen, bevor Sie lösen

CapSkip hat für jede Familie eine eigene Methode: recaptcha, turnstile, geetest, altcha, capy, captchafox, friendly_captcha und normal für Bilder im SDK, dazu hcaptcha über die HTTP API, die das SDK noch nicht abdeckt. Auch innerhalb von reCAPTCHA kommt es auf die Optionen an. Invisible, Enterprise und v3 sind Flags am selben Aufruf. Wenn Sie die falschen senden, ist der beste Fall ein fehlgeschlagener Job, der schlimmste ein Token, den die Website ohne jeden Hinweis ablehnt. Ich konnte nicht prüfen, was die echte App bei einer falschen reCAPTCHA-Version tut. In beiden Fällen ist der Lösevorgang verbraucht.

Andere Fehler schlagen schnell und laut fehl. Wenn Sie den sitekey von einer Seite lesen, die reCAPTCHA explizit rendert, erhalten Sie das Wort explicit, und CapSkip lehnt es mit ERROR_GOOGLEKEY ab. Die Anleitung zu ERROR_GOOGLEKEY und ERROR_PAGEURL behandelt diesen Fehler von der anderen Seite. Um den Captcha-Typ zuverlässig zu erkennen, muss ein Detektor die Signale in einer festen Reihenfolge prüfen, und diese Reihenfolge vermeidet beide Arten von Fehlern. Ein Detektor ist außerdem das Erste, was man vor einem Scraper baut, der auf mehr als eine Website trifft. Wenn Sie den Hintergrund zu den einzelnen Typen möchten: Der Beitrag Was ein Captcha ist erklärt die Familien ohne Code.

Schritt 1: Installieren und die Seite abrufen

Legen Sie in einem leeren Ordner eine virtuelle Umgebung an und installieren Sie die zwei Pakete. Die Befehle rufen das Python der Umgebung direkt über den Pfad auf, Sie müssen sie also nicht aktivieren.

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

Um den Captcha-Typ aus Text zu erkennen, braucht der Detektor nur zwei Eingaben: das HTML der Seite und ihre Adresse. Rufen Sie die Seite mit einer requests-Session ab und behalten Sie diese Session, denn der Schritt für das Bild-Captcha lädt das Bild später mit denselben Cookies herunter. Ein einfacher Abruf mit requests zeigt, was der Server sendet, bevor JavaScript läuft. Die reCAPTCHA-Demoseiten von Google, die Turnstile-Demo von Cloudflare und die hCaptcha-Demo tragen ihr Widget alle in diesem HTML. Seiten, die das Widget per JavaScript hinzufügen, tun das nicht, und der Abschnitt “Wenn das HTML nichts hergibt” behandelt diesen Fall.

Schritt 2: Die Hilfsfunktionen schreiben

Speichern Sie alles von hier bis Schritt 9 in einer Datei namens detect.py, Block für Block hintereinander. Der erste Block importiert, was gebraucht wird, und schreibt vier kleine Leser. Jeder gibt None zurück, wenn nichts passt, sodass ein fehlendes Attribut nie eine Exception auslöst.

import base64, html as htmllib, re, time
from types import SimpleNamespace as NS
from urllib.parse import urljoin
import requests

SITEKEY_JS = r"sitekey['\"]?\s*:\s*['\"]([^'\"]+)"   # sitekey: '...' inside a render() call


def grab(pattern, text):  # first capture group that matched, or None
    m = re.search(pattern, text or "", re.I | re.S)
    return next(g for g in m.groups() if g is not None) if m else None

def tag(cls, h):  # opening tag with CSS class cls (and not g-recaptcha-response)
    pat = r'(<\w+[^>]*\sclass\s*=\s*["\'][^"\']*(?<![\w-])%s(?![\w-])[^"\']*["\'][^>]*>)'
    return grab(pat % cls, h)

def attr(t, name):  # attribute value from a tag string, either quote style, HTML-unescaped
    v = grab(r'\s%s\s*=\s*(?:"([^"]*)"|\'([^\']*)\')' % name, t)
    return None if v is None else htmllib.unescape(v)

def render_opts(call, js):  # first 400 characters of the options in call(el, {...})
    return grab(call + r"\([^,]+,\s*\{(.{0,400})", js)

Die Funktion grab gibt die erste Capture-Gruppe zurück, die gepasst hat. Die Funktion tag findet ein öffnendes Tag über die CSS-Klasse und weist eine Klasse zurück, die den Namen nur innerhalb einer längeren enthält, sodass eine Klasse namens cf-turnstile-response kein Turnstile-Widget ist. Die Funktion attr liest ein Attribut, das mit beiden Anführungszeichen-Arten geschrieben sein kann, und hebt die Maskierung von HTML-Entities auf, was bei einer Challenge in einem Attribut wichtig ist. Attribute ohne Anführungszeichen werden nicht gelesen.

Der zweite Block macht aus einer Seite etwas, das die Prüfungen gemeinsam nutzen können. Er entfernt Kommentare und Style-Blöcke, sodass eine CSS-Regel, die eine Widget-Klasse erwähnt, nicht passen kann, und er sammelt die Skript-Adressen und das Inline-JavaScript getrennt. Die letzten zwei Hilfsfunktionen bauen die beiden möglichen Antworten: einen Treffer oder eine Antwort needs_browser.

def parse(html, url):
    h = re.sub(r"<!--.*?-->|<style\b.*?</style>", "", html, flags=re.S | re.I)
    srcs = " ".join(re.findall(r'<script\b[^>]*?\ssrc\s*=\s*["\']([^"\']+)', h, re.I))
    js = "\n".join(re.findall(r"<script\b[^>]*>(.*?)</script>", h, re.S | re.I))
    return NS(h=h, srcs=htmllib.unescape(srcs), js=js, url=url)

def hit(kind, method, **args):
    return {"type": kind, "method": method, "args": args}

def browser(kind, method, p, why):  # the static HTML is not enough, so do not guess
    return {"type": kind, "method": method, "args": {"url": p.url}, "needs_browser": why}

Schritt 3: hCaptcha und was CapSkip nicht lösen kann

Beginnen Sie mit den Seiten, bei denen die richtige Antwort Nein lautet. CapSkip löst weder FunCaptcha noch Arkose, und GeeTest v4 liegt außerhalb der GeeTest-Methode, die für v3 gedacht ist. Würde der Detektor diese überspringen, würde eine Seite mit einem Arkose-Kasten zur nächsten Prüfung durchfallen und könnte als etwas zurückkommen, das sie nicht ist. Deshalb läuft diese Prüfung zuerst und gibt ein Ergebnis ohne Methode zurück.

Derselbe Block behandelt die Vollseiten-Prüfung von Cloudflare, die Zwischenseite “Just a moment”. Diese Seite ist kein Turnstile-Widget. Die Methode turnstile braucht von ihr zwei Werte, data und pagedata, und ein einfacher Abruf enthält sie nicht. Der Detektor sucht nach den Inline-Challenge-Optionen der Seite und antwortet needs_browser. Die cf_clearance-Anleitung behandelt, was folgt, nachdem eine Challenge-Seite bestanden ist.

Als Nächstes kommt hCaptcha, noch vor reCAPTCHA. CapSkip löst es, ob das Widget eine Checkbox, unsichtbar oder Enterprise ist, aber eine Website, die von reCAPTCHA zu hCaptcha gewechselt ist, behält an ihrem Widget oft die Klasse g-recaptcha, und nur das hCaptcha-Skript unterscheidet die beiden. Die Prüfung liest den sitekey aus dem Widget oder aus einem Aufruf von hcaptcha.render und fügt invisible hinzu, wenn das Widget data-size auf invisible gesetzt hat. Eine Enterprise-Seite, die dem Widget über hcaptcha.setData einen rqdata-Wert übergibt, bekommt stattdessen needs_browser, weil dieser Wert für eine einzige Challenge ausgestellt wird und schnell abläuft.

UNSUPPORTED = {"arkose": r"arkoselabs\.com/v2/|funcaptcha\.com/fc/", "geetest_v4": r"gt4\.js|initGeetest4"}

def unsupported(p):  # CapSkip cannot solve these: say so instead of misleading the reader
    for kind, pat in UNSUPPORTED.items():
        if re.search(pat, p.srcs + " " + p.js):
            return {"type": kind, "method": None, "args": {}, "unsupported": True}

def cf_challenge(p):  # the full-page "Just a moment..." interstitial, not a widget
    if re.search(r"_cf_chl_opt\s*=\s*\{", p.js):
        why = "data and pagedata (Cloudflare calls them cData and chlPageData) only exist at run time"
        return browser("turnstile_challenge", "turnstile", p, why)

def hcaptcha(p):  # before reCAPTCHA: a site that moved to hCaptcha may keep the g-recaptcha class
    if not re.search(r"hcaptcha\.com/1/api\.js", p.srcs + " " + p.js):
        return None
    if re.search(r"\.setData\s*\(|rqdata", p.js):  # Enterprise rqdata is issued per challenge
        return browser("hcaptcha_enterprise", "hcaptcha", p, "rqdata is issued for one challenge at run time")
    w = tag("h-captcha", p.h) or tag("g-recaptcha", p.h)
    key = attr(w, "data-sitekey") or grab(SITEKEY_JS, render_opts(r"hcaptcha\.render", p.js))
    if not key:
        return browser("hcaptcha", "hcaptcha", p, "no sitekey in the static HTML")
    args = {"sitekey": key, "url": p.url}
    if attr(w, "data-size") == "invisible":
        args["invisible"] = 1
    return hit("hcaptcha", "hcaptcha", **args)

Schritt 4: reCAPTCHA, in der richtigen Reihenfolge

reCAPTCHA braucht die meiste Sorgfalt, weil mehrere CapSkip-Varianten zwei Skriptnamen teilen. Die Reihenfolge der Fragen hält sie auseinander. Zuerst der Skriptname: enterprise.js bedeutet das Enterprise-Flag, api.js nicht. Dann der Parameter render in der Skript-Adresse. Ein Schlüssel nach render= bedeutet v3, aber das Wort explicit nach render= bedeutet eine v2-Seite, die ihr Widget aus Code rendert, und es darf nie als Schlüssel verwendet werden. Dann das Markup. Ein Button mit der Klasse g-recaptcha und einem Attribut data-action ist die v3-Button-Bindung von Google. Ein Button ohne data-action oder ein beliebiges Element mit data-size auf invisible ist das unsichtbare v2-Widget. Alles andere mit dieser Klasse ist die Checkbox.

def recaptcha(p):  # Enterprise flag first, then v3 (render=KEY), then v2
    m = re.search(r"/recaptcha/(api|enterprise)\.js(\?\S*)?", p.srcs)
    if not m:
        return None
    ent = {"enterprise": 1} if m.group(1) == "enterprise" else {}
    render = grab(r"[?&]render=([^&\s]+)", m.group(2))  # a v3 sitekey, or "explicit" (a v2 page)
    t, o = tag("g-recaptcha", p.h), render_opts(r"\.render", p.js)
    button, action = (t or "").lower().startswith("<button"), attr(t, "data-action")
    v3 = bool(render and render != "explicit") or bool(button and action)
    key = render if render != "explicit" else None
    key = key or attr(t, "data-sitekey") or grab(SITEKEY_JS, o)
    if not key:
        return browser("recaptcha", "recaptcha", p, "no sitekey in the static HTML (set by JavaScript)")
    small = attr(t, "data-size") == "invisible" or grab(r"size['\"]?\s*:\s*['\"](invisible)", o)
    invisible = not v3 and bool(button or small)
    action = action or grab(r"execute\(\s*[^,)]+,\s*\{\s*['\"]?action['\"]?\s*:\s*['\"]([^'\"]+)", p.js)
    args = {"sitekey": key, "url": p.url, **ent}
    if v3:
        args.update(version="v3", **({"action": action} if action else {}))
    if invisible:
        args["invisible"] = 1
    kind = "recaptcha_" + ("v3" if v3 else "v2_invisible" if invisible else "v2")
    return hit(kind + ("_enterprise" if ent else ""), "recaptcha", **args)

Jedes Ergebnis ist einem Aufruf zugeordnet, den Sie schon kennen. Eine Checkbox ist solver.recaptcha mit einem sitekey und der Seiten-URL. Invisible fügt invisible=1 hinzu, Enterprise fügt enterprise=1 hinzu, und v3 fügt version="v3" und die Action hinzu. Der Name der Action stammt aus dem Attribut data-action oder aus dem execute-Aufruf der Seite; steht keins von beiden im HTML, wird das Argument weggelassen, und CapSkip verwendet seinen Standard verify. Siehe die reCAPTCHA-Solver-Seite für jede Variante, die sie abdeckt.

Schritt 5: Turnstile

Ein Turnstile-Widget braucht auf der Seite zwei Dinge: die api.js von Cloudflare und einen sitekey. Der Schlüssel steht in einem Attribut data-sitekey an einem div mit der Klasse cf-turnstile oder in einem Aufruf turnstile.render. Die Prüfung verlangt das Skript, sodass ein verirrter Klassenname auf einer fremden Seite sie nicht auslösen kann. Sie liest außerdem data-action und data-cdata, wenn sie vorhanden sind, und reicht sie weiter. Die Turnstile-Solver-Seite listet die Optionen auf.

def turnstile(p):  # widget only: implicit (div.cf-turnstile) or explicit (turnstile.render)
    if "challenges.cloudflare.com/turnstile/v0/api.js" not in p.srcs:
        return None
    t, o = tag("cf-turnstile", p.h), render_opts(r"turnstile\.render", p.js)
    key = attr(t, "data-sitekey") or grab(SITEKEY_JS, o)
    if not key:
        return browser("turnstile", "turnstile", p, "no sitekey in the static HTML")
    action = attr(t, "data-action") or grab(r"\baction['\"]?\s*:\s*['\"]([^'\"]+)", o)
    cdata = attr(t, "data-cdata") or grab(r"\bcData['\"]?\s*:\s*['\"]([^'\"]+)", o)
    extra = {k: v for k, v in (("action", action), ("data", cdata)) if v}
    return hit("turnstile", "turnstile", sitekey=key, url=p.url, **extra)

Schritt 6: GeeTest und ALTCHA

GeeTest v3 liefert zwei Werte, und einer davon ist eine Falle. Der Wert gt ist für die Website fest und kann in der Seite stehen. Der Wert challenge ist einmalig, und Websites holen ihn zur Laufzeit von einem Register-Endpunkt. Trägt die Seite beide, gibt der Detektor sie zurück. Trägt sie nur den Aufruf initGeetest, antwortet er needs_browser, denn ein challenge aus altem HTML wäre schon tot.

ALTCHA hat seine Attributnamen zwischen den Generationen geändert. Die Versionen 1 und 2 verwenden challengeurl oder challengejson für eine eingebettete Challenge. Version 3 und später verwenden ein einziges Attribut namens challenge, das entweder eine URL oder die Challenge-Daten annimmt. Die Prüfung liest alle drei und verbindet eine relative URL mit der Seitenadresse.

def geetest(p):  # v3 only: gt is static, challenge comes from the register endpoint
    if not (re.search(r"initGeetest\s*\(", p.js) or re.search(r"/gt(\.[\d.]+)?\.js", p.srcs)):
        return None
    gt = grab(r"\bgt['\"]?\s*:\s*['\"]([0-9a-f]{32})", p.js)
    challenge = grab(r"\bchallenge['\"]?\s*:\s*['\"]([0-9a-f]{32,34})", p.js)
    if not (gt and challenge):
        return browser("geetest", "geetest", p, "gt and challenge come from the register endpoint")
    return hit("geetest", "geetest", gt=gt, challenge=challenge, url=p.url)

def altcha(p):  # challengeurl / challengejson (v1, v2) or challenge (v3+)
    t = grab(r"(<altcha-widget\b[^>]*>)", p.h)
    if not t:
        return None
    c = attr(t, "challenge")
    as_json = attr(t, "challengejson") or (c if (c or "").lstrip().startswith("{") else None)
    as_url = attr(t, "challengeurl") or (None if as_json else c)
    if as_json:
        return hit("altcha", "altcha", url=p.url, challenge_json=as_json)
    if as_url:
        return hit("altcha", "altcha", url=p.url, challenge_url=urljoin(p.url, as_url))
    return browser("altcha", "altcha", p, "no challenge attribute")

Schritt 7: Capy, CaptchaFox und Friendly Captcha

Diese drei sind leicht zu finden und leicht ein wenig falsch zu verstehen. Capy braucht für diese Prüfung kein Widget-div. Sein Schlüssel beginnt mit PUZZLE_ und steht im Parameter k der Adresse des Widget-Skripts, und ein anderer Host als der Standard wird als api_server übergeben. CaptchaFox gibt es in zwei Ausführungen. Die einfache lädt von cdn.captchafox.com. Die andere wird von einer Hosting-Plattform unter einem gemeinsamen CDN-Pfad gebündelt, daher sucht der Detektor in diesem Pfad nach dem Paketnamen von CaptchaFox und nicht nur nach dem Pfad allein, den auch andere Widgets verwenden.

Friendly Captcha ist das Kniffligste. Version 1 und Version 2 verwenden dasselbe div und denselben Namensraum für den sitekey, aber unterschiedliche Protokolle, und ein Token für das falsche wird von CapSkip akzeptiert und von der Website abgelehnt. Die Skripte unterscheiden sie: Version 2 lädt das Paket @friendlycaptcha/sdk, Version 1 lädt friendly-challenge. Gibt die Seite keinen Hinweis, weigert sich der Detektor zu raten und sagt needs_browser.

def capy(p):  # the widget script is .../puzzle/get_js/?k=PUZZLE_...
    m = re.search(r"((?:https?:)?//[^\s\"']+?)/puzzle/get_js/\S*?(PUZZLE_[\w-]+)", p.srcs)
    if m:
        host = m.group(1) if m.group(1).startswith("http") else "https:" + m.group(1)
        more = {} if host == "https://jp.api.capy.me" else {"api_server": host}
        return hit("capy", "capy", sitekey=m.group(2), url=p.url, **more)

MAM = "mampkg/@mamdev/core.frontend.libs.captchafox"

def captchafox(p):  # plain build from cdn.captchafox.com, or the bundled build from s.uicdn.com
    t, key = tag("captchafox", p.h), grab(r"captchafox[^\"']*/api\.js\?key=([^\"'&]+)", p.srcs)
    if t or key:
        more = {"api_server": "https://s.uicdn.com/" + MAM + "/"} if MAM in p.srcs else {}
        sitekey = attr(t, "data-sitekey") or key
        if not sitekey:
            return browser("captchafox", "captchafox", p, "no sitekey in the static HTML")
        return hit("captchafox", "captchafox", sitekey=sitekey, url=p.url, **more)

def friendly(p):  # v2 loads @friendlycaptcha/sdk (site.min.js), v1 loads friendly-challenge
    t = tag("frc-captcha", p.h)
    if not t:
        return None
    v2_end, v1_end = attr(t, "data-api-endpoint"), attr(t, "data-puzzle-endpoint")
    server = v2_end or v1_end
    v2 = v2_end is not None or re.search(r"friendlycaptcha/sdk|site(\.compat)?\.min\.js", p.srcs)
    v1 = v1_end is not None or re.search(r"friendly-challenge|widget(\.module)?\.min\.js", p.srcs)
    if bool(v2) == bool(v1):
        return browser("friendly_captcha", "friendly_captcha", p, "cannot tell v1 from v2 in the HTML")
    if not attr(t, "data-sitekey"):
        return browser("friendly_captcha", "friendly_captcha", p, "no sitekey in the static HTML")
    more = {"api_server": server} if server else {}
    return hit("friendly_captcha_" + ("v2" if v2 else "v1"), "friendly_captcha",
               sitekey=attr(t, "data-sitekey"), url=p.url, version="v2" if v2 else "v1", **more)

Jedes davon hat eine eigene Solver-Seite mit den Optionen, die die Methode annimmt:

Schritt 8: das Bild-Captcha, ein Tipp

Ein Bild-Captcha hat kein Anbieter-Skript, das sich finden ließe, daher ist diese Prüfung ein Tipp und läuft nur, wenn nichts anderes gepasst hat. Die Prüfung sucht ein Bild, dessen Tag captcha enthält, und ein Textfeld an anderer Stelle der Seite, dessen Tag captcha oder security code enthält. Sie hat in jedem Fall danebengelegen, in dem das Markup diese Wörter nicht verwendete, und ein gezeichnetes Canvas ist für sie unsichtbar. Behandeln Sie einen Treffer als wahrscheinlich, nicht als sicher.

def image(p):  # a guess: an <img> that says captcha, next to a text box that says captcha
    box = r"<input\b(?![^>]*type\s*=\s*[\"']?(?:hidden|checkbox|submit))[^>]*(?:captcha|security.?code)"
    for img in re.findall(r"<img\b[^>]*>", p.h, re.I):
        if re.search(box, p.h, re.I) and re.search(r"captcha", img, re.I) and attr(img, "src"):
            return hit("image", "normal", image=urljoin(p.url, attr(img, "src")))

Schritt 9: die Prüfungen, detect und solve

Der letzte Block listet die Prüfungen der Reihe nach auf und stellt detect und solve bereit, mit detect_all als Hilfsfunktion. Die Reihenfolge ist die Logik: zuerst nicht unterstützte Anbieter, dann die Cloudflare-Seite, dann hCaptcha, dann jede andere Familie, und der Tipp für das Bild nur, wenn die Liste nichts gefunden hat. Die Funktion detect gibt das erste Ergebnis zurück und fügt eine also-Liste hinzu, wenn die Seite mehr als eine Familie trägt, zum Beispiel ein v3-Skript plus ein Turnstile-Kasten. Die Liste nennt Familien, nicht ein zweites reCAPTCHA-Widget mit einem anderen Schlüssel. Die Funktion solve leitet das Ergebnis an die richtige CapSkip-Methode weiter. Sie lehnt alles ab, was unsupported oder als needs_browser markiert ist, mit einem RuntimeError, und sie lädt ein Bild über Ihre eigene Session herunter, damit die Cookies passen.

Das SDK hat noch keinen hCaptcha-Aufruf, daher geht ein hCaptcha-Ergebnis stattdessen an solve_hcaptcha. Die Funktion sendet den sitekey und die Seitenadresse mit method=hcaptcha an in.php und fragt dann alle 5 Sekunden res.php ab, bis der Token bereit ist, wie der hCaptcha-Abschnitt der API-Dokumentation beschreibt. Übergeben Sie die Adresse des Solvers als api und Ihren API key als key, falls die Prüfung des API keys eingeschaltet ist. Das Ergebnis trägt den Token in code und den User-Agent, mit dem er erzielt wurde, in userAgent.

CHECKS = [unsupported, cf_challenge, hcaptcha, recaptcha, turnstile, geetest, altcha, capy, captchafox, friendly]

def detect_all(html, page_url):
    p = parse(html, page_url)
    return [r for r in (check(p) for check in CHECKS) if r] or [r for r in [image(p)] if r]

def detect(html, page_url):
    found = detect_all(html, page_url)
    if len(found) > 1:
        found[0]["also"] = [r["type"] for r in found[1:]]
    return found[0] if found else None

def solve_hcaptcha(args, api, key=None):  # no SDK call yet: post to in.php, poll res.php
    form = {"method": "hcaptcha", "sitekey": args["sitekey"], "pageurl": args["url"],
            "invisible": args.get("invisible", 0), "data": args.get("data"), "key": key, "json": 1}
    job = requests.post(api + "/in.php", data=form, timeout=30).json()
    if job["status"] != 1:
        raise RuntimeError(job["request"])
    for _ in range(36):  # a solve takes 10 to 30 seconds, so give up after 3 minutes
        time.sleep(5)
        q = {"key": key, "action": "get", "id": job["request"], "json": 1}
        r = requests.get(api + "/res.php", params=q, timeout=30).json()
        if r["request"] != "CAPCHA_NOT_READY":
            if r["status"] != 1:
                raise RuntimeError(r["request"])
            return {"code": r["request"], "userAgent": r.get("useragent")}
    raise TimeoutError("hCaptcha was not solved within 3 minutes")

def solve(info, solver, session=None, api="http://127.0.0.1:8080", key=None):
    if info.get("unsupported") or info.get("needs_browser"):
        raise RuntimeError(info.get("needs_browser") or info["type"] + " is not supported by CapSkip")
    args = dict(info["args"])
    if info["method"] == "hcaptcha":
        return solve_hcaptcha(args, api, key)
    if info["method"] == "normal":  # fetch the image with YOUR session so the cookie matches it
        src = args.pop("image")
        if not src.startswith("data:"):
            r = (session or requests).get(src, timeout=30)
            r.raise_for_status()
            src = "data:image/png;base64," + base64.b64encode(r.content).decode()
        args["file"] = src
    return getattr(solver, info["method"])(**args)

Ausführen

Speichern Sie dies als use.py neben detect.py und führen Sie es mit dem Python der Umgebung aus, wie in .\.venv\Scripts\python.exe use.py. Der Port 8080 ist der Standard; verwenden Sie den Port aus den Verbindungseinstellungen von CapSkip, falls Ihrer abweicht. Das Skript ruft Ihre Seite ab, gibt aus, was der Detektor gefunden hat, und ruft CapSkip nur auf, wenn es eine Methode gibt und nichts fehlt.

import requests
from capskip import CapSkip
from detect import detect, solve

URL = "https://example.com/login"   # a page of yours with a CAPTCHA
HOST, PORT = "127.0.0.1", 8080      # from CapSkip's connection settings
session = requests.Session()
html = session.get(URL, timeout=30).text

info = detect(html, URL)
print(info)

if info and info["method"] and not info.get("needs_browser"):
    solver = CapSkip(host=HOST, port=PORT)
    result = solve(info, solver, session, api=f"http://{HOST}:{PORT}")
    print(result)

Auf einer Seite mit einer reCAPTCHA-v2-Checkbox zeigt die erste Ausgabe den Typ, die Methode und die Argumente, und die zweite zeigt das Ergebnis mit dem Token in seinem Feld code. Im Mock-Lauf sah es so aus:

Seitedetect gibt zurück
reCAPTCHA v2 checkboxTyp recaptcha_v2, Methode recaptcha, sitekey und url
hCaptcha-WidgetTyp hcaptcha, Methode hcaptcha, sitekey und url
Cloudflare-Seite “Just a moment”Typ turnstile_challenge, needs_browser gesetzt
v3-Skript plus Turnstile-WidgetTyp recaptcha_v3, also listet turnstile

Wenn das HTML nichts hergibt: die Browser-Konsole

Manche Seiten fügen ihr Widget nach dem Laden mit JavaScript hinzu. Ein einfacher Abruf findet dann gar kein Captcha, und der Detektor gibt für eine Seite, die eines hat, None zurück. Zwei Fälle sind bekannt und zu erwarten: ein Skript von Google oder Cloudflare, das per Code hinzugefügt wird, und ein sitekey, der per Code gesetzt wird. Öffnen Sie dafür die Seite in Chrome, drücken Sie F12 und fügen Sie dies in die Console ein. Blockiert Chrome das Einfügen, tippen Sie zuerst allow pasting und drücken Sie Enter. Es liest die sitekeys aus den Ressourcen, die die Seite bereits geladen hat, und sieht daher, was der Browser gebaut hat.

(() => {
  const iframes = [...document.querySelectorAll('iframe')].map((f) => f.src);
  const urls = performance.getEntriesByType('resource').map((r) => r.name).concat(iframes);
  const found = new Map(), add = (o) => found.set(JSON.stringify(o), o);
  for (const u of urls) {
    let m;
    if ((m = u.match(/recaptcha\/(?:api2|(enterprise))\/anchor\?.*?\bk=([\w-]+)/)))
      add({ type: 'recaptcha', sitekey: m[2], enterprise: !!m[1], invisible: /size=invisible/.test(u) });
    else if ((m = u.match(/recaptcha\/(?:api|enterprise)\.js\?.*?\brender=([\w-]+)/)) && m[1] !== 'explicit')
      add({ type: 'recaptcha v3', sitekey: m[1], enterprise: /enterprise\.js/.test(u) });
    else if ((m = u.match(/challenges\.cloudflare\.com\/.*turnstile\/.*\/(0x4[\w-]+|[123]x0+[A-Za-z]+)\//)))
      add({ type: 'turnstile', sitekey: m[1] });
    else if ((m = u.match(/hcaptcha\.com\/captcha\/.*[#&?]sitekey=([\w-]+)/))) add({ type: 'hcaptcha', sitekey: m[1] });
    else if (/arkoselabs\.com\/fc\//.test(u)) add({ type: 'arkose: CapSkip cannot solve' });
  }
  if (window._cf_chl_opt) add({ type: 'cloudflare challenge page' });
  document.querySelectorAll('altcha-widget').forEach((e) => add({ type: 'altcha', challenge: e.getAttribute('challenge') || e.getAttribute('challengeurl') }));
  document.querySelectorAll('.h-captcha[data-sitekey]').forEach((e) => add({ type: 'hcaptcha', sitekey: e.dataset.sitekey }));
  document.querySelectorAll('.frc-captcha, .captchafox').forEach((e) => add({ type: e.classList.contains('captchafox') ? 'captchafox' : 'frc-captcha', sitekey: e.dataset.sitekey }));
  const v3 = [...found.values()].filter((o) => o.type === 'recaptcha v3').map((o) => o.sitekey);  // a v3 badge also adds an invisible anchor frame
  const res = [...found.values()].filter((o) => !(o.type === 'recaptcha' && v3.includes(o.sitekey)));
  return res.length ? res : 'no known CAPTCHA found (GeeTest, Capy and CaptchaFox scripts are not checked)';
})();

Es hat den sitekey und die Flags auf einer reCAPTCHA-v2-Seite, einer unsichtbaren Seite, einer v3-Seite, einer Turnstile-Seite und der hCaptcha-Demo gefunden und auf einer Seite ohne Captcha eine Meldung ausgegeben. Drei Grenzen: Der Browser behält standardmäßig nur etwa 250 Ressourceneinträge, sodass eine lange laufende Single-Page-App frühe verlieren kann, und das Snippet kann Friendly Captcha Version 1 nicht von Version 2 unterscheiden und sucht nicht nach Skripten von GeeTest, Capy oder CaptchaFox. Bei der Vollseiten-Prüfung von Cloudflare kann es den sitekey aus einer Ressourcenadresse zeigen, aber nicht die Werte data und pagedata, die einen Hook auf den render-Aufruf brauchen. Dieser Hook wurde hier nicht getestet.

Wo die Erkennung scheitert

SituationWas detect zurückgibtWas zu tun ist
Widget-Skript, das per JavaScript hinzugefügt wirdKeineLaden Sie die Seite in einem Browser und führen Sie das Konsolen-Snippet aus
sitekey, der per JavaScript gesetzt wirdneeds_browserLesen Sie den Schlüssel aus der aktiven Seite
Vollseiten-Prüfung von Cloudflareneeds_browserLesen Sie die Werte in einem Browser, siehe die cf_clearance-Anleitung
CaptchaFox-, Friendly-Captcha- oder Capy-Skript ohne Widget im HTMLKeineFühren Sie das Konsolen-Snippet aus oder lesen Sie den Seitenquelltext
Eine 403- oder 503-Seite der Website selbstKeineGeben Sie zuerst den Statuscode aus: Eine Sperrseite enthält kein Captcha, das sich finden ließe
Arkose, GeeTest v4unsupportedCapSkip kann diese nicht lösen
hCaptcha Enterprise, das rqdata setztneeds_browserLesen Sie für jede Aufgabe ein frisches rqdata von der Live-Seite und übergeben Sie es als data an solve_hcaptcha
Friendly Captcha ohne Skript-Hinweisneeds_browserPrüfen Sie, welches Skript die Seite lädt
Bild-Captcha ohne das Wort captchaKeineFügen Sie eine eigene Regel für diese Website hinzu

Häufige Fehler

FehlerUrsacheBeheben
ERROR_GOOGLEKEYDer Schlüssel war das Wort explicit oder ein Schlüssel, der kein reCAPTCHA-Schlüssel istLesen Sie den Schlüssel aus dem Widget, nicht aus dem Parameter render
ERROR_CAPTCHA_UNSOLVABLEJeder fehlgeschlagene Job sieht so aus, auch bei einer falschen reCAPTCHA-Version, und die Ursache wird nicht genanntPrüfen Sie zuerst den erkannten Typ, dann lesen Sie die API-Dokumentation
ValidationExceptionEine Option, die die Methode nicht annimmt, etwa eine Action bei einem v2-AufrufVerwenden Sie die Argumente, die der Detektor zurückgegeben hat
NetworkExceptionCapSkip läuft nicht, oder Host oder Port sind falschStarten Sie CapSkip und prüfen Sie die Verbindungseinstellungen
RuntimeError von solveDas Ergebnis ist unsupported oder needs_browser, oder CapSkip hat einen hCaptcha-Job abgelehnt und die Meldung ist sein FehlercodeBehandeln Sie die ersten beiden Fälle, bevor Sie solve aufrufen, und prüfen Sie beim dritten den sitekey und die Seitenadresse

Den Löser auf einem Server betreiben

Der Detektor braucht von CapSkip nur zwei Dinge: einen Host und einen Port. Im Local-Modus sind das 127.0.0.1 und Ihr eigener PC. Im Server-Modus lauscht CapSkip auf Ihrer Netzwerkadresse oder einer öffentlichen IP, sodass ein Detektor auf einem anderen PC, einem VPS oder einer gehosteten Plattform es über dieselbe API aufrufen kann. Ändern Sie HOST und PORT oben in use.py, und nichts anderes im Code ändert sich. Für einen stabilen Fernzugriff wird eine statische öffentliche IP empfohlen. Der Server-Modus läuft weiterhin auf Ihrer eigenen Hardware, und es wird weiterhin nichts pro Lösung abgerechnet. Beachten Sie, dass der Server-Modus den Port für jede Adresse öffnet, fügen Sie daher in der Windows Firewall eine Blockierregel für die Adressen hinzu, die ihn nicht erreichen sollen. Wie Sie den Modus wechseln, finden Sie in den Verbindungseinstellungen beschrieben.

FAQ

Kann ich ohne Code herausfinden, welches Captcha eine Website verwendet?

Ja. Öffnen Sie DevTools, wechseln Sie zum Tab Network, laden Sie die Seite neu und sehen Sie sich die Skript-Adressen an. Ein Skript von google.com/recaptcha oder recaptcha.net ist reCAPTCHA, challenges.cloudflare.com ist Turnstile und js.hcaptcha.com ist hCaptcha. Das Console-Snippet oben erledigt dieselbe Suche für Sie.

Warum gibt detect needs_browser zurück, statt zu raten?

Weil ein falscher Tipp einen ganzen Lösevorgang verschwendet und einen Token zurückgibt, den die Website ohne jeden Hinweis ablehnt. Enthält das statische HTML den sitekey, eine GeeTest-Challenge oder eine Version von Friendly Captcha nicht, geht es zügiger, das zu sagen, als einen Job scheitern zu lassen.

Löst CapSkip hCaptcha?

Ja. CapSkip löst hCaptcha-Widgets als Checkbox, unsichtbar und Enterprise über seine HTTP API mit method=hcaptcha, wie es die API-Dokumentation beschreibt. Das Python-SDK hat noch keinen hCaptcha-Aufruf, daher sendet solve_hcaptcha oben den Job selbst an in.php. FunCaptcha, Arkose und GeeTest v4 werden weiterhin nicht unterstützt, und der Detektor meldet sie als unsupported, damit ein Skript die Seite überspringen kann, statt an ihr zu scheitern.

Kann eine gehostete Plattform den Detektor mit meinem CapSkip verwenden?

Ja, über den Server-Modus. Stellen Sie CapSkip so ein, dass es auf Ihrer Netzwerkadresse oder öffentlichen IP lauscht, setzen Sie HOST und PORT in use.py auf diese Adresse, und der Detektor und der Aufruf zum Lösen funktionieren wie auf einem einzigen PC. Der Solver bleibt auf Ihrer Hardware.

Die Kurzfassung

Erst den Captcha-Typ erkennen, dann lösen: Lesen Sie das Skript und das Markup in fester Reihenfolge, übergeben Sie die Argumente an die richtige Methode und behandeln Sie None, needs_browser und unsupported als Antworten, nicht als Fehler. Mit CapSkip auf Ihrem eigenen Rechner läuft beim Prüfen einer Seite kein Zähler. Genau deshalb ist ein unbegrenzter Captcha-Löser praktisch für einen Crawl, der alle paar Minuten auf eine neue Website trifft. Zum Python-SDK selbst siehe die Python-Captcha-Solver-Seite.