So lösen Sie Captchas in Windows-Task-Scheduler-Jobs

task scheduler captcha - How to Solve CAPTCHAs in Windows Task Scheduler Jobs

Um Captchas aus dem Windows Task Scheduler zu lösen, registrieren Sie einen Task, der die python.exe Ihrer virtuellen Umgebung direkt ausführt, geben dem Skript eigene Wiederholungsversuche und HTTP-Timeouts mit und lassen das Zeitlimit des Tasks alles abfangen, was sonst noch schiefgeht. Das ist das gesamte Task-Scheduler-Captcha-Setup, und CapSkip läuft auf demselben PC, sodass keine Queue zu hosten ist und keine Kosten pro Lösung entstehen. Die Standardwerte verbergen vier Fallen, und dieser Leitfaden behandelt jede davon. Der Arbeitsordner ist System32, daher scheitert ein relativer Dateiname. Ein einziger hängender Durchlauf beendet den Zeitplan stillschweigend. Ein cmd-Wrapper lässt den eigentlichen Python-Prozess weiterlaufen, wenn der Scheduler den Task beendet. Und die Einstellung für den Neustart bei Fehlern startet kein Skript neu, das mit einem Fehler endet.

Was Sie brauchen

  • CapSkip auf einem Windows-PC, mit eingeschaltetem reCAPTCHA-Löser. CapSkip ist eine Desktop-App, der Task funktioniert also nur, solange sie geöffnet ist.
  • Python 3.10 oder neuer, die Mindestversion des CapSkip SDK. Alles Folgende wurde unter Windows 11 mit Python 3.14.0 und capskip 1.3.0 in Windows PowerShell 5.1 ausgeführt.
  • Eine normale PowerShell-Eingabeaufforderung, keine mit Administratorrechten. Das Registrieren und Ausführen des Tasks erforderte im Test keine Administratorabfrage.
  • 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 Task auf einem anderen Rechner es über die API aufrufen kann. Eingestellt werden beide Modi unter Verbindungseinstellungen.

Schritt 1: Einen Job-Ordner und eine Python-Umgebung anlegen

Legen Sie den Job in einem eigenen Ordner mit einer eigenen virtuellen Umgebung ab. Der Task führt die python.exe in dieser Umgebung über ihren vollständigen Pfad aus, braucht also nichts aus Ihrem PATH und keine aktivierte Shell.

# PowerShell: make a job folder with its own Python environment
mkdir C:\CapSkipJobs
cd C:\CapSkipJobs
python -m venv venv
.\venv\Scripts\python.exe -m pip install capskip==1.3.0 requests

Die SDK-Version ist gepinnt, weil der Timeout-Patch in Schritt 2 in die Interna des SDK eingreift. Testen Sie ihn erneut, bevor Sie auf eine neuere Version wechseln. Task Manager zeigt für jeden Durchlauf zwei python.exe-Prozesse: den Launcher der Umgebung und den eigentlichen Interpreter, den er startet. Das ist normal.

Schritt 2: Einstellungen einlesen, in eine Datei loggen und dem SDK ein Timeout geben

Speichern Sie den Code aus den Schritten 2 bis 4 in der gezeigten Reihenfolge in einer einzigen Datei, C:\CapSkipJobs\job.py. Die gesamte Datei steht am Ende dieses Leitfadens noch einmal. Der Job liest seine Einstellungen aus Umgebungsvariablen und übergibt sie selbst an den Client, weil das SDK sie nicht von sich aus liest. Außerdem hat er Standardwerte, die Datei läuft also ohne jede Einrichtung. CAPSKIP_API_KEY wird nur gebraucht, wenn Sie in CapSkip die Validierung des API-Schlüssels einschalten.

import functools
import logging
import os
import re
import sys
import time
import types

import requests
import capskip.api
from capskip import ApiException, CapSkip, NetworkException, TimeoutException

HOST = os.getenv('CAPSKIP_HOST', '127.0.0.1')
PORT = int(os.getenv('CAPSKIP_PORT', '8080'))
API_KEY = os.getenv('CAPSKIP_API_KEY', 'YOUR_API_KEY')
SITE = os.getenv('SITE_URL', 'https://example.com').rstrip('/')
USER = os.getenv('SITE_USER', 'YOUR_USERNAME')
PASSWORD = os.getenv('SITE_PASSWORD', 'YOUR_PASSWORD')
LOG_FILE = os.getenv('JOB_LOG', r'C:\CapSkipJobs\job.log')
ATTEMPTS = int(os.getenv('JOB_ATTEMPTS', '3'))
PAUSE = int(os.getenv('JOB_PAUSE', '30'))

