Der beste Weg, einen Infinite-Scroll-Feed zu scrapen, besteht meist darin, gar nicht zu scrollen. Hinter den meisten Feeds steckt eine paginierte Anfrage, oft cursorbasiert, und diese Anfrage direkt zu wiederholen ist schneller, günstiger und zuverlässiger, als einen Browser zu steuern. Dieser Ansatz wird ausführlich behandelt in how to scrape pagination and infinite scroll reliably.
Diese Anleitung ist für die Fälle gedacht, in denen das nicht funktioniert. Die Anfrage ist mit einem kurzlebigen Token signiert. Die Antwort ist ein undurchsichtiger Blob, den die Seite clientseitig dekodiert. Der Feed lädt nur, wenn ein Element in den sichtbaren Bereich eintritt. Die Schaltfläche “Weiter” ist mit einem Zustand verdrahtet, den man nicht rekonstruieren kann. In diesen Fällen muss man das Scrollen und Klicken innerhalb eines echten Renderings handhaben, und es gibt eine Handvoll Arten, wie dabei etwas schiefgeht.
Die Beispiele verwenden die Shifter Web Scraping API, die die Seite in headless Chrome ausführt und eine Kette von Browseraktionen vor der Erfassung akzeptiert.
Vier Arten von dynamischer Paginierung
Bestimmen Sie, welche Art vorliegt, bevor Sie etwas schreiben, denn jede benötigt eine andere Technik.
| Muster | Was den nächsten Batch auslöst | Technik |
|---|---|---|
| Scroll-gesteuerter Feed | Ein Element nahe dem unteren Rand tritt in den sichtbaren Bereich ein | Zu einem Sentinel scrollen, warten, wiederholen |
| Load-more-Schaltfläche | Ein Klick auf eine Schaltfläche, die Elemente anhängt | Klicken, auf neue Elemente warten, wiederholen |
| URL-Zustand-Paginierung | Die Seite aktualisiert die URL, während man durch die Ergebnisse geht | Diese URLs direkt abrufen, kein Scrollen |
| Virtualisierte Liste | Scrollen, aber alte Zeilen werden aus dem DOM entfernt, während neue erscheinen | In Fenstern erfassen, oder die zugrunde liegende Anfrage verwenden |
Das dritte lohnt sich zuerst zu prüfen, da es am günstigsten zu handhaben ist. Scrollen Sie einen Feed in einem normalen Browser und beobachten Sie die Adressleiste. Wenn sich ein Seiten- oder Offset-Parameter ändert, hat die Seite bereits eine paginierte URL bereitgestellt, und jede Seite ist eine gewöhnliche Anfrage.
Das vierte ist dasjenige, das stillschweigend Daten verliert, und dafür gibt es unten einen eigenen Abschnitt.
Korrektes Rendern und Warten
Jede dynamische Technik beginnt mit denselben zwei Steuerungen. render_js=1 führt die Seite in headless Chrome aus, zu denselben Kosten von einem Credit pro erfolgreicher Anfrage wie bei einem statischen Abruf. wait_for_css hält die Erfassung an, bis ein Selektor im DOM existiert, sodass man nie aus einer Seite extrahiert, die noch nicht befüllt ist. Die Rendering-Steuerungen sind dokumentiert unter rendering JavaScript.
Wählen Sie den Wartselektor sorgfältig aus. Auf den Listen-Container zu warten reicht nicht aus, da viele Frameworks zuerst einen leeren Container rendern und ihn später befüllen. Warten Sie stattdessen auf das erste Element in der Liste.
Scroll-gesteuerte Feeds: zu einem Sentinel scrollen
Browseraktionen kommen in js_instructions, ein JSON-Array von Schritten, die der Reihe nach vor der Erfassung ausgeführt werden. Die dokumentierten Aktionen sind scrollTo, click und wait.
Das Muster für einen scroll-gesteuerten Feed besteht darin, zu einem Element zu scrollen, das nach der Liste sitzt, auf das Laden des nächsten Batches zu warten und dies zu wiederholen. Da die Liste wächst, verschiebt sich dieses Element bei jedem Mal weiter nach unten, sodass erneutes Scrollen dorthin den nächsten Ladevorgang auslöst.
import json
import os
import requests
API = "https://scrape.shifter.io/v1"
instructions = [
{"action": "click", "selector": "button.accept-cookies"},
{"action": "scrollTo", "selector": "footer", "timeout": 5000, "block": "start"},
{"action": "wait", "duration": 2000},
{"action": "scrollTo", "selector": "footer", "timeout": 5000, "block": "start"},
{"action": "wait", "duration": 2000},
{"action": "scrollTo", "selector": "footer", "timeout": 5000, "block": "start"},
{"action": "wait", "duration": 2000},
]
rules = {
"items": {
"selector": "article.card",
"type": "list",
"item": {
"link": {"selector": "a.card-link", "output": "@href"},
"name": {"selector": "h3", "output": "text"},
"price": {"selector": ".price", "output": "text"},
},
}
}
params = {
"api_key": os.environ["SHIFTER_API_KEY"],
"url": "https://shop.example.com/category/shoes",
"render_js": 1,
"wait_for_css": "article.card",
"js_instructions": json.dumps(instructions),
"extract_rules": json.dumps(rules),
}
resp = requests.get(API, params=params, timeout=120)
resp.raise_for_status()
items = resp.json()["items"]
Ein paar Details darin sind bewusst gewählt.
Das Cookie-Banner wird zuerst weggeklickt, da ein Overlay den Scroll- oder Klickvorgang abfangen kann. Die Wartezeit zwischen den Scrollvorgängen gibt der Netzwerkanfrage Zeit, sich abzuschließen, und dem DOM Zeit, sich zu aktualisieren; zu kurz, und man erfasst, bevor der Batch eintrifft. extract_rules mit einem List-Typ gibt jede Karte als JSON-Objekt zurück, sodass auf Ihrer Seite kein Parser nötig ist, und requests URL-kodiert beide JSON-Parameter für Sie. Die Extraktionssyntax findet sich unter extraction rules.
Testen Sie die Kette an einer Beispielseite, bevor Sie sie im großen Maßstab ausführen, und stimmen Sie die Anzahl der Scroll-Schritte und die Wartedauer darauf ab, wie schnell diese Seite tatsächlich lädt.
Load-more-Schaltflächen: klicken, dann auf Wachstum warten
Eine Load-more-Schaltfläche ist derselbe Loop mit einem anderen Auslöser:
[
{"action": "click", "selector": "button.load-more", "timeout": 3000},
{"action": "wait", "duration": 2000},
{"action": "click", "selector": "button.load-more", "timeout": 3000},
{"action": "wait", "duration": 2000}
]
Zwei Dinge bringen Leute ins Straucheln. Der Schaltflächen-Selektor ändert während des Ladens oft seinen Zustand, indem er eine disabled-Klasse oder einen Spinner erhält, sodass der zweite Klick auslösen kann, bevor die Schaltfläche wieder klickbar ist, es sei denn, die Wartezeit ist lang genug. Und die Schaltfläche verschwindet meist, wenn nichts mehr zu laden ist, was ein nützliches Signal für Vollständigkeit ist, aber bedeutet, dass Ihre Kette tolerieren muss, dass die letzten Klicks ins Leere gehen. Testen Sie auch hier an der echten Seite.
Das Zeitbudget eines einzelnen Renderings
Alles oben Genannte geschieht innerhalb einer Browsersitzung mit einem Zeitlimit. wait_for_css läuft standardmäßig nach 30 Sekunden ab, und timeout begrenzt, wie lange der Browser auf der Seite verbringen darf. Ein Feed mit Tausenden von Elementen wird nicht innerhalb eines Renderings vollständig laden, egal wie viele Scroll-Schritte Sie verketten.
Teilen Sie das Problem also, statt die Kette zu verlängern.
Ergebnismenge eingrenzen. Filter, Sortierreihenfolgen und Kategoriefacetten erzeugen meist kleinere Feeds. Zwanzig eingegrenzte Feeds, die jeweils vollständig laden, sind zuverlässiger als ein riesiger Feed, der nie fertig wird.
Durch gefilterte URLs blättern. Viele Seiten kombinieren einen Filter mit URL-Zustand, was einen unbegrenzten Scroll in eine endliche Menge gewöhnlicher Anfragen verwandelt.
Auf die zugrunde liegende Anfrage zurückgreifen, wenn der Feed wirklich lang ist und nicht eingegrenzt werden kann. Rufen Sie ihn ohne Rendering ab; für Endpunkte, die JSON zurückgeben, liefert auto_parser=1 den geparsten Body.
Virtualisierte Listen: der stille Datenverlust
Manche Feeds, insbesondere sehr lange, verwenden Listenvirtualisierung. Nur die Zeilen nahe dem sichtbaren Bereich existieren im DOM. Beim Herunterscrollen werden Zeilen oben entfernt.
Scrollt man eine virtualisierte Liste bis nach unten und erfasst sie, erhält man den letzten Bildschirm von Elementen, nicht jedes Element, an dem man vorbeigescrollt ist. Nichts meldet einen Fehler. Die Extraktion liefert eine saubere, plausible, unvollständige Liste.
Erkennen Sie es, bevor Sie einer Ausgabe vertrauen. Führen Sie die Scroll-Kette aus, und prüfen Sie dann, ob die Elemente vom ersten Bildschirm noch im erfassten Ergebnis vorhanden sind. Falls sie fehlen, ist die Liste virtualisiert, und Scrollen-dann-Erfassen kann nicht funktionieren. Verwenden Sie stattdessen URL-Zustand-Paginierung oder die zugrunde liegende Anfrage.
Dynamische Paginierung über Anfragen hinweg
Wenn jede Seite eine separate Anfrage ist, die von serverseitigem Zustand abhängt, etwa einem gegen Ihre Sitzung gespeicherten Cursor, halten Sie diesen Zustand über den gesamten Durchlauf hinweg mit session_id konstant. Eine Sitzung bewahrt Cookies, Browserzustand und die vorgelagerte IP über Anfragen hinweg und läuft nach 10 Minuten Inaktivität ab. Behalten Sie das country für die Dauer einer Sitzung bei, da ein Wechsel mitten im Durchlauf an Locale gebundene Cookies ungültig machen kann. Die Details finden sich unter sessions and proxies.
params.update({
"session_id": "shoes-walk-07",
"country": "de",
})
Halten Sie einen Durchlauf in Bewegung. Eine Sitzung, die untätig verweilt, während Ihr Code zwischen den Seiten etwas Langsames tut, läuft ab und bricht den Cursor.
Wissen, dass man alles bekommen hat
Ein Scraper, der zu früh stoppt, sieht genau aus wie ein Scraper, der fertig ist. Bauen Sie Vollständigkeitsprüfungen in jeden Durchlauf ein.
- Mit der angezeigten Gesamtzahl vergleichen. Viele Feeds zeigen eine Ergebniszahl an. Wenn die Seite 1.284 sagt und Sie 960 extrahiert haben, haben Sie zu früh gestoppt.
- Anhand eines stabilen Schlüssels deduplizieren, etwa dem Link oder der ID des Elements, niemals anhand der Position. Scroll-Feeds rendern Elemente regelmäßig neu, und die Position verschiebt sich, wenn Inhalt eingefügt wird.
- Auf den Endmarker prüfen. Eine verschwundene Load-more-Schaltfläche oder eine Ende-der-Ergebnisse-Meldung ist eine positive Bestätigung. Ihr Fehlen nach Ihrem letzten Schritt bedeutet, dass die Kette zu kurz war.
- Vollständigkeit über die Zeit verfolgen. Eine Kategorie, die gestern 1.200 Elemente geliefert hat und heute 400, hat meist ihr Markup oder ihr Ladeverhalten geändert, nicht ihren Bestand.
Credits und Latenz: den Ansatz pro Seite wählen
| Ansatz | Credits | Latenz | Zuverlässigkeitsrisiko |
|---|---|---|---|
| Zugrunde liegende Anfrage wiederholen | Keine, wenn direkt gesendet; einer pro Seite über die API | Niedrig | Signierte oder undurchsichtige Anfragen |
| URL-Zustand-Paginierung | Einer pro Seite | Niedrig bis moderat | Benötigt eine paginierte URL |
| Scroll- oder Klick-Kette in einem Rendering | Einer für die gesamte Kette | Hoch | Zeitbudget, Virtualisierung |
Ein Rendering, das durch mehrere Batches scrollt, kostet einen Credit, während das Wiederholen derselben Batches als separate Anfragen jeweils einen Credit kostet. Der Tausch betrifft Latenz und Zeitbudget: lange Ketten sind langsamer und laufen eher in ein Timeout. Verwenden Sie Scroll-Ketten für moderate Feeds, bei denen die zugrunde liegende Anfrage unpraktisch ist, und greifen Sie für alles andere zu den anderen beiden Ansätzen.
FAQ
Sollte ich für Infinite Scroll immer JavaScript rendern?
Nein. Prüfen Sie zuerst auf URL-Zustand-Paginierung und auf eine wiederholbare zugrunde liegende Anfrage. Rendering ist die Rückfalllösung, nicht die Standardeinstellung.
Wie viele Scroll-Schritte sollte die Kette haben?
So viele, wie die Seite benötigt, um die gewünschten Batches innerhalb des Zeitbudgets zu laden. Messen Sie an einer Beispielseite, und wenn Sie mehr benötigen, als das Budget erlaubt, grenzen Sie stattdessen den Feed ein.
Warum liefert meine Extraktion weniger Elemente, als ich gescrollt habe?
Fast immer Listenvirtualisierung, bei der Zeilen beim Scrollen aus dem DOM verschwinden. Prüfen Sie, ob die Elemente vom ersten Bildschirm bis zur Erfassung erhalten bleiben.
Kostet jeder Scroll-Schritt einen Credit?
Nein. Die gesamte Kette läuft innerhalb einer Anfrage, und eine erfolgreiche Anfrage kostet einen Credit.
Fazit
Infinite Scroll ist Cursor-Paginierung mit einem Browser davor, und wenn Sie den Cursor direkt erreichen können, sollten Sie das tun. Wenn nicht, handhaben Sie es innerhalb des Renderings: warten Sie auf das erste Element statt auf den Container, scrollen Sie zu einem Sentinel oder klicken Sie auf Load-more mit ausreichend Wartezeit zwischen den Schritten, respektieren Sie das Zeitbudget, indem Sie den Feed eingrenzen, testen Sie auf Virtualisierung, bevor Sie einer Erfassung vertrauen, und überprüfen Sie die Vollständigkeit anhand der eigenen Gesamtzahlen der Seite.
Um zu entscheiden, wann eine gerenderte API-Anfrage überhaupt das richtige Werkzeug ist, siehe when you need a web scraping API for JavaScript-heavy sites. Das Produkt finden Sie auf der Seite web scraping API with JS rendering, mit Tarifen auf der pricing page.