So bauen Sie einen Captcha-Detektor in Node.js und lösen das Captcha

captcha detector in node.js - How to Build a CAPTCHA Detector in Node.js and Solve It

Ein Captcha-Detektor in Node.js liest das HTML einer Seite nach dem Skript und dem Container des Widgets, zieht den sitekey heraus und ordnet 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 Lösevorgang und liefert ein Ergebnis, das Sie nicht verwenden können. Dieser Leitfaden baut einen Detektor von etwa 165 Zeilen ohne weitere Pakete außer capskip. Er gibt den Typ, die Methode und die Argumente zurück oder sagt offen, 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. Denselben Detektor in Python finden Sie in der Anleitung zur Captcha-Typ-Erkennung.

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.
  • Node.js 20 oder neuer. Der Code unten lief auf Node.js 24.22.0, und der Detektor und der Schritt zum Lösen liefen auch auf 20.11.1 und 25.2.1, jeweils mit capskip 1.3.0. Das Paket selbst braucht Node.js 18 oder neuer, aber Node.js 18 wurde hier nicht ausprobiert.
  • 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 dieselben 66 gespeicherten Seiten wie der Python-Detektor und lieferte für jede Seite dasselbe 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, friendlyCaptcha 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 einen Ordner an, initialisieren Sie ein Projekt und installieren Sie capskip. Der Detektor verwendet nur reguläre Ausdrücke (Regex), und die Abrufe verwenden das fetch, das Node.js mitbringt.

npm init -y
npm install capskip

Ein Captcha-Detektor in Node.js arbeitet mit Text und braucht daher nur zwei Eingaben: das HTML der Seite und ihre Adresse. Ein einfaches fetch 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. Das fetch von Node.js behält keine Cookies, was für den Schritt mit dem Bild-Captcha wichtig ist, daher reicht das Skript unten sie von Hand weiter.

Schritt 2: Die Hilfsfunktionen schreiben

Speichern Sie alles von hier bis Schritt 9 in einer Datei namens detect.js, Block für Block hintereinander. Der erste Block schreibt die kleinen Leser. Jeder gibt null zurück, wenn nichts passt, sodass ein fehlendes Attribut nie einen Fehler wirft.