Task Scheduler hat kein Feld für Umgebungsvariablen, und ein Task sieht keine Variable, die Sie nur in einem Terminal gesetzt haben. Eine mit setx gesetzte Benutzervariable erreicht ihn dagegen, sogar eine, die erst nach der Registrierung des Tasks gesetzt wurde: Der nächste Durchlauf übernimmt sie, ohne Abmeldung. Die Werte liegen als Klartext in der Registry Ihres Kontos, ungefähr so privat wie eine .env-Datei.

# Task Scheduler has no environment field. User variables work:
# the next run of the task sees them, with no sign-out needed.
setx SITE_URL "https://example.com"
setx SITE_USER "YOUR_USERNAME"
setx SITE_PASSWORD "YOUR_PASSWORD"

Als Nächstes kommen die Logdatei und ein Timeout für das SDK. Task Scheduler verwirft die Ausgabe eines Programms, print() zeigt also nirgends etwas an, und der Job schreibt stattdessen eine Logdatei an einem absoluten Pfad. Ist kein Arbeitsordner gesetzt, läuft der Task in C:\Windows\System32, wo ein relativer Dateiname wie job.log mit einem PermissionError scheitert.

os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
logging.basicConfig(filename=LOG_FILE, level=logging.INFO,
                    format='%(asctime)s pid=%(process)d %(levelname)s %(message)s')
log = logging.getLogger('job')

# The SDK sets no HTTP timeouts, so give every CapSkip call one: connect 5 s, read 30 s.
# This reaches into the SDK, so the guard fails loudly if a newer version changes it.
if not hasattr(capskip.api, 'requests'):
    log.error('capskip.api.requests is gone: check the timeout patch')
    sys.exit(1)
capskip.api.requests = types.SimpleNamespace(
    RequestException=requests.RequestException,
    post=functools.partial(requests.post, timeout=(5, 30)),
    get=functools.partial(requests.get, timeout=(5, 30)))

Die letzten Zeilen beheben ein Problem, das der Scheduler nicht lösen kann. Das Python-SDK von CapSkip setzt kein HTTP-Timeout. Nimmt CapSkip also eine Verbindung an und antwortet dann nie, wartet der Aufruf für immer, und die Einstellung recaptchaTimeout zählt nur zwischen zwei Abfragen. Der Patch gibt jedem SDK-Aufruf ein Verbindungs-Timeout von 5 Sekunden und ein Lese-Timeout von 30 Sekunden. Das Einreichen der Aufgabe, auf das nie eine Antwort kommt, scheitert dann nach 30 Sekunden als NetworkException. Eine Ergebnisabfrage, auf die nie eine Antwort kommt, endet kurz nach Ablauf von recaptchaTimeout als TimeoutException: nach 333 Sekunden mit dem Standardwert von 300. Die Schutzprüfung über dem Patch beendet den Job mit einer Zeile im Log, wenn ein neueres SDK dieses Attribut nicht mehr besitzt, statt ihn ohne Timeouts laufen zu lassen. Der Leitfaden zum Lösen von Captchas in APScheduler erklärt denselben Patch ausführlicher.

Schritt 3: Das Formular abrufen, lösen und absenden

Der Job lädt die Login-Seite in einer requests-Session, entnimmt ihr den CSRF-Token und den sitekey und fordert bei CapSkip einen Token an. Eine einzige Session trägt das Cookie, das zum CSRF-Token gehört. Der POST sendet auch den Referer mit, weil manche Frameworks, etwa Django über HTTPS, einen Formular-POST ohne ihn ablehnen. Ersetzen Sie die Pfade und Feldnamen durch die Ihrer Website.

def fetch_login(session):
    """Load the login page: the session cookie, the CSRF token and the sitekey."""
    page = session.get(SITE + '/login', timeout=(5, 30))
    if page.status_code != 200:
        raise RuntimeError('login page returned HTTP %d' % page.status_code)
    csrf = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', page.text)
    key = re.search(r'data-sitekey="([^"]+)"', page.text)
    if not csrf or not key:
        raise RuntimeError('login page has no CSRF token or no sitekey')
    return csrf.group(1), key.group(1)


