So lösen Sie Captchas in einem Apify Actor (Node.js)

apify captcha - How to Solve CAPTCHA Inside an Apify Actor (Node.js)

Ein Captcha-Schritt in Apify besteht aus den üblichen drei Zügen, plus einem, der zur Plattform gehört. Den sitekey lesen, ihn lösen, das Token mit dem Formular zurücksenden. Der zusätzliche Zug ist die Entscheidung, wo der Löser wohnt, denn ein Actor läuft nicht auf Ihrem Laptop. Er läuft in einem Container auf der Infrastruktur von Apify, die Loopback-Adresse darin gehört also diesem Container und sonst nichts. Treffen Sie diese eine Entscheidung richtig, und der Rest sind zwanzig Zeilen.

Was Sie brauchen

  • Node.js 18 oder neuer, die Apify CLI und ein Apify-Konto.
  • Die Pakete apify und capskip, installiert im Actor.
  • CapSkip im Server-Modus auf einer Maschine, die der Actor erreichen kann, mit statischer öffentlicher IP und eingeschalteter Schlüsselvalidierung. Der Lokal-Modus funktioniert weiterhin, solange Sie auf Ihrer eigenen Maschine entwickeln. Beide Modi sind beschrieben unter Verbindungseinstellungen.
# Scaffold an Actor, then add the solver client.
apify create captcha-actor -t getting_started_node
cd captcha-actor
npm install capskip

Der Server-Modus ist hier nicht optional

Das ist der Punkt, an dem die Leute stolpern, deshalb kommt er zuerst. Actors laufen auf den Workern von Apify. Wenn Ihr Actor-Code eine Verbindung zu 127.0.0.1:8080 öffnet, spricht er mit seinem eigenen Container, in dem kein Löser läuft, und der Aufruf scheitert mit einem Verbindungsfehler, der aussieht, als wäre der Löser abgestürzt. Abgestürzt ist nichts. Die Adresse war schlicht auf der falschen Maschine lokal.

Genau dafür hat CapSkip zwei Verbindungsmodi. Der Lokal-Modus bindet an 127.0.0.1 und antwortet nur diesem Gerät. Der Server-Modus bindet an Ihre Netzwerkadresse oder öffentliche IP, sodass ein anderer Rechner, ein VPS oder eine gehostete Plattform wie Apify denselben Löser über die API aufrufen kann. Eine statische öffentliche IP hält die Adresse zwischen den Läufen stabil.

Einmal klar gesagt, weil die Frage aufkommt: Der Server-Modus macht aus CapSkip keinen verbrauchsabhängigen Cloud-Dienst. Es bleibt Ihre Maschine, und es bleibt unbegrenzt. Es ändert sich nur, auf welcher Schnittstelle er lauscht. Sobald er auf einer Netzwerkadresse lauscht, schalten Sie die Schlüsselvalidierung ein und geben Sie dem Actor einen eigenen Schlüssel, damit dieser Schlüssel widerrufen werden kann, ohne alles andere zu stören.

Schritt 1: die Adresse des Lösers als geheime Eingabe deklarieren

Schreiben Sie den Host nicht fest in den Code. Das Input-Schema von Apify unterstützt verschlüsselte Felder, und das ist der richtige Ort für eine Löser-Adresse und ihren Schlüssel, denn so werden die Werte pro Lauf gesetzt, statt in einen Build eingebacken zu sein. Die Verschlüsselung funktioniert mit den Editoren textfield, textarea und hidden.

{
  "title": "CAPTCHA actor input",
  "type": "object",
  "schemaVersion": 1,
  "properties": {
    "targetUrl": {
      "title": "Target URL",
      "type": "string",
      "editor": "textfield"
    },
    "solverHost": {
      "title": "Solver host",
      "type": "string",
      "editor": "textfield",
      "isSecret": true
    },
    "solverKey": {
      "title": "Solver API key",
      "type": "string",
      "editor": "textfield",
      "isSecret": true
    }
  },
  "required": ["targetUrl", "solverHost"]
}

Diese Datei liegt im Ordner .actor neben actor.json, und die Werte kommen über das Input-Objekt in Ihrem Code an.

Schritt 2: den Host zur Laufzeit wählen

Sie wollen einen Actor, der an beiden Orten funktioniert: Er spricht mit 127.0.0.1, solange Sie ihn lokal ausführen, und mit Ihrem Server, sobald er deployt ist. Das SDK bietet genau dafür einen Boolean. Actor.isAtHome() liefert true, wenn der Code auf der Apify-Plattform läuft, und false, wenn nicht.

// npm install apify capskip
import { Actor } from 'apify';
import { CapSkip } from 'capskip';

await Actor.init();
const input = await Actor.getInput();

