Pagination sieht aus wie der triviale Teil eines Scrapes. Erhöhe page=2, iteriere, bis es keinen Weiter-Button mehr gibt, fertig. Es ist auch der Ort, an dem Scraper öfter als irgendwo sonst still Daten verlieren, und der Verlust ist lautlos: du sammelst die Seiten eins bis acht, verpasst neun bis vierzig, weil die Seite dich gedeckelt hat, und nichts wirft einen Fehler. Oder du zählst Elemente doppelt, weil die Liste sich unter dir verschob. Oder du hörst zu früh auf, weil das Laden pausierte und du es mit dem Ende verwechselt hast. Das letzte Element genau einmal zu bekommen und zu wissen, dass du es hast, ist das ganze Spiel.
Es gibt zwei Formen des Problems, klassische Pagination und Infinite Scroll, und sie teilen eine Kernfrage: wie sammle ich alles genau einmal und weiß, wann ich wirklich fertig bin? So beantwortest du sie für beide.
Klassische Pagination: wisse, welche du hast
Bevor du eine Schleife schreibst, identifiziere den Pagination-Mechanismus, denn zwei der drei üblichen Arten haben Fallen, die deine Daten korrumpieren.
Offset- oder Seitennummer-Pagination (?page=5 oder ?offset=100&limit=25) ist die häufigste und die gefährlichste. Zwei Dinge gehen schief. Erstens, Drift: ändert sich die zugrunde liegende Liste zwischen Anfragen, und auf einer aktiven Seite tut sie das, verschieben neu nach oben geschobene Elemente alles nach unten, sodass Seite zwei jetzt Seite eins überlappt und ein Element durch die Ritzen fällt. Dedupliziere nie nach Position; dedupliziere nach einer stabilen eindeutigen ID. Zweitens, Deep-Pagination-Deckel: viele Seiten weigern sich, über Seite 100 oder Offset 10.000 hinaus zu liefern, sodass der Schwanz per Offset schlicht unerreichbar ist. Triffst du auf diese Wand, kannst du dich nicht bis zum Ende blättern, du musst die Daten anders partitionieren, nach Datumsbereich, Kategorie oder Preisband, und innerhalb jeder Scheibe paginieren, damit keine Scheibe den Deckel überschreitet.
Cursor- oder Keyset-Pagination (?after=<token>) ist die robuste Art. Die Antwort reicht dir einen Cursor für den nächsten Batch, du folgst ihm, und du hörst auf, wenn er fehlt. Sie ist immun gegen Drift, weil der Cursor auf eine stabile Position in den Daten zeigt, nicht auf einen Offset, der sich verschiebt. Bevorzuge sie, wann immer die Seite sie anbietet, und versuche nie, einen Cursor zu konstruieren oder zu erraten, behandle ihn als opak und folge nur dem, den du bekommen hast.
cursor, seen = None, set()while True: resp = fetch(url, params={"after": cursor} if cursor else {}) for item in resp["items"]: if item["id"] not in seen: # dedupliziere nach stabiler id, nie nach Position seen.add(item["id"]); yield item cursor = resp.get("next_cursor") if not cursor: # ein fehlender Cursor ist das echte End-Signal breakDer einzelne nützlichste Zug hier ist, die Netzwerkschicht zu betrachten, nicht das gerenderte HTML. Öffne das Ziel in einem Browser, beobachte die XHR/fetch-Anfragen, und sehr oft findest du einen sauberen JSON-Endpunkt mit Cursor-Pagination hinter der Seite. Diesen direkt aufzurufen ist schneller, leichter auf der Bandbreite und weit zuverlässiger, als Seite für Seite HTML zu scrapen.
Infinite Scroll: meist ist es verkleidete Cursor-Pagination
Infinite Scroll braucht fast nie einen echten Browser. Unter der Haube löst Scrollen nur dieselbe paginierte Anfrage aus, und findest du diese zugrunde liegende Anfrage im Netzwerk-Tab, kannst du sie direkt mit ihrem Cursor aufrufen, genau wie eine API, und den Browser ganz überspringen, was dramatisch billiger auf Bandbreite und Zeit ist.
Wenn die Seite echt Rendering erfordert, steuere sie mit Playwright oder Puppeteer, und respektiere drei Fallen, die jeden erwischen.
Erstens, wisse, was ein Laden auslöst: die Scroll-Position, ein IntersectionObserver-Sentinel nahe dem Boden, oder ein “Mehr laden”-Button. Löse den richtigen aus.
Zweitens, warte darauf, dass der neue Batch tatsächlich ankommt, nicht auf einen festen Timer. Ein sleep(2) ist eine Race Condition: manchmal ist der Inhalt geladen, manchmal nicht, und auf einem langsamen Proxy-Hop oft nicht. Warte, bis der Elementzähler steigt oder das Netzwerk untätig wird.
prev = 0while True: page.mouse.wheel(0, 20000) # löse den nächsten Batch aus page.wait_for_function( # warte auf echte Ankunft, keinen Timer "n => document.querySelectorAll('.item').length > n", arg=prev) items = page.query_selector_all('.item') if len(items) == prev: # kein Wachstum nach echtem Warten break # ...aber siehe Terminierung unten prev = len(items)Drittens, und die, die die meisten Daten still abschneidet: virtualisierte Listen. Bibliotheken wie react-window entfernen off-screen Zeilen aus dem DOM, um schnell zu bleiben, also wenn du zum Boden scrollst und erst dann das DOM scrapst, bekommst du das letzte sichtbare Fenster und nichts sonst. Du musst die Elemente extrahieren, während sie beim Scrollen erscheinen, nicht einmal am Ende.
Wissen, wann du wirklich fertig bist
Terminierung ist der schwierigste Teil, denn “das Laden hörte auf” hat zwei sehr verschiedene Ursachen, die identisch aussehen: du hast das echte Ende erreicht, oder die Seite hat dich mitten in der Sequenz rate-limitiert und still aufgehört, mehr zu liefern. Das Zweite als das Erste zu behandeln ist genau, wie du ein Dataset auslieferst, dem der Schwanz fehlt.
Drei Verteidigungen. Wiederhole ein stockendes Laden ein paar Mal, bevor du das Ende erklärst, damit ein langsamer Batch nicht mit Vollständigkeit verwechselt wird. Vergleiche gegen eine Gesamtzahl, wenn die Seite eine offenlegt, ein “1.240 Ergebnisse”-Header ist eine Prüfsumme: hast du 900 gesammelt, wurdest du abgeschnitten, nicht fertig. Und behandle ein Laden, das direkt nach einem Schwall von Anfragen stoppt, als vermuteten Soft-Block statt dem Ende, und wiederhole diesen Schwanz mit einer frischen Identität, statt partielle Daten zu akzeptieren. Das ist dasselbe Lautlos-Scheitern-Problem, das die Füllrate- und Erwartungszahl-Prüfungen des Pipeline-Monitorings über einen ganzen Lauf abfangen sollen.
Dedup und Vollständigkeit, immer
Zwei Gewohnheiten machen den Unterschied zwischen einem vollständigen Dataset und einem plausibel aussehenden unvollständigen. Dedupliziere nach einer stabilen eindeutigen ID, nie nach Seitennummer, Reihenfolge oder Inhalts-Hash einer ganzen Zeile, denn die Reihenfolge verschiebt sich und Zeilen werden leicht bearbeitet. Und verfolge Gesammelt-gegen-Erwartet-Zählungen überall dort, wo die Seite dir eine Gesamtzahl gibt, damit Abschneiden als eine Zahl auftaucht, die nicht aufgeht, statt als eine Lücke, die niemand bis viel später bemerkt.
Wo Proxies hineinpassen
Eine paginierte Sequenz ist meist eine logische Session, und sie sollte wie eine aussehen. Nutze eine Sticky Session, damit jede Seite einer einzelnen Abfrage über dieselbe IP austritt: mitten in der Sequenz zu rotieren kann Anti-Bot-Systeme auslösen, die erwarten, dass ein Besucher kohärent blättert, und auf personalisierten oder geo-variierenden Seiten kann es sogar inkonsistente Ergebnisse zwischen Seiten zurückgeben. Gib jeder Abfrage ihre eigene Sticky Session und rotiere zwischen Abfragen, nicht innerhalb einer.
Deep Pagination bedeutet auch viele Anfragen an einen einzelnen Host in einem kurzen Fenster, was genau der Ort ist, an dem du rate-limitiert und geblockt wirst. Takte die Sequenz, fahre bei 429s zurück statt zu hämmern, und stütze dich auf einen sauberen Pool, damit du von vornherein seltener herausgefordert wirst. Da ein Block mitten in der Sequenz sich als das Ende verkleidet, leistet bessere IP-Reputation hier doppelte Arbeit: sie reduziert das Abschneiden und macht, kombiniert mit Erwartungszahl-Prüfungen, das Abschneiden, das du doch triffst, sichtbar statt lautlos.
Das Fazit
Pagination ist nicht der triviale Teil, sie ist, wo Vollständigkeit gewonnen oder verloren wird. Identifiziere den echten Mechanismus und bevorzuge Cursor-Pagination oder den zugrunde liegenden JSON-Endpunkt gegenüber Offset-und-HTML. Dedupliziere nach stabiler ID, nie nach Position. Für Infinite Scroll rufe die zugrunde liegende Anfrage auf, wenn du kannst, und wenn du rendern musst, warte auf echte Ladevorgänge statt auf Timer und extrahiere Elemente, während sie erscheinen, damit virtualisierte Listen deine Daten nicht auffressen. Vor allem, behandle “das Laden hörte auf” als verdächtig, bis eine Erwartungszahl-Prüfung oder ein wiederholter Schwanz beweist, dass es wirklich das Ende war, und fahre jede paginierte Abfrage auf ihrer eigenen Sticky Session, damit die Sequenz kohärent bleibt.
Mach das, und du sammelst die ganze Liste, genau einmal, und weißt, dass du es getan hast. Richte die Sequenz auf ein sauberes Residential Gateway, damit der Schwanz nicht zu einer Wand aus Blocks wird, und die Pro-GB-Preisgestaltung lässt dich Deep Pagination crawlen, ohne dass ein Pro-Anfrage-Zähler gegen dich arbeitet.