def solve(sitekey):
    solver = CapSkip(apiKey=API_KEY, host=HOST, port=PORT)
    return solver.recaptcha(sitekey=sitekey, url=SITE + '/login')['code']

solver.recaptcha gibt ein Ergebnis zurück, dessen Feld code der Token ist. Der POST unten setzt ihn neben den übrigen Formularfeldern in das Feld g-recaptcha-response. Ein gescheiterter Login antwortet oft mit Status 200 und erneut dem Login-Formular, deshalb wertet der Job auch eine Seite, die noch ein Passwortfeld enthält, als Ablehnung. Passen Sie diese Prüfung an Ihre Website an, denn ein Job, der für einen abgelehnten Login Erfolg meldet, ist schlimmer als einer, der fehlschlägt.

def submit(session, csrf, token):
    """Post the login form, the way the browser would, Referer included."""
    resp = session.post(SITE + '/login', timeout=(5, 30),
                        headers={'Referer': SITE + '/login'},
                        data={'csrfmiddlewaretoken': csrf, 'username': USER,
                              'password': PASSWORD, 'g-recaptcha-response': token})
    # Many sites answer a failed login with 200 and the form again.
    if resp.status_code != 200 or 'type="password"' in resp.text:
        raise RuntimeError('login rejected: HTTP %d %s' % (resp.status_code, resp.text[:120]))
    log.info('login accepted: %s', resp.text[:40])

Für andere Captcha-Typen rufen Sie die passende SDK-Methode auf und ändern den Namen des Formularfelds im POST. Der Leitfaden zum Lösen von reCAPTCHA v2 in Python behandelt denselben Aufruf in einem einfachen Skript.

Schritt 4: Im Skript wiederholen und mit einem Code beenden

Ein geplanter Job braucht einen Plan für den Fehlerfall, und die eigene Neustart-Einstellung von Task Scheduler ist keiner. RestartCount und RestartInterval starteten einen Task nur dann neu, wenn er nicht starten konnte: Ein fehlendes Programm wurde nach etwa einer und nach etwa zwei Minuten erneut gestartet. Ein Skript, das mit Code 1 oder 3 endete oder mit einer unbehandelten Exception abstürzte, lief einmal und wurde nie neu gestartet. Deshalb steckt die Wiederholung im Skript.

def run():
    session = requests.Session()  # a fresh session means a fresh CSRF token
    csrf, sitekey = fetch_login(session)
    log.info('solving reCAPTCHA v2, sitekey %s', sitekey)
    submit(session, csrf, solve(sitekey))


def is_retryable(exc):
    if isinstance(exc, (NetworkException, TimeoutException, requests.RequestException)):
        return True
    return isinstance(exc, ApiException) and 'UNSOLVABLE' in str(exc)

Vier Fehlschläge sind einen weiteren Versuch wert: ein CapSkip, das nicht antwortet, eine TimeoutException, ERROR_CAPTCHA_UNSOLVABLE (CapSkip konnte diese Aufgabe nicht lösen, ein neuer Versuch kann es) und die Website selbst, die nicht antwortet. Alles andere, etwa ein abgelehnter Login oder eine Seite ohne CSRF-Token, gilt als fatal, weil ein neuer Versuch denselben Fehler nur wiederholen würde.

def main():
    log.info('start, CapSkip at %s:%s, site %s', HOST, PORT, SITE)
    for attempt in range(1, ATTEMPTS + 1):
        try:
            run()
            log.info('done, exit 0')
            return 0
        except Exception as exc:
            retry = is_retryable(exc)
            log.warning('attempt %d/%d failed, %s: %s: %s', attempt, ATTEMPTS,
                        'retryable' if retry else 'fatal', type(exc).__name__, exc,
                        exc_info=not retry)
            if not retry:
                return 1
            if attempt < ATTEMPTS:
                time.sleep(PAUSE)
    log.error('giving up after %d attempts, exit 3', ATTEMPTS)
    return 3


if __name__ == '__main__':
    sys.exit(main())