// Local run talks to the loopback address. A platform run
// talks to the server address that came in as a secret.
const solver = new CapSkip({
  host: Actor.isAtHome() ? input.solverHost : '127.0.0.1',
  port: 8080,
  apiKey: input.solverKey,
});

Sonst muss nichts im Actor von diesem Unterschied wissen. Derselbe Codepfad deckt beides ab, und ein fehlgeschlagenes Deployment bedeutet nicht mehr, Konstanten zu bearbeiten.

Schritt 3: den sitekey lesen, ihn lösen, das Token senden

Der sitekey sitzt als data-sitekey-Attribut im Host-Dokument, ein einfacher Fetch und ein regulärer Ausdruck holen ihn also, ohne einen Browser zu starten. Auf einer kostenpflichtigen Plattform zählt das: Ein Actor ohne Browser braucht deutlich weniger Speicher, und Apify rechnet nach Speicher mal Zeit ab. Actor.fail() unten beendet den Lauf, statt zurückzukehren, deshalb darf der Code danach davon ausgehen, dass der Treffer geklappt hat.

// Fetch the form page and lift the sitekey out of it.
const html = await (await fetch(input.targetUrl)).text();
const match = html.match(/data-sitekey="([^"]+)"/);

if (!match) {
  await Actor.fail('No sitekey on the page. Did the widget render?');
}

const result = await solver.recaptcha(match[1], input.targetUrl);
const token = result.code;   // the g-recaptcha-response value

Senden Sie dann das Formular mit dem Token in dem Feld ab, das die Seite erwartet. Bei reCAPTCHA v2 heißt das versteckte Textarea g-recaptcha-response, und die meisten Formulare senden es unter genau diesem Namen. Übergibt die Seite das Token stattdessen an einen Callback, prüfen Sie, was der Callback tatsächlich absendet.

// The token travels as an ordinary form field.
const body = new URLSearchParams({
  email: 'someone@example.com',
  'g-recaptcha-response': token,
});

const posted = await fetch(input.targetUrl, { method: 'POST', body });
await Actor.pushData({ url: input.targetUrl, status: posted.status });

Turnstile und GeeTest haben eigene Methoden auf demselben Client, und beide haben dieselbe Form. Turnstile liefert außerdem einen User Agent zurück, der auf Challenge-Seiten zusammen mit dem Token gesendet werden muss. Die vollständigen Parameterlisten stehen in der CapSkip-API-Dokumentation.

Vollständiges lauffähiges Beispiel

Der komplette Actor als src/main.js. Ein Actor ist ein ES-Modul mit Top-Level-await, es gibt also keine Wrapper-Funktion, und Actor.exit() am Ende ist das, was das Dataset schreibt und den Lauf sauber beendet.

// npm install apify capskip
import { Actor } from 'apify';
import { CapSkip, NetworkException, TimeoutException } from 'capskip';

await Actor.init();

const input = await Actor.getInput();
const solver = new CapSkip({
  host: Actor.isAtHome() ? input.solverHost : '127.0.0.1',
  port: 8080,
  apiKey: input.solverKey,
});

try {
  const html = await (await fetch(input.targetUrl)).text();
  const match = html.match(/data-sitekey="([^"]+)"/);
  if (!match) throw new Error('No sitekey found on the page.');

  const result = await solver.recaptcha(match[1], input.targetUrl);

  const body = new URLSearchParams({
    'g-recaptcha-response': result.code,
  });
  const posted = await fetch(input.targetUrl, { method: 'POST', body });

  await Actor.pushData({ url: input.targetUrl, status: posted.status });
} catch (err) {
  if (err instanceof NetworkException) {
    await Actor.fail('Cannot reach the solver. Check Server mode and the host.');
  }
  if (err instanceof TimeoutException) {
    await Actor.fail('The solve outlasted recaptchaTimeout.');
  }
  throw err;
}

await Actor.exit();

Die beiden verbindungsnahen Exceptions getrennt abzufangen ist die sechs Zeilen wert. Actor.fail() ist Actor.exit() mit Exit-Code 1 und einer angehängten Meldung, der Lauf endet also als FAILED, mit einem Satz im Log, der Ihnen sagt, welche Hälfte des Aufbaus kaputt ist. Ohne das ergeben ein Löser, der schlicht nicht erreichbar ist, und eine Lösung, die wirklich nicht gelesen werden konnte, denselben roten Lauf.

Vor dem Deployment lokal ausführen

Führen Sie den Actor zuerst auf Ihrer eigenen Maschine aus, mit dem Löser im Lokal-Modus. isAtHome() liefert dort false, der Code greift also zu 127.0.0.1, ohne dass Sie etwas ändern, und Sie können nachweisen, dass das Lesen des sitekeys und das Absenden des Formulars funktionieren, bevor der Netzwerksprung dazukommt.

# Reads INPUT from storage/key_value_stores/default.
apify run

