Alle Beiträge

· Casimiro Ferreira· 6 Min. Lesezeit

Kombinierbare, direkt einsetzbare requests-Sessions für widerstandsfähigen Zugriff auf öffentliche Daten

  • HTTP
  • Scraping
  • Cloudflare
  • Anti-Bot
  • Python
  • Open Source

Ein Großteil unserer Arbeit — Medien-Metadaten-Clients, Katalog-Anreicherung, Archivierung — hängt davon ab, öffentliche Webseiten zuverlässig zu lesen. Das Problem sind selten die Daten; es ist, dass ein Großteil der Bot-Erkennungsinfrastruktur auf skriptgesteuerte Angriffe abgestimmt ist und am Ende jeden wohlverhaltenen Nicht-Browser-Client als einen solchen fehlklassifiziert. Das geschieht auf zwei getrennten Achsen:

  • „Was sind Sie?” — Cloudflare und Konsorten markieren Anfragen nicht dafür, was Sie anfragen, sondern dafür, wie Sie auf der Leitung aussehen: Ihr TLS-Handshake, Ihr JA3-Fingerabdruck, ob Sie eine JavaScript-Challenge ausführen können. Ein einfacher requests-Handshake sieht einem Browser-Handshake überhaupt nicht ähnlich, sodass er von Prüfungen erfasst wird, die auf skriptgesteuerten Missbrauch abzielen, selbst wenn der Traffic selbst harmlos ist.
  • „Wer sind Sie?” — IP-Reputation und Ratenbegrenzungen ignorieren Ihren Fingerabdruck völlig; sie zählen, wie viele Anfragen von einer Adresse kommen, was einen einzelnen wohlverhaltenen Client ebenso treffen kann wie einen missbräuchlichen.

Die beiden Achsen sind orthogonal, daher beantworten wir sie mit zwei kleinen Bibliotheken, die sich sauber stapeln lassen: unblock_requests beantwortet was sind Sie, anon_requests beantwortet wer sind Sie. Beide sind direkte Ersatzlösungen für eine requests-Session im alltäglichen Code. In diesem Beitrag geht es speziell um diese Transportschicht — den Bytes-auf-der-Leitung-Teil — nicht um das Parsing oder die Pipeline darüber.

Der Geltungsbereich, unverblümt gesagt: Diese Transporte sind ausschließlich für öffentliche, nicht authentifizierte Seiten gedacht. Sie respektieren robots.txt und jede deklarierte Crawl-Verzögerung — siehe unseren Beitrag zu robots.txt & Sitemaps dazu, wie wir das prüfen, bevor wir einen Scraper schreiben — und jeder darauf aufbauende Client wird auf ein niedriges Anfragevolumen beschränkt, sodass eine Zielseite nie eine nennenswerte Last von uns sieht. Das ist kein nachträglich angeklebter Disclaimer; es ist eine echte technische Randbedingung dafür, wie diese Sessions eingesetzt werden, denn ein widerstandsfähiger Client, der zugleich rücksichtslos ist, konterkariert seinen eigenen Zweck.

Die Design-Randbedingung: die requests-Form beibehalten

unblock_requests-Sessions leiten sich von requests.Session ab und überschreiben nur request() — alles andere (.get(), .post(), Cookies, Header, die Context-Manager-Semantik) wird geerbt, sodass alles, was gegen requests.Session typisiert ist, sie unverändert akzeptiert. Die anon_requests-Sessions umschließen statt abzuleiten — sie stellen dieselben Verb-Methoden und die Context-Manager-Schnittstelle bereit, bauen aber ihre innere Session bei jeder Rotation neu auf:

from unblock_requests import CloudflareSession   # alias: Session
import requests

s = CloudflareSession(flaresolverr_url="http://your-flaresolverr-host:8191")
html = s.get("https://www.progarchives.com/artist.asp?id=1").text
assert isinstance(s, requests.Session)            # True