Der Job macht bis zu drei Versuche im Abstand von 30 Sekunden und beendet sich dann mit einem Code, den Task Scheduler als Last Run Result anzeigt.

Exit-CodeBedeutung
0Der Login war erfolgreich
1Ein Fehler, den eine Wiederholung nicht beheben kann; das Log enthält den Traceback
3Jeder Versuch ist auf einen Fehler gestoßen, der sich von selbst beheben kann

Code 2 fehlt mit Absicht. Python selbst beendet sich mit 2, wenn es die Skriptdatei nicht öffnen kann, eine 2 im Last Run Result wäre also mehrdeutig.

Dieselbe Schleife deckt auch ein CapSkip ab, das noch nicht geöffnet ist. Bei geschlossenem CapSkip scheitert ein Versuch nach etwa 2 Sekunden. In einem Test, bei dem Solver und Website beide verspätet hochkamen, scheiterte der erste Versuch, und der zweite, 30 Sekunden später, gelang. Wenn der Job bei der Anmeldung statt nach einem Zeitplan starten soll, lässt sich ein Anmelde-Trigger für Ihr eigenes Konto ohne Administratorrechte registrieren und erlaubt eine Verzögerung; eine Verzögerung von einer Minute ließ sich problemlos registrieren.

Schritt 5: Den Task registrieren

Dieses PowerShell-Skript registriert den Task für den aktuellen Benutzer. Nichts darin braucht eine Eingabeaufforderung mit Administratorrechten.

$python = 'C:\CapSkipJobs\venv\Scripts\python.exe'
$jobDir = 'C:\CapSkipJobs'

$action = New-ScheduledTaskAction -Execute $python `
    -Argument ('"{0}\job.py"' -f $jobDir) -WorkingDirectory $jobDir
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) `
    -RepetitionInterval (New-TimeSpan -Hours 1)
$settings = New-ScheduledTaskSettingsSet `
    -ExecutionTimeLimit (New-TimeSpan -Minutes 30) `
    -MultipleInstances IgnoreNew `
    -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries

Register-ScheduledTask -TaskName 'CapSkip login job' -Action $action `
    -Trigger $trigger -Settings $settings -Force

Jeder Teil hat seine Berechtigung.

  • Die Aktion führt die python.exe der Umgebung direkt aus, mit dem Skript als absolutem Pfad in Anführungszeichen und dem Job-Ordner als Arbeitsordner. Setzen Sie weder cmd /c noch eine .bat-Datei davor. Der Abschnitt dazu, was das Zeitlimit wirklich stoppt, zeigt, warum.
  • Der Trigger löst in einer Minute zum ersten Mal aus und danach jede Stunde, ohne Enddatum.
  • Das Zeitlimit beträgt 30 Minuten. Der Standardwert sind 72 Stunden, also drei Tage. Ein Durchlauf kann rechnerisch höchstens etwa 18 Minuten dauern: drei Versuche, die jeweils 333 Sekunden lang auf ein stummes CapSkip warten, plus zwei Pausen von je 30 Sekunden. Dreißig Minuten lassen dafür genug Spielraum und befreien einen festgefahrenen Durchlauf trotzdem noch am selben Morgen.
  • IgnoreNew ist für mehrere Instanzen ohnehin der Standard. Es wird trotzdem ausgeschrieben, damit die Entscheidung sichtbar ist.
  • Standardmäßig startet ein Task nicht im Akkubetrieb und wird beendet, wenn der PC auf Akku umschaltet. AllowStartIfOnBatteries und DontStopIfGoingOnBatteries schalten beides ab, was bei einem Laptop wichtig ist. StartWhenAvailable lässt den Scheduler einen verpassten Durchlauf, zum Beispiel weil der PC im Energiesparmodus war, so früh wie möglich starten. Microsoft dokumentiert jede Option in der Referenz zu New-ScheduledTaskSettingsSet.
  • Der Task läuft unter Ihrem Konto, nur solange Sie angemeldet sind. Das passt zu CapSkip, einer Desktop-App, die in Ihrer Sitzung läuft.

