So lösen Sie ALTCHA in Scrapy mit einer einzigen Inline-Anfrage

Um ALTCHA in Scrapy zu lösen, lesen Sie die Challenge-Adresse am Element altcha-widget ab, rufen sie mit einer Inline-Anfrage aus demselben Callback ab, übergeben das JSON an die Methode altcha von AsyncCapSkip und senden das Formular mit dem Token in dem Feld ab, das das Widget nennt, standardmäßig altcha. ALTCHA ist Proof of Work und kein Bild, nichts davon braucht also einen Browser, und ein einfacher Scrapy-Spider genügt. Die einzige Scrapy-spezifische Falle übersieht man leicht: Ein Challenge-Endpunkt liefert jedes Mal ein neues Dokument unter derselben URL, und der Duplikatfilter von Scrapy verwirft die zweite Anfrage daran stillschweigend. Dieser Leitfaden behandelt das Widget, den Abruf, das Lösen und das Absenden, mit einem Spider, den Sie ausführen können.
Was Sie brauchen
- Scrapy 2.14 oder neuer, wegen der Methode für Inline-Anfragen, die unten verwendet wird. Scrapy läuft seit 2.13 standardmäßig auf dem asyncio-Reactor, und genau deshalb kann ein Callback den asynchronen Client mit await aufrufen. Ein Abschnitt gegen Ende behandelt ältere Versionen.
- Version 1.2.0 oder neuer des Python-Pakets capskip, also das Release, das die Methode altcha hinzugefügt hat, sowie CapSkip 1.3.0 oder neuer auf einem Windows-Rechner.
- Das Paket form2request, das Scrapy inzwischen zum Erstellen von Formularübermittlungen empfiehlt.
- Die Seite mit dem Formular. Alles andere, einschließlich der Challenge, stammt von dieser Seite, und Schritt 1 zeigt, wo.
- Eine Adresse für den Solver. Der Local-Modus antwortet auf 127.0.0.1 nur für dieses Gerät; der Server-Modus lauscht auf Ihrer Netzwerkadresse oder öffentlichen IP, damit ein Crawler auf einem anderen Rechner ihn über die API aufrufen kann. Beide finden Sie unter Verbindungseinstellungen, und ein Abschnitt weiter unten erklärt, wann Sie wechseln sollten.
# pip install scrapy capskip form2request pip install scrapy capskip form2request
Schritt 1: Die Challenge am Widget ablesen
ALTCHA hat keinen sitekey. Was Sie brauchen, um ALTCHA in Scrapy zu lösen, ist die Challenge oder die Adresse, von der sie kommt, und das Widget-Element trägt eines von beiden. Welches Attribut sie enthält, hängt von der Widget-Generation ab, lesen Sie also alle drei.
| Widget | Attribut | Enthält |
|---|---|---|
| v1 und v2 | challengeurl oder challengejson | challengeurl nennt den Endpunkt, der eine Challenge ausliefert, oft als relativen Pfad; challengejson trägt das Challenge-Dokument selbst |
| v3 und neuer | challenge | Entweder diesen Endpunkt oder das Challenge-Dokument selbst |
# Inside a spider callback; response is the page with the form.
widget = response.css("altcha-widget")
source = (widget.attrib.get("challenge")
or widget.attrib.get("challengejson")
or widget.attrib.get("challengeurl"))
# The token goes in a field named by the widget, altcha by default.
field = widget.attrib.get("name", "altcha")
# A v3 challenge or a v1/v2 challengejson carries the document inline.
inline = source.lstrip().startswith("{")Beachten Sie auch das Attribut name. Das Widget schreibt seine Payload in ein verstecktes Input-Feld mit diesem Namen, standardmäßig altcha, aber eine Website kann ihn ändern. Das Auslesen kostet eine Zeile und erspart Ihnen ein Absenden, das das eigentliche Feld stillschweigend leer lässt. Einige Deployments erzeugen die Challenge im eigenen Skript der Seite, statt sie über einen Endpunkt auszuliefern, und dann gibt es kein Attribut zum Auslesen. Der Network-Tab zeigt, welche Variante Sie vor sich haben.
Schritt 2: Die Challenge innerhalb des Callbacks abrufen
Zeigt das Widget auf einen Endpunkt, rufen Sie ihn aus dem Callback ab, in dem Sie sich ohnehin befinden, und zwar mit der Methode download_async der Engine, die die Scrapy-Dokumentation unter Inline-Anfragen beschreibt. Die Anfrage läuft durch die Downloader-Middlewares, daher gelten der Cookie-Jar, der User-Agent und die Proxy-Einstellungen des Spiders auch für sie, so wie für die Seite. Durch den Scheduler läuft sie nicht, und genau darum geht es.
# The page's jar and proxy; no scheduler, no dupefilter.
meta = {"dont_cache": True, "allow_offsite": True}
for key in ("cookiejar", "proxy"):
if key in response.meta:
meta[key] = response.meta[key]
reply = await self.crawler.engine.download_async(
scrapy.Request(response.urljoin(source), meta=meta,
headers={"Referer": response.url})
)
if reply.status != 200:
raise ValueError(f"challenge endpoint answered {reply.status}")
challenge_json = reply.textDarum ist es wichtig, den Scheduler zu umgehen. Die naheliegende Alternative, einen Request für die Challenge mit eigenem Callback per yield auszugeben, funktioniert beim ersten Formular und verliert das zweite. Jeder Abruf des Endpunkts liefert eine neue Challenge, aber die URL ändert sich nie, daher hält der Duplikatfilter von Scrapy die zweite Anfrage für eine bereits gestellte und verwirft sie. Es gibt keinen Fehler. Der Filter protokolliert sein erstes Verwerfen einmalig auf Debug-Level, erhöht einen Zähler in den Crawl-Statistiken, und das Formular dahinter wird einfach nie abgesendet. Die Inline-Anfrage erreicht den Filter nie.
Der Rest dieses Blocks kümmert sich um Details, die sonst der Scheduler für Sie erledigen würde. Zustand pro Anfrage wird nicht von selbst übernommen, daher kopiert die Schleife einen Schlüssel cookiejar oder proxy aus dem meta der Seite, wenn die Seite mit einem solchen abgerufen wurde. Der Referer-Header wird von Hand gesetzt, weil die Middleware, die ihn hinzufügt, auf der Spider-Seite läuft. Liegt der Challenge-Endpunkt auf einer anderen Domain, etwa bei einem gehosteten ALTCHA-Dienst, und setzt der Spider allowed_domains, würde der Offsite-Filter den Abruf blockieren, und allow_offsite lässt genau diese eine Anfrage durch. Die Statusprüfung gibt es, weil ein 403 vom Endpunkt hier als gewöhnliche Response zurückkommt: Die Middleware, die Fehlerstatus in Fehlschläge umwandelt, läuft auf der Spider-Seite, und die überspringt eine Inline-Anfrage. Und dont_cache hält den HTTP-Cache von Scrapy von der Challenge fern, falls Sie ihn aktiviert haben. Eine gecachte Challenge ist eine abgelaufene, und CapSkip lehnt eine abgelaufene Inline-Challenge sofort ab, statt CPU für einen Token aufzuwenden, den die Website zurückweisen wird. Ohne das Flag bekommt bei einem zweiten Durchlauf jede Seite, die sich den Endpunkt teilt, dieselbe gespeicherte Challenge. Dieselbe Logik gilt für die Formularseite. Eine gecachte Seite spielt einen alten Anti-Forgery-Token und ein altes Sitzungs-Cookie erneut ab, bei einer Inline-Challenge auch eine alte Challenge. Setzen Sie bei aktiviertem Cache also dont_cache auch auf die Formularseiten, oder lassen Sie den Cache für diesen Spider aus.
Sie könnten den Endpunkt stattdessen als challenge_url übergeben und CapSkip die Challenge selbst abrufen lassen. Für einen öffentlichen Endpunkt ist das in Ordnung. Aber der Abruf durch CapSkip trägt keines der Cookies Ihres Spiders, und manche Websites geben Challenges nur an die Sitzung aus, die das Formular geladen hat. Der Abruf über Scrapy hält eine einzige Sitzung von Anfang bis Ende.
Schritt 3: Lösen, ohne den Crawl zu blockieren
Verwenden Sie AsyncCapSkip und definieren Sie den Callback mit async def. Im Python-Paket ist diese Klasse ein echter asyncio-Client und kein Alias, ein await darauf gibt die Kontrolle also an Scrapy zurück, während das Lösen läuft, und alle anderen Anfragen laufen weiter. Die synchrone Klasse CapSkip würde den gesamten Crawl für die Dauer jedes Lösens anhalten. Ausführlich erklärt dieses Problem der Leitfaden zur Scrapy-Captcha-Middleware.
from capskip import AsyncCapSkip solver = AsyncCapSkip(host="127.0.0.1", port=8080) # Pass the document exactly as the endpoint sent it. result = await solver.altcha(url=response.url, challenge_json=challenge_json) token = result["token"] # base64 payload for the form field
Weil die Challenge inline mitgeliefert wird, stellt CapSkip überhaupt keine Netzwerkanfrage. Es hasht, bis es die Antwort findet, was meist Millisekunden dauert, und der Client pollt sofort und eine Viertelsekunde später erneut, sodass ein typisches Lösen nach etwa einer Viertelsekunde zurückkommt. Der Aufruf wartet bis zu defaultTimeout, 120 Sekunden, denn das ist CPU-Arbeit und keine Browser-Sitzung. Zwei Challenge-Algorithmen, Argon2id und scrypt, werden abgelehnt statt versucht, und diese Ablehnung kommt in unter einer Sekunde als ApiException an. Die häufigeren PBKDF2- und SHA-Verfahren, einschließlich der von ALTCHA empfohlenen Voreinstellung, sind abgedeckt.
Übergeben Sie den Text der Antwort so, wie er angekommen ist; Sie müssen ihn nicht parsen. Was sich nicht ändern darf, ist der Token: Er ist base64 eines JSON-Dokuments, dessen Felder der Server der Website signiert hat, ein gekürzter, dekodierter oder neu kodierter Token scheitert also an der Verifizierung.
Schritt 4: Das Formular mit dem Token absenden
Das versteckte Input-Feld, das das Widget füllt, existiert in dem HTML, das Scrapy heruntergeladen hat, nicht. Das Widget erzeugt es im Browser, nachdem es gelaufen ist, daher kann nichts, was das ausgelieferte HTML liest, es finden. Fügen Sie es selbst hinzu, unter dem Namen aus Schritt 1. Das Beispiel baut das Absenden mit form2request, das Scrapy anstelle von FormRequest.from_response empfiehlt: Seit Scrapy 2.16 protokolliert diese ältere Methode bei jedem Aufruf eine Deprecation-Warnung.
from form2request import form2request
form = response.xpath("//form[.//altcha-widget]")
data = {"email": "[email protected]", field: token}
yield form2request(form, data).to_scrapy(
callback=self.after_submit,
priority=10,
meta={"handle_httpstatus_all": True},
)form2request behält, was die Seite bereits in das Formular gesetzt hat, etwa einen Anti-Forgery-Token, und drückt den ersten Submit-Button, sodass die Anfrage aussieht wie die eines Browsers. Der XPath wählt das Formular aus, das das Widget enthält, was auf Seiten wichtig ist, die zusätzlich ein Suchfeld haben. handle_httpstatus_all lässt den Callback ein abgelehntes Absenden sehen, das Scrapy sonst verwerfen würde, bevor es dort ankommt. Die erhöhte Priorität schiebt das Absenden vor alles, was noch im Scheduler wartet, denn Challenges laufen ab. Manche Zeitfenster sind kaum zwei Minuten lang, und ein Token, der hinter einem langen Crawl wartet, kann ablaufen, bevor er gesendet wird.
Betrachten Sie jeden Token als gültig für ein einziges Absenden. Lehnt die Website das Formular ab, beginnen Sie mit einer neuen Challenge wieder bei Schritt 2, statt dieselbe Anfrage zu wiederholen. Manche Integrationen senden die Payload in einem JSON-Body oder einem Cookie statt in einem Formularfeld, senden Sie das Formular also einmal von Hand mit geöffneten DevTools ab und bilden Sie nach, was die Seite sendet.
Den Löser woanders betreiben
Die Beispiele verwenden 127.0.0.1, weil das richtig ist, solange der Spider und CapSkip auf demselben Rechner liegen. Bei einem Crawler, der auf Scrapyd auf einem anderen Rechner, auf einem VPS oder auf einer gehosteten Scrapy-Plattform bereitgestellt ist, zeigt Loopback auf ihn selbst, und der erste Lösungsversuch löst eine NetworkException aus. Schalten Sie CapSkip in den Server-Modus, dann lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, sodass der Spider es von überall, wo Sie es erlauben, über die API erreichen kann. Verwenden Sie eine statische öffentliche IP, wenn der Weg über das Internet führt, aktivieren Sie die Validierung des API-Schlüssels und beschränken Sie den Port mit einer Regel in der Windows Firewall auf die Adressen, die Sie erwarten. Es bleibt Ihr eigener Windows-Rechner, und abgerechnet wird pro Lösung weiterhin nichts.
Der Client liest Umgebungsvariablen nicht von selbst. Lesen Sie CAPSKIP_HOST und CAPSKIP_API_KEY im Spider aus und übergeben Sie die Werte an den Konstruktor, wie es das vollständige Beispiel tut, damit derselbe Code auf Ihrem Schreibtisch und auf dem Server läuft.
Vollständiges lauffähiges Beispiel
# pip install scrapy capskip form2request
import os
import scrapy
from capskip import AsyncCapSkip, CapSkipError
from form2request import form2request
class SignupSpider(scrapy.Spider):
name = "signup"
start_urls = ["https://example.com/signup"]
solver = AsyncCapSkip(
apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
port=8080,
)
async def parse(self, response):
widget = response.css("altcha-widget")
source = (widget.attrib.get("challenge")
or widget.attrib.get("challengejson")
or widget.attrib.get("challengeurl"))
if not source:
self.logger.warning("no ALTCHA challenge on %s", response.url)
return
field = widget.attrib.get("name", "altcha")
if source.lstrip().startswith("{"):
challenge_json = source
else:
# Inline request: the page's jar and proxy, no dupefilter.
meta = {"dont_cache": True, "allow_offsite": True}
for key in ("cookiejar", "proxy"):
if key in response.meta:
meta[key] = response.meta[key]
reply = await self.crawler.engine.download_async(
scrapy.Request(response.urljoin(source), meta=meta,
headers={"Referer": response.url})
)
if reply.status != 200:
self.logger.error("challenge endpoint answered %s", reply.status)
return
challenge_json = reply.text
try:
result = await self.solver.altcha(
url=response.url, challenge_json=challenge_json)
except CapSkipError as exc:
# Expired challenge, unsupported algorithm, or CapSkip unreachable.
self.logger.error("ALTCHA not solved on %s: %r", response.url, exc)
return
form = response.xpath("//form[.//altcha-widget]")
data = {"email": "[email protected]", field: result["token"]}
yield form2request(form, data).to_scrapy(
callback=self.after_submit,
priority=10,
meta={"handle_httpstatus_all": True},
)
def after_submit(self, response):
yield {"url": response.url, "status": response.status}Führen Sie ihn mit scrapy runspider und dem Dateinamen aus, oder übernehmen Sie die Klasse in ein Projekt. Der Spider beherrscht beide Widget-Generationen, ruft die Challenge innerhalb der Sitzung der Seite ab und gibt pro abgesendetem Formular ein Item mit dem Status aus, mit dem die Website geantwortet hat, Ablehnungen eingeschlossen. Weil der Challenge-Abruf den Scheduler umgeht, bekommen Seiten, die sich einen Challenge-Endpunkt teilen, jeweils eine eigene Challenge. Den rohen Endpunkt hinter der Methode altcha finden Sie in der API-Referenz, und den einfachen Python-Aufruf ohne Scrapy drumherum zeigt der Python-Leitfaden zu ALTCHA.
Mit Scrapy-Versionen vor 2.14
Vor 2.14 gibt es kein download_async, aber ab Scrapy 2.6 funktioniert derselbe Inline-Abruf über engine.download. Abgewartet wird er per await mit einem Helfer, der das Ergebnis in etwas verwandelt, worauf eine Coroutine warten kann.
from scrapy.utils.defer import maybe_deferred_to_future
# Scrapy 2.6 to 2.13: the same inline fetch, through engine.download.
reply = await maybe_deferred_to_future(self.crawler.engine.download(
scrapy.Request(response.urljoin(source),
meta={"dont_cache": True, "allow_offsite": True})
))In diesen Versionen baut FormRequest.from_response das Absenden ohne Warnung, Sie können es also anstelle von form2request verwenden: Übergeben Sie denselben XPath als formxpath und setzen Sie den Token in formdata. Wenn Sie die Challenge stattdessen über den Scheduler leiten, als gewöhnliche Anfrage mit eigenem Callback, setzen Sie dont_filter darauf, sonst verwirft der Duplikatfilter jeden Abruf nach dem ersten.
Bei Scrapy-Versionen vor 2.13 braucht AsyncCapSkip außerdem die Einstellung TWISTED_REACTOR in settings.py, gesetzt auf den asyncio-Reactor. Projekte, die seit Scrapy 2.7 erzeugt wurden, haben diese Zeile bereits.
Häufige Fehler und was sie bedeuten
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| Das erste Formular wird abgesendet, spätere nie, und es gibt keinen Fehler | Die Challenge wurde als normale Anfrage per yield ausgegeben, und der Duplikatfilter hat die Wiederholungen verworfen | Rufen Sie sie mit download_async ab oder setzen Sie dont_filter auf die Anfrage |
| Eine ApiException fast sofort, aber erst nach dem ersten Durchlauf | Der HTTP-Cache hat eine alte, abgelaufene Challenge erneut ausgeliefert | Fügen Sie der Challenge-Anfrage dont_cache hinzu |
| Die Website meldet eine fehlgeschlagene Verifizierung, obwohl das Lösen gelungen ist | Der Token hat zu lange gewartet, wurde wiederverwendet oder landete im falschen Feld | Senden Sie sofort mit erhöhter Priorität ab, einmal pro Token, unter dem name-Attribut des Widgets |
| Ein 403 vom Challenge-Endpunkt | Der Endpunkt verlangt die Sitzung, die das Formular geladen hat | Rufen Sie die Challenge wie oben über Scrapy ab, nicht über challenge_url |
| Eine ApiException in unter einer Sekunde, jedes Mal | Die Website verwendet Argon2id oder scrypt, die abgelehnt statt versucht werden | Nichts zu wiederholen; diese Website braucht einen anderen Weg |
| Der Crawl stockt, während jedes Formular gelöst wird | Die synchrone Klasse CapSkip läuft in der Event Loop | Wechseln Sie zu AsyncCapSkip und einem Callback mit async def |
| NoEventLoopError, not currently running on any asynchronous event loop (bei älteren Installationen AsyncLibraryNotFoundError) | Das Projekt legt einen anderen Reactor als den asyncio-Reactor fest | Entfernen Sie diese TWISTED_REACTOR-Zeile oder setzen Sie sie auf den asyncio-Reactor |
| AttributeError: ‘ExecutionEngine’ object has no attribute ‘download_async’ | Scrapy ist älter als 2.14 | Aktualisieren Sie oder verwenden Sie engine.download wie oben gezeigt |
| IgnoreRequest, filtered offsite request, beim Abruf der Challenge | Der Challenge-Endpunkt liegt auf einer anderen Domain, und der Spider setzt allowed_domains | Fügen Sie allow_offsite zum meta der Challenge-Anfrage hinzu |
| Eine ScrapyDeprecationWarning zu from_response bei jedem Absenden | Ab Scrapy 2.16 ist FormRequest.from_response als veraltet markiert | Bauen Sie das Absenden mit form2request |
| Eine NetworkException beim ersten Lösen | CapSkip läuft nicht, oder Host und Port sind falsch | Starten Sie CapSkip und prüfen Sie dann, ob es im Local-Modus oder im Server-Modus laufen soll |
FAQ
Brauche ich für ALTCHA scrapy-playwright oder einen Headless-Browser?
Nein, Sie können ALTCHA in Scrapy mit einfachen Anfragen lösen. Die ganze Aufgabe des Widgets besteht darin, eine Challenge abzurufen, etwas CPU für die Suche nach einer Zahl zu verbrennen und das Ergebnis in ein Feld zu schreiben. Scrapy kann die Challenge abrufen, CapSkip findet die Zahl, und form2request schreibt das Feld, es muss also nirgends JavaScript laufen. So bleibt der Spider im Betrieb so schnell und so günstig wie jeder andere Scrapy-Crawl.
Kann ein Spider auf Scrapyd oder einer gehosteten Scrapy-Plattform den Solver erreichen?
Ja. Schalten Sie CapSkip in den Verbindungseinstellungen in den Server-Modus, damit es auf einer Netzwerkadresse statt auf Loopback lauscht, setzen Sie CAPSKIP_HOST in der Umgebung des Spiders und übergeben Sie den Wert an den Client, wie es das vollständige Beispiel tut. Eine gehostete Plattform verbindet sich über dieselbe HTTP-API wie ein lokaler Spider. Verwenden Sie eine statische öffentliche IP mit einer Firewall-Regel, wenn der Weg über das Internet führt. Der Solver bleibt auf Hardware, die Ihnen gehört, die Zahl der Lösungen ändert also nie, was Sie bezahlen.
Warum steckt das in einem Callback und nicht in einer Downloader-Middleware?
Weil ALTCHA auf einem Formular sitzt, das Sie bewusst absenden, und nicht auf einer Sperrseite, die den Crawl unterbricht. Eine Middleware ist der richtige Ort für eine Challenge, die bei jeder Response auftauchen kann, und genau so geht der Leitfaden zur reCAPTCHA-Middleware damit um. ALTCHA ist Teil eines einzelnen Schritts im Ablauf, und der Callback, der dieses Formular baut, hat bereits alles, was das Lösen braucht.
Kann ich viele Formulare parallel lösen?
Ja, und Scrapy erledigt das bereits für Sie. Jeder asynchrone Callback, der per await auf den Solver wartet, gibt die Kontrolle ab, daher kann jede Seite, die Scrapy heruntergeladen hat, ihr Lösen gleichzeitig in Arbeit haben. CONCURRENT_REQUESTS begrenzt das nicht, denn es beschränkt Downloads, nicht Callbacks. Auf der Seite von CapSkip legt Max. Threads in den ALTCHA-Einstellungen fest, wie viele Challenges gleichzeitig gehasht werden, und da das CPU-Arbeit ist, bringen mehr Threads als Kerne nichts. Weil jeder Callback seine eigene Challenge kurz vor dem Lösen abruft, veraltet keine davon in einer Warteschlange. Rufen Sie aus demselben Grund keinen Stapel Challenges im Voraus ab, um sie später zu lösen.
Die Kurzfassung
Um ALTCHA in Scrapy zu lösen, lesen Sie das Attribut challenge, challengejson oder challengeurl sowie das Attribut name am Element altcha-widget ab. Rufen Sie die Challenge mit download_async und dont_cache ab, damit sie in der Sitzung des Spiders mitläuft und nie auf den Duplikatfilter trifft. Rufen Sie die Methode altcha von AsyncCapSkip per await mit dem JSON so auf, wie es angekommen ist, und geben Sie per yield ein Absenden über form2request aus, mit dem Token in seinen Daten und erhöhter Priorität. Eine Challenge, ein Token, ein Absenden, und der Server-Modus, wenn der Spider woanders läuft.
- Wie ALTCHA funktioniert und was der Solver abdeckt: die ALTCHA-Solver-Seite.
- Captcha-Handling über einen ganzen Scraping-Stack hinweg: die Web-Scraping-Solver-Seite.
- Alle Methoden, die das Python-Paket bereitstellt, finden Sie auf der Python-Captcha-Solver-Seite.
Noch eine letzte Sache zum Volumen. Ein Crawler, der Tausende Formulare absendet, löst Tausende Challenges, und da ein Captcha-Löser auf Ihrem eigenen Rechner läuft, kostet das CPU-Zeit statt einer Gebühr pro Lösung.