Das ist die gesamte ethische und technische Haltung in einer Zeile: Wir automatisieren keinen Browser-als-Nutzer, wir bauen einen widerstandsfähigen HTTP-Client für bereits öffentliche Daten. Kein sichtbarer Browser erscheint auf jemandes Bildschirm, und nichts im heißen Pfad braucht ein Display.

Schicht eins: unblock_requests und seine Transporte

unblock_requests macht einen einfachen Python-Client interoperabel mit Bot-Erkennungsprüfungen, die auf Browser abgestimmt sind. Sie wählen einen Transport mit dem Schlüsselwortargument mode= (oder der Umgebungsvariablen UNBLOCK_REQUESTS_TRANSPORT — explizite Schlüsselwortargumente gewinnen immer). Die wichtigsten vier:

ModusWas er tut
curl_cffi (Standard)Chrome-TLS/JA3-Imitation über curl_cffi. Erzeugt in den meisten Netzwerken einen browserförmigen Handshake, ohne zusätzliche Infrastruktur.
requestsEinfaches requests, keine Imitation.
flaresolverrLeitet über einen FlareSolverr-Headless-Browser, der die JS-Challenge löst — Live-Daten.
waybackLiest den neuesten Snapshot des Internet Archive — veraltet, braucht aber nichts.

Der Standard, curl_cffi, ist der günstige Gewinn. Die meisten „Sie sind ein Bot”-Urteile sind eine TLS-Fingerabdruck-Nichtübereinstimmung: Das Standard-requests (über OpenSSL) macht seinen Handshake in nichts wie Chrome. curl_cffi imitiert einen echten Chrome-Build (impersonate="chrome" standardmäßig), sodass Handshake und JA3 übereinstimmen und die Prüfung einfach passiert. Kein JavaScript ausgeführt, kein Browser gestartet.

Wenn eine Website zu einer tatsächlichen interaktiven JS-Challenge eskaliert, reicht curl_cffi nicht aus — etwas muss die Challenge ausführen. Das ist der flaresolverr-Modus: Eine FlareSolverr-Instanz, die Sie selbst hosten, übernimmt das Lösen in einem Headless-Browser außerhalb Ihres Prozesses, und unblock_requests sendet nur ein POST daran und hebt das gelöste HTML aus der Antwort heraus. Das Setzen von flaresolverr_url wählt diesen Modus automatisch:

CloudflareSession(flaresolverr_url="http://host:8191")          # solve live
CloudflareSession(mode="wayback")                              # force archive
CloudflareSession(flaresolverr_url="http://host:8191",
                  wayback_fallback=True)                       # live, archive on failure

Elegante Degradation zum Archiv

Infrastruktur hat schlechte Tage — FlareSolverr ist ausgefallen, die Website ist nicht erreichbar, die Challenge ist gerade nicht lösbar. Statt den ganzen Job scheitern zu lassen, kann die Session auf die Wayback Machine zurückgreifen. Die Challenge-Erkennung ist heuristisch: Ein kleiner is_challenge()-Helfer schnüffelt am ersten Teil des Body nach den verräterischen Markern eines Cloudflare-Zwischenbildschirms („just a moment”, challenge-platform, cf_chl_opt, cf-mitigated). Bei einem blockierten GET löst die Session, wenn wayback_fallback aktiviert ist, den neuesten Snapshot über die Verfügbarkeits-API von archive.org auf und gibt dessen Rohbytes zurück (die rohe …id_/-Form, ohne Toolbar oder Link-Umschreibung). archive.org ist nicht Cloudflare-geschützt, sodass einfaches requests es erreicht.

Zwei Implementierungshinweise, die es zu kennen lohnt: In den Modi wayback und flaresolverr ist das Ergebnis eine synthetisierte, aber echte requests.Response, die aus dem abgerufenen HTML aufgebaut wird — daher gelten dort stream=, benutzerdefinierte Adapter und Connection-Pooling nicht, während die Modi requests/curl_cffi vollständig nativ sind. Und der Fallback greift nur bei GETs; wir wiederholen niemals stillschweigend eine verändernde Anfrage aus einem Archiv.