Eine weitere Wahl ist python.exe oder pythonw.exe. Auf dem Windows-11-PC, der für diese Tests diente, öffnete python.exe kein sichtbares Fenster, nur einen versteckten Konsolenhost. pythonw.exe hat überhaupt keine Konsole, daher sind sys.stdout und sys.stderr dort None: print() tut nichts, sys.stderr.write löst einen AttributeError aus, und eine unbehandelte Exception hinterlässt nirgends eine Spur. Der Job loggt in eine Datei und funktioniert deshalb mit beiden. Wechseln Sie nur dann zu pythonw.exe, wenn auf Ihrem PC ein Konsolenfenster erscheint.

Schritt 6: Ausführen und das Ergebnis lesen

Der Block unten startet den Task von Hand, wartet, bis er nicht mehr läuft, und zeigt dann, wie er geendet hat und was der Job geloggt hat.

# Run the task now instead of waiting for the trigger
Start-ScheduledTask -TaskName 'CapSkip login job'
Start-Sleep -Seconds 2

# Wait until it is no longer running
while ((Get-ScheduledTask -TaskName 'CapSkip login job').State -eq 'Running') {
    Start-Sleep -Seconds 2
}

# How did it end, and what did the job write?
Get-ScheduledTaskInfo -TaskName 'CapSkip login job' |
    Select-Object LastRunTime, LastTaskResult, NextRunTime
Get-Content C:\CapSkipJobs\job.log -Tail 6

Das Last Run Result ist eine Zahl. Diese Werte werden Ihnen begegnen.

LastTaskResultHexBedeutung
00x0Der Job hat sich mit 0 beendet
10x1Der Job hat sich mit 1 beendet oder ist mit einer unbehandelten Exception abgestürzt
30x3Der Job hat sich nach seinen Versuchen mit 3 beendet
2670090x41301Der Task läuft gerade
2670110x41303Der Task ist noch nicht gelaufen
2670140x41306Der Scheduler hat ihn gestoppt, am Zeitlimit oder mit Stop-ScheduledTask
21479467200x800710E0Ein Trigger wurde ignoriert, weil noch ein Durchlauf lief
21479424020x80070002Der Programmpfad existiert nicht

Warum ein einziger hängender Durchlauf den ganzen Zeitplan stoppt

Das ist der Fehler, den die Standardwerte am schlimmsten machen. Die Standardrichtlinie für mehrere Instanzen ist IgnoreNew, ein Trigger, der auslöst, während der vorherige Durchlauf noch läuft, wird also verworfen. Das Standard-Zeitlimit beträgt 72 Stunden. Zusammen bedeuten sie, dass ein einziger Durchlauf, der nie endet, den Zeitplan drei Tage lang blockiert.

In einem Test lief der Job ohne den Timeout-Patch gegen ein CapSkip, das die Verbindung annahm und nie antwortete, mit einem Trigger pro Minute. Der Task blieb in den fünf beobachteten Minuten im Status Running. Das Job-Log enthielt eine Startzeile und danach nichts. Währenddessen rückte LastRunTime jede Minute vor, und LastTaskResult wechselte auf 0x800710E0, den Code für einen Trigger, der ignoriert wurde, weil noch eine Instanz lief. Der Task sah lebendig aus, und die Zahl wies auf den übersprungenen Trigger statt auf den festhängenden Durchlauf. Das eigene Verlaufsprotokoll von Task Scheduler ist standardmäßig ausgeschaltet, Ihr Job-Log ist daher die einzige Aufzeichnung, die Sie haben werden.

Drei Ebenen verhindern das, und jede fängt etwas ab, das die anderen übersehen. Das SDK-Timeout beendet einen hängenden CapSkip-Aufruf nach 30 bis 333 Sekunden. Die Retry-Schleife macht daraus einen weiteren Versuch oder ein sauberes Beenden mit Exit-Code 3. Das Zeitlimit ist das letzte Sicherheitsnetz für den Hänger, an den Sie nicht gedacht haben. Mit allen dreien beendete ein CapSkip, das auf das Einreichen nie antwortete, den Job nach etwa 152 Sekunden mit Exit-Code 3: drei Timeouts von je 30 Sekunden und zwei Pausen. Eines, das auf die Ergebnisabfragen nie antwortete, kostet bis zu 333 Sekunden pro Versuch, was das Limit von 30 Minuten weiterhin abdeckt.

Was das Zeitlimit wirklich stoppt