Klappt das, schalten Sie CapSkip in den Server-Modus, notieren Sie die Adresse, auf der er jetzt lauscht, und tragen Sie diese Adresse auf der Plattform in das Feld solverHost ein. Geändert hat sich nur ein einziger String.

Häufige Fehler und was sie bedeuten

Was Sie sehenUrsacheBeheben
NetworkException auf der Plattform, nie lokalDer Actor hat 127.0.0.1 aufgerufen und seinen eigenen Container erreichtDen Löser in den Server-Modus schalten und seine Adresse übergeben
ERROR_WRONG_USER_KEYDie Schlüsselvalidierung ist an, und der Actor hat den falschen Schlüssel gesendetDen Schlüssel als geheime Eingabe setzen und aus dem Input lesen
ERROR_GOOGLEKEYDer reguläre Ausdruck hat nichts getroffen, und ein leerer Key ging hinausDen Treffer prüfen, bevor Sie eine Lösung dafür verbrauchen
Lauf-Status TIMED-OUTDas Timeout des Laufs ist kürzer als Abrufen plus Lösen plus AbsendenDas Timeout in den Standard-Laufoptionen des Actors anheben
TimeoutExceptionDie Lösung hat recaptchaTimeout überdauertÜber den Standardwert von 300 Sekunden anheben
Das Formular weist einen Token ab, der sauber gelöst wurdeDas Token ist zwischen dem Lösen und dem Absenden abgelaufenUnmittelbar vor dem Absenden lösen, nicht zu Beginn des Laufs

FAQ

Kann ein Apify Actor wirklich einen Löser auf meiner eigenen Maschine erreichen?

Ja, über dieselbe API, die er für jeden internen Dienst nutzen würde. Der Server-Modus lässt CapSkip auf Ihrer Netzwerkadresse oder öffentlichen IP statt auf der Loopback-Adresse lauschen, der Actor ruft ihn also auf wie jeden anderen HTTP-Endpunkt. Eine statische öffentliche IP ist empfehlenswert, damit die Adresse zwischen den Läufen nicht wandert, und die Schlüsselvalidierung sollte an sein, bevor Sie den Port öffnen.

Funktioniert das auch in einem Crawlee-Crawler auf Apify?

Ja, und die drei Züge sind identisch. Der Unterschied liegt darin, wo sie sitzen: Das Lesen des sitekeys und das Injizieren des Tokens gehören in den Request-Handler, und der Client des Lösers wird einmal außerhalb davon erzeugt, damit sich alle Requests eine Instanz teilen. Das crawlerspezifische Detail, samt der Frage, warum Sie eine Lösung nicht in eine eigene Retry-Schleife packen sollten, behandelt die Crawlee-Captcha-Anleitung.

Sollte ich über Apify Proxy lösen?

Nur wenn die Seite die IP bewertet, die gelöst hat. Proxy-Unterstützung gibt es für reCAPTCHA, Turnstile und GeeTest, und sie zählt, wenn das Token gegen die Adresse geprüft wird, die es angefordert hat. Übergeben Sie den Proxy am Lösungsaufruf, statt den gesamten Löser darüber zu leiten, damit Abruf und Lösung unterschiedliche Ausgänge nutzen können, wenn Sie genau das wollen. Die Abwägungen stehen im Leitfaden zur CAPTCHA-Proxy-Rotation.

Wie viel Speicher sollte der Actor bekommen?

Weniger, als Sie denken, wenn Sie den Browser weglassen. Der Actor oben holt HTML, wartet auf einen Netzwerkaufruf und sendet ein Formular ab, er ist also die meiste Zeit untätig und braucht nichts, was einem Chromium-Fußabdruck nahekommt. Greifen Sie erst dann zum Browser, wenn die Seite ihren sitekey ohne ausgeführtes JavaScript nicht herausrückt. Das Lösen selbst passiert so oder so auf der Maschine des Lösers, Ihr Actor läuft dabei allerdings weiter.

Die Kurzfassung

Versetzen Sie den Löser in den Server-Modus, hinterlegen Sie seine Adresse und seinen Schlüssel als verschlüsselte Eingaben und lassen Sie Actor.isAtHome() zwischen dieser Adresse und 127.0.0.1 wählen. Lesen Sie dann den sitekey, lösen Sie ihn und senden Sie das Token als g-recaptcha-response. Für das größere Node-Bild samt Puppeteer und Playwright siehe die Node.js-Captcha-Solver-Seite. Den Crawling-Teil desselben Problems finden Sie auf der Web-Scraping-Seite.

Eine Konsequenz sollte klar benannt werden, bevor Sie einen Actor hochskalieren. Weil diese Captcha-Umgehung auf Hardware läuft, die Ihnen ohnehin gehört, bewegt der Sprung von zehn auf tausend Läufe Ihre Apify-Rechnung und lässt Ihre Rechnung fürs Lösen dort, wo sie war.