Schicht zwei: anon_requests und IP-Rotation

Das orthogonale Problem ist die IP-Reputation. Selbst ein perfekt browserförmiger Handshake kann ratenbegrenzt werden, wenn jede Anfrage von einer Adresse kommt — volumenbasierte Heuristiken schauen auf die Adresse, nicht auf den Fingerabdruck. anon_requests verteilt die Last über Adressen hinweg mit RotatingProxySession (gescrapte öffentliche Proxys, optionale Validierung, SOCKS5/HTTP) und RotatingTorSession (rotierende Tor-Circuits), sodass ein Client mit niedrigem Volumen nie mit einem verwechselt wird, der eine Website von einer einzigen IP aus bombardiert. Jede Anfrage geht über einen frischen Exit hinaus, und tote Proxys werden bei Verbindungsfehlern wegrotiert.

from anon_requests import RotatingProxySession, ProxyType

with RotatingProxySession(proxy_type=ProxyType.SOCKS5, validate=True) as s:
    print(s.get("https://ipecho.net/plain", timeout=5).text)  # a new IP each time

Die Komposition: verteilte Last und ein kompatibler Handshake zugleich

Diese beiden Bibliotheken sind darauf ausgelegt, sich zu stapeln statt sich zu überlappen. anon_requests-Sessions akzeptieren ein session_factory — ein beliebiges Callable, das eine requests.Session zurückgibt, standardmäßig requests.Session. Die Rotations- und Proxy-Einstellungen werden auf das angewendet, was diese Factory zurückgibt. So injizieren Sie eine CloudflareSession als Factory und erhalten beide Verhaltensweisen aus einem Objekt:

from anon_requests import RotatingProxySession
from unblock_requests import CloudflareSession

session = RotatingProxySession(
    session_factory=lambda: CloudflareSession(flaresolverr_url="http://host:8191"),
)
session.get(url)   # spreads load across IPs *and* uses a browser-compatible handshake

Der rotierte Proxy fließt durch jeden Transport — auch in FlareSolverr hinein, das seinen Headless-Browser über das Feld proxy der Solve-Anfrage steuert. So bleibt die gesamte Anfrage — Handshake, Challenge-Lösung und Exit-IP — durchgängig konsistent, was für einen Client, der nicht versucht, wie mehr als ein Besucher auszusehen, schlicht korrektes Verhalten ist.

Warum diese Form

Jedes Anliegen als eigene dünne requests.Session-Subklasse zu halten bedeutet, dass Aufrufer nur das wählen, was sie brauchen — TLS-Imitation allein, den vollständigen Rotations-plus-Solve-Stack oder alles dazwischen —, indem sie einen Konstruktor austauschen, nicht indem sie ihren HTTP-Code umschreiben. Das teure, schwergewichtige Werkzeug (ein echter Browser) bleibt außerhalb des Prozesses in FlareSolverr und wird nur herbeigerufen, wenn eine JS-Challenge es wirklich erfordert; der häufige Fall ist ein günstiger imitierter Handshake. Und wenn das Live-Web sich weigert, antwortet das Archiv.

Die session_factory-Naht ist der Ort, an dem die Komposition geschieht, und sie hält die Bibliotheken unabhängig erweiterbar: Fügen Sie unblock_requests einen Transportmodus hinzu, und anon_requests komponiert ihn kostenlos mit. Widerstandsfähiger Zugriff auf öffentliche Daten, sauber umgesetzt.

Beide sind FOSS und selbst hostbar: unblock_requests und anon_requests.

Diese Transporte treiben all unsere Musikdatenbank-Scraper an. Für die Website-Aufklärung, bevor Sie einen Scraper bauen, siehe sitemapper und den Beitrag zu robots.txt & Sitemaps.