Das Zeitlimit ist präzise. Ein Limit von einer Minute beendete den Durchlauf nach 60,2 Sekunden, und Task Scheduler akzeptierte ein Limit von 30 Sekunden und beendete bei 30,5. Was es beendet, hängt von der Form der Aktion ab.

AktionAm Limit beendetLäuft danach weiter
Die python.exe der UmgebungDer Launcher und der InterpreterNichts
cmd /c python job.pycmd.exeDer Launcher und der Interpreter, die ihre Verbindung zu CapSkip weiter halten
Eine .bat-Datei, die python ausführtcmd.exeDer Launcher und der Interpreter, die ihre Verbindung zu CapSkip weiter halten

Dieselbe Aufteilung zeigte sich bei Stop-ScheduledTask. Für einen Captcha-Job ist das wichtig, weil ein verwaister Prozess seine Verbindung zu CapSkip offen hält. Auch Wrapper haben kleinere Fallen. Eine Zeile mit cmd /c und einer Umleitung braucht ein zusätzliches äußeres Paar Anführungszeichen, sonst endet der Job mit Exit 1 und ohne Logeintrag, und das Umleitungsziel muss ein absoluter Pfad sein. Eine .bat-Datei, deren letzte Zeile echo ist, meldet 0, unabhängig davon, was Python zurückgab, beenden Sie sie also mit exit /b %ERRORLEVEL%. Und Unregister-ScheduledTask stoppt keinen Durchlauf, der gerade läuft, rufen Sie daher zuerst Stop-ScheduledTask auf.

Der vollständige Job

Hier sind die sechs Blöcke von job.py zusammen. Speichern Sie die Datei als C:\CapSkipJobs\job.py, denn auf diesen Pfad zeigt der Task.

import functools
import logging
import os
import re
import sys
import time
import types

import requests
import capskip.api
from capskip import ApiException, CapSkip, NetworkException, TimeoutException

HOST = os.getenv('CAPSKIP_HOST', '127.0.0.1')
PORT = int(os.getenv('CAPSKIP_PORT', '8080'))
API_KEY = os.getenv('CAPSKIP_API_KEY', 'YOUR_API_KEY')
SITE = os.getenv('SITE_URL', 'https://example.com').rstrip('/')
USER = os.getenv('SITE_USER', 'YOUR_USERNAME')
PASSWORD = os.getenv('SITE_PASSWORD', 'YOUR_PASSWORD')
LOG_FILE = os.getenv('JOB_LOG', r'C:\CapSkipJobs\job.log')
ATTEMPTS = int(os.getenv('JOB_ATTEMPTS', '3'))
PAUSE = int(os.getenv('JOB_PAUSE', '30'))


os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
logging.basicConfig(filename=LOG_FILE, level=logging.INFO,
                    format='%(asctime)s pid=%(process)d %(levelname)s %(message)s')
log = logging.getLogger('job')

# The SDK sets no HTTP timeouts, so give every CapSkip call one: connect 5 s, read 30 s.
# This reaches into the SDK, so the guard fails loudly if a newer version changes it.
if not hasattr(capskip.api, 'requests'):
    log.error('capskip.api.requests is gone: check the timeout patch')
    sys.exit(1)
capskip.api.requests = types.SimpleNamespace(
    RequestException=requests.RequestException,
    post=functools.partial(requests.post, timeout=(5, 30)),
    get=functools.partial(requests.get, timeout=(5, 30)))


def fetch_login(session):
    """Load the login page: the session cookie, the CSRF token and the sitekey."""
    page = session.get(SITE + '/login', timeout=(5, 30))
    if page.status_code != 200:
        raise RuntimeError('login page returned HTTP %d' % page.status_code)
    csrf = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', page.text)
    key = re.search(r'data-sitekey="([^"]+)"', page.text)
    if not csrf or not key:
        raise RuntimeError('login page has no CSRF token or no sitekey')
    return csrf.group(1), key.group(1)


def solve(sitekey):
    solver = CapSkip(apiKey=API_KEY, host=HOST, port=PORT)
    return solver.recaptcha(sitekey=sitekey, url=SITE + '/login')['code']