'use strict';
const SITEKEY_JS = /sitekey['"]?\s*:\s*['"]([^'"]+)/; // sitekey: '...' inside a render() call
const NAMED = { '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&apos;': "'" };
const unescapeHtml = (s) => s.replace(/&(?:#x([0-9a-f]+)|#(\d+)|[a-z]+);/gi, (m, hex, dec) =>
  hex || dec ? String.fromCodePoint(parseInt(hex || dec, hex ? 16 : 10)) : NAMED[m.toLowerCase()] || m);
function grab(re, text) { // first capture group that matched, or null
  const m = new RegExp(re.source, 'is').exec(text || '');
  return m ? m.slice(1).find((g) => g !== undefined) ?? null : null;
}
const tag = (cls, h) => grab(new RegExp( // opening tag that has the CSS class cls
  `(<\\w+[^>]*\\sclass\\s*=\\s*["'][^"']*(?<![\\w-])${cls}(?![\\w-])[^"']*["'][^>]*>)`), h);
function attr(t, name) { // attribute value from a tag string, either quote style, unescaped
  const v = grab(new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`), t);
  return v === null ? null : unescapeHtml(v);
}
const renderOpts = (call, js) => grab(new RegExp(`${call}\\([^,]+,\\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 zwei Hilfsfunktionen nach parse bauen die Antworten: hit und browser, die den Grund für needsBrowser trägt. Eine dritte Antwort, unsupported, bauen die Prüfungen am Ende dieses Blocks, was der nächste Schritt zusammen mit der hCaptcha-Prüfung erklärt.

function parse(html, url) {
  const h = html.replace(/<!--.*?-->|<style\b.*?<\/style>/gis, '');
  const srcs = [...h.matchAll(/<script\b[^>]*?\ssrc\s*=\s*["']([^"']+)/gi)].map((m) => m[1]).join(' ');
  const js = [...h.matchAll(/<script\b[^>]*>(.*?)<\/script>/gis)].map((m) => m[1]).join('\n');
  return { h, srcs: unescapeHtml(srcs), js, url };
}
const hit = (type, method, args) => ({ type, method, args });
const browser = (type, method, p, why) => ({ type, method, args: { url: p.url }, needsBrowser: why });

const UNSUPPORTED = { arkose: /arkoselabs\.com\/v2\/|funcaptcha\.com\/fc\//, geetest_v4: /gt4\.js|initGeetest4/ };
function unsupported(p) { // CapSkip cannot solve these: say so instead of misleading the reader
  const type = Object.keys(UNSUPPORTED).find((k) => UNSUPPORTED[k].test(p.srcs + ' ' + p.js));
  if (type) return { type, method: null, args: {}, unsupported: true };
}
const cfChallenge = (p) => /_cf_chl_opt\s*=\s*\{/.test(p.js) // the full-page "Just a moment..." page
  ? browser('turnstile_challenge', 'turnstile', p, 'data and pagedata (Cloudflare calls them cData and chlPageData) only exist at run time') : null;
function hcaptcha(p) { // before reCAPTCHA: a site that moved to hCaptcha may keep the g-recaptcha class
  if (!/hcaptcha\.com\/1\/api\.js/.test(p.srcs + ' ' + p.js)) return null;
  if (/\.setData\s*\(|rqdata/.test(p.js)) // Enterprise rqdata is issued per challenge
    return browser('hcaptcha_enterprise', 'hcaptcha', p, 'rqdata is issued for one challenge at run time');
  const w = tag('h-captcha', p.h) || tag('g-recaptcha', p.h);
  const key = attr(w, 'data-sitekey') || grab(SITEKEY_JS, renderOpts('hcaptcha\\.render', p.js));
  if (!key) return browser('hcaptcha', 'hcaptcha', p, 'no sitekey in the static HTML');
  const args = { sitekey: key, url: p.url };
  if (attr(w, 'data-size') === 'invisible') args.invisible = 1;
  return hit('hcaptcha', 'hcaptcha', args);
}

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

Diese drei Prüfungen stehen am Ende des zweiten Blocks oben, in diesem Schritt gibt es also nichts Neues einzufügen. 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 needsBrowser. 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 needsBrowser, weil dieser Wert für eine einzige Challenge ausgestellt wird und schnell abläuft.

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.

function recaptcha(p) { // Enterprise flag first, then v3 (render=KEY), then invisible, then v2
  const m = /\/recaptcha\/(api|enterprise)\.js(\?\S*)?/.exec(p.srcs);
  if (!m) return null;
  const ent = m[1] === 'enterprise' ? { enterprise: 1 } : {};
  const render = grab(/[?&]render=([^&\s]+)/, m[2]); // a v3 sitekey, or "explicit" (a v2 page)
  const t = tag('g-recaptcha', p.h), o = renderOpts('\\.render', p.js);
  const button = (t || '').toLowerCase().startsWith('<button'), dataAction = attr(t, 'data-action');
  const v3 = Boolean(render && render !== 'explicit') || Boolean(button && dataAction);
  const key = (render !== 'explicit' ? render : null) || attr(t, 'data-sitekey') || grab(SITEKEY_JS, o);
  if (!key) return browser('recaptcha', 'recaptcha', p, 'no sitekey in the static HTML (set by JavaScript)');
  const small = attr(t, 'data-size') === 'invisible' || grab(/size['"]?\s*:\s*['"](invisible)/, o);
  const invisible = !v3 && Boolean(button || small);
  const action = dataAction || grab(/execute\(\s*[^,)]+,\s*\{\s*['"]?action['"]?\s*:\s*['"]([^'"]+)/, p.js);
  const args = { sitekey: key, url: p.url, ...ent };
  if (v3) Object.assign(args, { version: 'v3' }, action ? { action } : {});
  if (invisible) args.invisible = 1;
  const kind = 'recaptcha_' + (v3 ? 'v3' : invisible ? 'v2_invisible' : 'v2');
  return hit(kind + (ent.enterprise ? '_enterprise' : ''), 'recaptcha', args);
}

Jedes Ergebnis ist einem Aufruf zugeordnet, den Sie schon kennen. Eine Checkbox ist solver.recaptcha mit einem sitekey, der Seiten-URL und einem leeren Options-Objekt. Invisible fügt invisible mit dem Wert 1 hinzu, Enterprise fügt enterprise mit dem Wert 1 hinzu, und v3 fügt version mit dem Wert 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 die Option weggelassen, und CapSkip verwendet seinen Standard verify. Siehe die reCAPTCHA-Solver-Seite für jede Variante, die sie abdeckt.

Schritt 5: Turnstile und GeeTest

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.

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 needsBrowser, denn ein challenge aus altem HTML wäre schon tot.

function turnstile(p) { // widget only: implicit (div.cf-turnstile) or explicit (turnstile.render)
  if (!p.srcs.includes('challenges.cloudflare.com/turnstile/v0/api.js')) return null;
  const t = tag('cf-turnstile', p.h), o = renderOpts('turnstile\\.render', p.js);
  const sitekey = attr(t, 'data-sitekey') || grab(SITEKEY_JS, o);
  if (!sitekey) return browser('turnstile', 'turnstile', p, 'no sitekey in the static HTML');
  const action = attr(t, 'data-action') || grab(/\baction['"]?\s*:\s*['"]([^'"]+)/, o);
  const data = attr(t, 'data-cdata') || grab(/\bcData['"]?\s*:\s*['"]([^'"]+)/, o);
  return hit('turnstile', 'turnstile', { sitekey, url: p.url, ...(action && { action }), ...(data && { data }) });
}
function geetest(p) { // v3 only: gt is static, challenge comes from the register endpoint
  if (!(/initGeetest\s*\(/.test(p.js) || /\/gt(\.[\d.]+)?\.js/.test(p.srcs))) return null;
  const gt = grab(/\bgt['"]?\s*:\s*['"]([0-9a-f]{32})/, p.js);
  const challenge = grab(/\bchallenge['"]?\s*:\s*['"]([0-9a-f]{32,34})/, p.js);
  if (!(gt && challenge)) return browser('geetest', 'geetest', p, 'gt and challenge come from the register endpoint');
  return hit('geetest', 'geetest', { gt, challenge, url: p.url });
}

Schritt 6: ALTCHA und Capy

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. In Node.js heißen die Optionen challengeUrl und challengeJson.

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 apiServer übergeben.

function altcha(p) { // challengeurl / challengejson (v1, v2) or challenge (v3+)
  const t = grab(/(<altcha-widget\b[^>]*>)/, p.h);
  if (!t) return null;
  const c = attr(t, 'challenge');
  const asJson = attr(t, 'challengejson') || ((c || '').trimStart().startsWith('{') ? c : null);
  const asUrl = attr(t, 'challengeurl') || (asJson ? null : c);
  if (asJson) return hit('altcha', 'altcha', { url: p.url, challengeJson: asJson });
  if (asUrl) return hit('altcha', 'altcha', { url: p.url, challengeUrl: new URL(asUrl, p.url).href });
  return browser('altcha', 'altcha', p, 'no challenge attribute');
}
function capy(p) { // the widget script is .../puzzle/get_js/?k=PUZZLE_...
  const m = /((?:https?:)?\/\/[^\s"']+?)\/puzzle\/get_js\/\S*?(PUZZLE_[\w-]+)/.exec(p.srcs);
  if (!m) return null;
  const host = m[1].startsWith('http') ? m[1] : 'https:' + m[1];
  const more = host === 'https://jp.api.capy.me' ? {} : { apiServer: host };
  return hit('capy', 'capy', { sitekey: m[2], url: p.url, ...more });
}

Schritt 7: CaptchaFox und Friendly Captcha

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 kann von der Website abgelehnt werden. 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 needsBrowser.

const MAM = 'mampkg/@mamdev/core.frontend.libs.captchafox';
function captchafox(p) { // plain build from cdn.captchafox.com, or the bundled build from s.uicdn.com
  const t = tag('captchafox', p.h), key = grab(/captchafox[^"']*\/api\.js\?key=([^"'&]+)/, p.srcs);
  if (!t && !key) return null;
  const more = p.srcs.includes(MAM) ? { apiServer: 'https://s.uicdn.com/' + MAM + '/' } : {};
  const sitekey = attr(t, 'data-sitekey') || key;
  if (!sitekey) return browser('captchafox', 'captchafox', p, 'no sitekey in the static HTML');
  return hit('captchafox', 'captchafox', { sitekey, url: p.url, ...more });
}
function friendly(p) { // v2 loads @friendlycaptcha/sdk (site.min.js), v1 loads friendly-challenge
  const t = tag('frc-captcha', p.h);
  if (!t) return null;
  const v2End = attr(t, 'data-api-endpoint'), v1End = attr(t, 'data-puzzle-endpoint');
  const v2 = v2End !== null || /friendlycaptcha\/sdk|site(\.compat)?\.min\.js/.test(p.srcs);
  const v1 = v1End !== null || /friendly-challenge|widget(\.module)?\.min\.js/.test(p.srcs);
  if (v2 === v1) return browser('friendly_captcha', 'friendlyCaptcha', p, 'cannot tell v1 from v2 in the HTML');
  const server = v2End || v1End;
  if (!attr(t, 'data-sitekey')) return browser('friendly_captcha', 'friendlyCaptcha', p, 'no sitekey in the static HTML');
  return hit('friendly_captcha_' + (v2 ? 'v2' : 'v1'), 'friendlyCaptcha',
    { sitekey: attr(t, 'data-sitekey'), url: p.url, version: v2 ? 'v2' : 'v1', ...(server && { apiServer: server }) });
}

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

Schritt 8: das Bild-Captcha, ein Tipp, und die Prüfungen

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.

Derselbe Block listet die Prüfungen der Reihe nach auf und definiert detect, das am Ende von Schritt 9 exportiert wird. 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.

function image(p) { // a guess: an <img> that says captcha, next to a text box that says captcha
  const box = /<input\b(?![^>]*type\s*=\s*["']?(?:hidden|checkbox|submit))[^>]*(?:captcha|security.?code)/i;
  const img = (p.h.match(/<img\b[^>]*>/gi) || []).find((i) => box.test(p.h) && /captcha/i.test(i) && attr(i, 'src'));
  if (img) return hit('image', 'normal', { image: new URL(attr(img, 'src'), p.url).href });
}
const CHECKS = [unsupported, cfChallenge, hcaptcha, recaptcha, turnstile, geetest, altcha, capy, captchafox, friendly];

function detect(html, pageUrl) {
  const p = parse(html, pageUrl);
  const found = CHECKS.map((check) => check(p)).filter(Boolean);
  const best = found[0] || image(p) || null; // the image guess comes last
  if (found.length > 1) best.also = found.slice(1).map((r) => r.type);
  return best;
}

Schritt 9: Lösen

Der letzte Block leitet das Ergebnis an die richtige CapSkip-Methode weiter. Er lehnt alles ab, was unsupported oder als needsBrowser markiert ist, mit einem Error. Die CapSkip-Methoden nehmen ihre Hauptwerte per Position und den Rest als Options-Objekt, daher sagt eine kleine Tabelle, welche Werte wohin gehören. Die Optionen in Node.js sind camelCase, mit einer Ausnahme: Wenn Sie ein Polling-Intervall pro Aufruf hinzufügen, heißt der Name polling_interval, weil ein pollingInterval pro Aufruf eine ValidationException auslöst. Die Bild-Methode nimmt überhaupt keine Optionen pro Aufruf an, setzen Sie daher für sie pollingInterval am Konstruktor von CapSkip. Ein Bild wird mit den Cookies heruntergeladen, die Sie übergeben, denn fetch hat keinen Cookie-Speicher und der eigene Download einer URL durch das SDK sendet keine. Diese Cookies gehen an jeden Host, auf dem das Bild liegt, lassen Sie das Cookie daher weg, wenn Ihr Captcha-Bild auf einem anderen Host liegt.

Das SDK hat noch keinen hCaptcha-Aufruf, daher geht ein hCaptcha-Ergebnis stattdessen an solveHcaptcha. 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 im letzten Argument von solve 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.

const POSITIONAL = { normal: ['image'], recaptcha: ['sitekey', 'url'], turnstile: ['sitekey', 'url'],
  geetest: ['gt', 'challenge', 'url'], altcha: ['url'], capy: ['sitekey', 'url'], captchafox: ['sitekey', 'url'],
  friendlyCaptcha: ['sitekey', 'url'] };

async function solveHcaptcha(args, api, key) { // no SDK call yet: post to in.php, poll res.php
  const form = { method: 'hcaptcha', sitekey: args.sitekey, pageurl: args.url, invisible: args.invisible || 0, json: 1 };
  if (args.data) form.data = args.data;
  if (key) form.key = key;
  const job = await (await fetch(`${api}/in.php`, { method: 'POST', body: new URLSearchParams(form) })).json();
  if (job.status !== 1) throw new Error(job.request);
  for (let i = 0; i < 36; i++) { // a solve takes 10 to 30 seconds, so give up after 3 minutes
    await new Promise((done) => setTimeout(done, 5000));
    const q = new URLSearchParams({ action: 'get', id: job.request, json: 1, ...(key ? { key } : {}) });
    const r = await (await fetch(`${api}/res.php?${q}`)).json();
    if (r.request !== 'CAPCHA_NOT_READY') {
      if (r.status !== 1) throw new Error(r.request);
      return { code: r.request, userAgent: r.useragent };
    }
  }
  throw new Error('hCaptcha was not solved within 3 minutes');
}

async function solve(info, solver, fetchOpts = {}, { api = 'http://127.0.0.1:8080', key } = {}) {
  if (info.unsupported || info.needsBrowser) throw new Error(info.needsBrowser || `${info.type} is not supported`);
  const args = { ...info.args };
  if (info.method === 'hcaptcha') return solveHcaptcha(args, api, key);
  if (info.method === 'normal' && !args.image.startsWith('data:')) { // fetch has no cookie jar: pass a Cookie header
    const res = await fetch(args.image, fetchOpts);
    if (!res.ok) throw new Error(`image download failed: HTTP ${res.status}`);
    args.image = 'data:image/png;base64,' + Buffer.from(await res.arrayBuffer()).toString('base64');
  }
  const names = POSITIONAL[info.method];
  const options = Object.fromEntries(Object.entries(args).filter(([k]) => !names.includes(k)));
  return solver[info.method](...names.map((k) => args[k]), options);
}
module.exports = { detect, solve };

Den Captcha-Detektor in Node.js ausführen

Speichern Sie dies als use.js neben detect.js. Es ruft Ihre Seite ab, gibt aus, was der Detektor gefunden hat, und ruft CapSkip nur auf, wenn es eine Methode gibt und nichts fehlt. Es sendet einen User-Agent wie ein Browser, weil viele Websites den Standard von Node.js ablehnen. Es gibt den HTTP-Status aus, wenn die Seite nicht mit 200 antwortet, und macht weiter, weil eine Cloudflare-Challenge-Seite als 403 oder 503 ankommt. Führen Sie es mit der Seitenadresse in Anführungszeichen aus, denn eine Adresse mit einem Und-Zeichen ist in PowerShell ein Parse-Fehler. Der Port 8080 ist der Standard; setzen Sie die Umgebungsvariable CAPSKIP_PORT, die dieses Skript selbst liest, falls Ihrer abweicht. Der Host bleibt in diesem Skript 127.0.0.1, bearbeiten Sie daher für den Server-Modus die Host-Zeile.

$env:CAPSKIP_PORT = "9090"   # only if your port is not 8080
node use.js "https://example.com/login"

Beachten Sie, dass fetch keine Cookies behält und dieses Skript Set-Cookie nur aus der endgültigen Antwort liest, sodass ein Cookie, das bei einer Weiterleitung gesetzt wird, verloren geht. Um es zu behalten, rufen Sie fetch mit der Option redirect auf manual auf, lesen Set-Cookie aus der Antwort mit der Weiterleitung und fordern die Adresse aus Location selbst an. Das Skript selbst steht unten.

'use strict';
const { CapSkip } = require('capskip');
const { detect, solve } = require('./detect');

async function main(pageUrl) {
  const res = await fetch(pageUrl, { headers: { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/130.0 Safari/537.36' } });
  if (!res.ok) console.error(`page answered HTTP ${res.status}`); // a challenge page is a 403 or 503, so keep going
  const html = await res.text();
  const info = detect(html, res.url); // res.url is the final address after any redirect
  console.log(JSON.stringify(info, null, 2));
  if (!info || info.unsupported || info.needsBrowser) return;
  const cookie = res.headers.getSetCookie().map((c) => c.split(';')[0]).join('; ');
  const host = '127.0.0.1', port = Number(process.env.CAPSKIP_PORT || 8080);
  const solver = new CapSkip({ host, port });
  console.log(await solve(info, solver, cookie ? { headers: { cookie } } : {}, { api: `http://${host}:${port}` }));
}

main(process.argv[2]).catch((err) => {
  console.error(`${err.constructor.name}: ${err.message}`);
  process.exit(1);
});

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. Bei reCAPTCHA steht der Token im Feld code; die anderen Methoden verwenden andere Felder, zum Beispiel userAgent neben dem Turnstile- und dem hCaptcha-Token. Ein reCAPTCHA-Token gehört in das Feld g-recaptcha-response des Formulars, ein hCaptcha-Token in h-captcha-response. Im Mock-Lauf gab der Detektor diese Ergebnisse zurück:

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, needsBrowser 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 einfaches fetch findet dann gar kein Captcha, und der Detektor gibt für eine Seite, die eines hat, null 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 wirdnullLaden Sie die Seite in einem Browser und führen Sie das Konsolen-Snippet aus
sitekey, der per JavaScript gesetzt wirdneedsBrowserLesen Sie den Schlüssel aus der aktiven Seite
Vollseiten-Prüfung von CloudflareneedsBrowserLesen Sie die Werte in einem Browser, siehe die cf_clearance-Anleitung. Die Seite kommt als 403 oder 503 an, was use.js meldet und trotzdem noch prüft
CaptchaFox-, Friendly-Captcha- oder Capy-Skript ohne Widget im HTMLnullFühren Sie das Konsolen-Snippet aus oder lesen Sie den Seitenquelltext
Eine Sperrseite (403, 503) ohne Widgetnulluse.js gibt zuerst den Status aus: Eine Sperrseite enthält kein Captcha, das sich finden ließe
Arkose, GeeTest v4unsupportedCapSkip kann diese nicht lösen
hCaptcha Enterprise, das rqdata setztneedsBrowserLesen Sie für jede Aufgabe ein frisches rqdata von der Live-Seite und übergeben Sie es als data an solveHcaptcha
Friendly Captcha ohne Skript-HinweisneedsBrowserPrüfen Sie, welches Skript die Seite lädt
Bild-Captcha ohne das Wort captchanullFü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_UNSOLVABLEEin fehlgeschlagener Job sieht so aus, und die Antwort nennt die Ursache nicht. Eine falsche reCAPTCHA-Version kann hier enden, das habe ich aber an der echten App nicht bestätigtPrü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-Aufruf, ein pollingInterval pro Aufruf oder irgendeine Option pro Aufruf bei der Bild-MethodeVerwenden 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
Fehler von solveDas Ergebnis ist unsupported oder braucht einen 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 in use.js, 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 needsBrowser 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, ist es schneller, 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 Node.js-SDK hat noch keinen hCaptcha-Aufruf, daher sendet solveHcaptcha 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.js 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

Ein Captcha-Detektor in Node.js verdient sich seinen Platz dadurch, dass er zuerst läuft: 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 null, needsBrowser 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 lokaler Captcha-Löser praktisch für einen Crawl, der alle paar Minuten auf eine neue Website trifft. Zum Node.js-SDK selbst siehe die Node.js-Captcha-Solver-Seite.