def submit(session, csrf, token):
    """Post the login form, the way the browser would, Referer included."""
    resp = session.post(SITE + '/login', timeout=(5, 30),
                        headers={'Referer': SITE + '/login'},
                        data={'csrfmiddlewaretoken': csrf, 'username': USER,
                              'password': PASSWORD, 'g-recaptcha-response': token})
    # Many sites answer a failed login with 200 and the form again.
    if resp.status_code != 200 or 'type="password"' in resp.text:
        raise RuntimeError('login rejected: HTTP %d %s' % (resp.status_code, resp.text[:120]))
    log.info('login accepted: %s', resp.text[:40])


def run():
    session = requests.Session()  # a fresh session means a fresh CSRF token
    csrf, sitekey = fetch_login(session)
    log.info('solving reCAPTCHA v2, sitekey %s', sitekey)
    submit(session, csrf, solve(sitekey))


def is_retryable(exc):
    if isinstance(exc, (NetworkException, TimeoutException, requests.RequestException)):
        return True
    return isinstance(exc, ApiException) and 'UNSOLVABLE' in str(exc)


def main():
    log.info('start, CapSkip at %s:%s, site %s', HOST, PORT, SITE)
    for attempt in range(1, ATTEMPTS + 1):
        try:
            run()
            log.info('done, exit 0')
            return 0
        except Exception as exc:
            retry = is_retryable(exc)
            log.warning('attempt %d/%d failed, %s: %s: %s', attempt, ATTEMPTS,
                        'retryable' if retry else 'fatal', type(exc).__name__, exc,
                        exc_info=not retry)
            if not retry:
                return 1
            if attempt < ATTEMPTS:
                time.sleep(PAUSE)
    log.error('giving up after %d attempts, exit 3', ATTEMPTS)
    return 3


if __name__ == '__main__':
    sys.exit(main())

CapSkip auf einem anderen Rechner betreiben

Der Task und der Solver müssen sich keinen PC teilen. Schalten Sie CapSkip in den Verbindungseinstellungen in den Server-Modus, dann lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP. Richten Sie den Job anschließend auf diese Adresse. Die Zeile mit setx unten wirkt ab dem nächsten Durchlauf.

# Only if CapSkip runs in Server mode on another Windows PC
setx CAPSKIP_HOST "192.168.1.50"
# Only if API key validation is switched on in CapSkip
setx CAPSKIP_API_KEY "YOUR_API_KEY"

Verwenden Sie eine statische öffentliche IP, wenn der Weg über das Internet führt, und aktivieren Sie die Validierung des API-Schlüssels, mit demselben Schlüssel in CAPSKIP_API_KEY. CapSkip öffnet seinen Port beim Start des Server-Modus für jede Adresse. Um den Zugriff auf die Rechner zu beschränken, die Sie erwarten, fügen Sie in der Windows Firewall eine Blockierregel für alle anderen hinzu. Der Solver bleibt Ihr eigener Windows-Rechner, und abgerechnet wird pro Lösung weiterhin nichts.

Häufige Captcha-Fehler mit Task Scheduler

Was Sie sehenUrsacheBeheben
Der Status des Tasks bleibt stundenlang auf RunningEin Aufruf hängt fest, und das Standardlimit von 72 Stunden ist noch weit entferntFügen Sie das SDK-Timeout und ein Zeitlimit im Minutenbereich hinzu
2147946720 (0x800710E0) bei jedem TriggerTrigger werden ignoriert, solange noch ein Durchlauf läuftLesen Sie die letzte Zeile des Job-Logs und beheben Sie dann den Hänger
2147942402 (0x80070002)Der Programmpfad existiert nichtVerwenden Sie den vollständigen Pfad zur python.exe der Umgebung
Last Run Result 2 und ein leeres LogPython konnte job.py nicht öffnen, meist wegen eines relativen Pfads ohne ArbeitsordnerGeben Sie der Aktion einen absoluten Skriptpfad und einen Arbeitsordner
PermissionError bei job.logEin relativer Dateiname wurde innerhalb von C:\Windows\System32 aufgelöstVerwenden Sie absolute Pfade
Exit 3 bei jedem DurchlaufCapSkip läuft nicht, Host oder Port sind falsch, oder jeder Lösevorgang ist gescheitertPrüfen Sie die erste Zeile des Logs und die Adresse in den Verbindungseinstellungen
267014 (0x41306)Der Scheduler hat den Durchlauf am Zeitlimit gestopptSuchen Sie die Stelle, an der das Log endet, und beheben Sie diesen Aufruf
Im Job fehlen EinstellungenDie Variablen wurden nur in einem Terminal gesetztSetzen Sie sie mit setx, oder behalten Sie Standardwerte in der Datei
Nach einem Stopp bleiben zwei python.exe-Prozesse übrigDie Aktion ist cmd /c oder eine .bat-DateiFühren Sie python.exe als Aktion aus

FAQ

Kann der Task laufen, wenn ich abgemeldet bin?

Das Registrieren eines Tasks, der unabhängig davon läuft, ob Sie angemeldet sind, ohne gespeichertes Passwort, scheiterte im Test mit Access is denied (0x80070005) in einer PowerShell-Eingabeaufforderung ohne Administratorrechte. Das Registrieren mit gespeichertem Passwort wurde nicht getestet, ebenso wenig ein Standardbenutzerkonto. In beiden Fällen muss CapSkip geöffnet sein, um zu antworten, und es ist eine Desktop-App, daher ist das übliche Setup ein PC, auf dem Sie angemeldet bleiben. Wenn Sie den Solver auf einem Rechner brauchen, den niemand benutzt, betreiben Sie CapSkip dort im Server-Modus und richten den Job darauf aus.

Kann der nächste Trigger einen hängenden Durchlauf ersetzen, statt ignoriert zu werden?

Ja, mit der Richtlinie StopExisting. New-ScheduledTaskSettingsSet bietet nur Parallel, Queue und IgnoreNew an, setzen Sie den Wert also von Hand am Settings-Objekt:

# New-ScheduledTaskSettingsSet cannot set this policy, so set it on the
# $settings object from Step 5 by hand, then update the task.
$settings.CimInstanceProperties['MultipleInstances'].Value = 3
Set-ScheduledTask -TaskName 'CapSkip login job' -Settings $settings

Der exportierte Task zeigt dann StopExisting. Mit python.exe als Aktion beendete jeder Trigger den alten Durchlauf und startete einen neuen. Mit cmd /c als Aktion hinterließ jedes Beenden einen verwaisten Prozess. Parallel startete jede Minute einen neuen Durchlauf, während der hängende bestehen blieb, und Queue wartete weiter hinter ihm.

Funktioniert das auch für ein kompiliertes C#-Programm?

Die Scheduler-Seite ist dieselbe. Richten Sie die Aktion auf Ihre exe, setzen Sie den Arbeitsordner, behalten Sie das Zeitlimit bei und verwenden Sie einen absoluten Log-Pfad. Die Wiederholungsversuche und Exit-Codes gehören dann in Ihren eigenen Code. Soll das Programm den ganzen Tag laufen, statt nach einem Zeitplan gestartet zu werden, behandelt der Leitfaden zum Lösen von Captchas in einem .NET Worker Service genau diese Variante.

Wann passt eine Queue besser als Task Scheduler?

Wenn anderer Code die Arbeit erzeugt oder wenn jedes Stück Arbeit einen Absturz überstehen und einzeln wiederholt werden muss. Task Scheduler startet nach einem Zeitplan einen frischen Prozess und weiß nichts über einzelne Jobs. Eine Queue wie Huey hält jeden Task in einer Datei und wiederholt ihn, und der Leitfaden zum Lösen von Captchas in Huey-Tasks zeigt ein Setup, das unter Windows mit einer einzigen SQLite-Datei läuft.

Die Kurzfassung

Ein Task-Scheduler-Captcha-Job braucht mehr als den Lösevorgang. Führen Sie die python.exe der Umgebung direkt aus, mit einem Arbeitsordner, einem absoluten Log-Pfad und einem absoluten Skriptpfad. Geben Sie den HTTP-Aufrufen des SDK ein Timeout und wiederholen Sie innerhalb des Skripts, weil die Neustart-Einstellung Exit-Codes ignoriert. Setzen Sie ein Ausführungszeitlimit im Minutenbereich, denn mit dem Standardwert von 72 Stunden blockiert ein einziger hängender Durchlauf jeden späteren Trigger.

Ein Job, der jede Stunde läuft, kommt auf etwa 720 Lösungen im Monat. Wenn Sie sie mithilfe lokaler Captcha-Löser auf Ihrem eigenen Windows-Rechner laufen lassen, wird aus dieser Zahl nie eine Rechnung.