La pagination a l’air de la partie triviale d’un scrape. Incrémentez page=2, itérez jusqu’à ce qu’il n’y ait plus de bouton suivant, terminé. C’est aussi là où les scrapers perdent des données en silence plus souvent que partout ailleurs, et la perte est silencieuse : vous collectez les pages une à huit, ratez la neuf à la quarante parce que le site vous a plafonné, et rien ne renvoie d’erreur. Ou vous comptez les éléments en double parce que la liste a bougé sous vous. Ou vous vous arrêtez trop tôt parce que le chargement s’est mis en pause et vous l’avez confondu avec la fin. Obtenir le dernier élément, exactement une fois, et savoir que vous l’avez, c’est tout le jeu.
Il y a deux formes du problème, pagination classique et scroll infini, et elles partagent une question centrale : comment collecter tout exactement une fois et savoir quand j’ai vraiment fini ? Voici comment y répondre pour les deux.
Pagination classique : sachez laquelle vous avez
Avant d’écrire une boucle, identifiez le mécanisme de pagination, car deux des trois types courants ont des pièges qui corrompent vos données.
La pagination par offset ou numéro de page (?page=5 ou ?offset=100&limit=25) est la plus courante et la plus dangereuse. Deux choses tournent mal. D’abord, la dérive : si la liste sous-jacente change entre les requêtes, et sur un site actif elle le fait, les nouveaux éléments poussés en haut décalent tout vers le bas, donc la page deux chevauche maintenant la page une et un élément se glisse entre les mailles. Ne dédupliquez jamais par position ; dédupliquez par un ID unique stable. Ensuite, les plafonds de pagination profonde : beaucoup de sites refusent de servir au-delà de la page 100 ou de l’offset 10 000, donc la queue est simplement inatteignable par offset. Quand vous heurtez ce mur, vous ne pouvez pas paginer jusqu’à la fin, vous devez partitionner les données autrement, par plage de dates, catégorie, ou tranche de prix, et paginer dans chaque tranche pour qu’aucune ne dépasse le plafond.
La pagination par curseur ou keyset (?after=<token>) est la robuste. La réponse vous tend un curseur pour le lot suivant, vous le suivez, et vous vous arrêtez quand il est absent. Elle est immunisée contre la dérive parce que le curseur pointe vers une position stable dans les données, pas un offset qui se décale. Préférez-la dès que le site l’offre, et n’essayez jamais de construire ou deviner un curseur, traitez-le comme opaque et suivez seulement celui qu’on vous a donné.
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: # dedup par id stable, jamais par position seen.add(item["id"]); yield item cursor = resp.get("next_cursor") if not cursor: # un curseur absent est le vrai signal de fin breakLe geste le plus utile ici est de regarder la couche réseau, pas le HTML rendu. Ouvrez la cible dans un navigateur, observez les requêtes XHR/fetch, et très souvent vous trouverez un endpoint JSON propre avec pagination par curseur derrière la page. L’appeler directement est plus rapide, plus léger en bande passante, et bien plus fiable que scraper du HTML page par page.
Scroll infini : c’est souvent de la pagination par curseur déguisée
Le scroll infini n’a presque jamais besoin d’un vrai navigateur. Sous le capot, le scroll déclenche juste la même requête paginée, et si vous trouvez cette requête sous-jacente dans l’onglet réseau, vous pouvez l’appeler directement avec son curseur exactement comme une API et sauter le navigateur entièrement, ce qui est nettement moins cher en bande passante et en temps.
Quand le site requiert vraiment le rendu, pilotez-le avec Playwright ou Puppeteer, et respectez trois pièges qui attrapent tout le monde.
D’abord, sachez ce qui déclenche un chargement : la position de scroll, une sentinelle IntersectionObserver près du bas, ou un bouton « charger plus ». Déclenchez le bon.
Ensuite, attendez que le nouveau lot arrive vraiment, pas un minuteur fixe. Un sleep(2) est une condition de course : parfois le contenu a chargé, parfois non, et sur un saut de proxy lent souvent non. Attendez que le compte d’éléments augmente ou que le réseau devienne inactif.
prev = 0while True: page.mouse.wheel(0, 20000) # declenche le lot suivant page.wait_for_function( # attends l'arrivee reelle, pas un minuteur "n => document.querySelectorAll('.item').length > n", arg=prev) items = page.query_selector_all('.item') if len(items) == prev: # pas de croissance apres une vraie attente break # ...mais voir la terminaison plus bas prev = len(items)Troisième, et celle qui tronque en silence le plus de données : les listes virtualisées. Des librairies comme react-window retirent du DOM les lignes hors écran pour rester rapides, donc si vous scrollez jusqu’en bas et ne scrapez le DOM qu’ensuite, vous obtenez la dernière fenêtre visible et rien d’autre. Vous devez extraire les éléments à mesure qu’ils apparaissent pendant le scroll, pas une fois à la fin.
Savoir quand vous avez vraiment fini
La terminaison est la partie la plus difficile, car « le chargement s’est arrêté » a deux causes très différentes qui se ressemblent : vous avez atteint la vraie fin, ou le site vous a limité le débit en milieu de séquence et a cessé de servir plus en silence. Traiter la seconde comme la première, c’est exactement comment vous livrez un jeu de données auquel il manque la queue.
Trois défenses. Réessayez un chargement bloqué deux ou trois fois avant de déclarer la fin, pour qu’un lot lent ne soit pas confondu avec la complétude. Comparez à un total quand le site en expose un, un en-tête « 1 240 résultats » est une somme de contrôle : si vous en avez collecté 900, vous avez été tronqué, pas terminé. Et traitez un chargement qui s’arrête juste après une rafale de requêtes comme un blocage doux suspecté plutôt que la fin, et réessayez cette queue avec une identité fraîche au lieu d’accepter des données partielles. C’est le même problème d’échec silencieux que les vérifications de taux de remplissage et de compte attendu du monitoring de pipeline sont conçues pour attraper sur toute une course.
Dédup et complétude, toujours
Deux habitudes font la différence entre un jeu de données complet et un incomplet à l’air plausible. Dédupliquez par un ID unique stable, jamais par numéro de page, ordre, ou hash de contenu d’une ligne entière, car l’ordre se décale et les lignes sont légèrement éditées. Et suivez les comptes collecté-face-à-attendu partout où le site vous donne un total, pour que la troncature apparaisse comme un nombre qui ne colle pas plutôt qu’un trou que personne ne remarque avant bien plus tard.
Où s’insèrent les proxys
Une séquence paginée est habituellement une session logique, et elle devrait en avoir l’air. Utilisez une session sticky pour que chaque page d’une seule requête sorte par la même IP : tourner en milieu de séquence peut déclencher des systèmes anti-bot qui attendent qu’un visiteur pagine de façon cohérente, et sur des sites personnalisés ou variant par géo, cela peut même renvoyer des résultats incohérents entre pages. Donnez à chaque requête sa propre session sticky et tournez entre requêtes, pas dans une.
La pagination profonde signifie aussi beaucoup de requêtes vers un seul hôte dans une courte fenêtre, ce qui est précisément là où vous êtes limité en débit et bloqué. Cadencez la séquence, reculez sur les 429 au lieu de marteler, et appuyez-vous sur un pool propre pour être défié moins souvent au départ. Comme un blocage en milieu de séquence se déguise en fin, une meilleure réputation d’IP fait un double travail ici : elle réduit la troncature et, combinée à des vérifications de compte attendu, rend visible la troncature que vous subissez quand même au lieu de silencieuse.
En résumé
La pagination n’est pas la partie triviale, c’est là où la complétude se gagne ou se perd. Identifiez le vrai mécanisme et préférez la pagination par curseur ou l’endpoint JSON sous-jacent à offset-et-HTML. Dédupliquez par ID stable, jamais par position. Pour le scroll infini, appelez la requête sous-jacente quand vous pouvez, et quand vous devez rendre, attendez de vrais chargements plutôt que des minuteurs et extrayez les éléments à mesure qu’ils apparaissent pour que les listes virtualisées ne mangent pas vos données. Surtout, traitez « le chargement s’est arrêté » comme suspect jusqu’à ce qu’une vérification de compte attendu ou une queue réessayée prouve que c’était vraiment la fin, et faites tourner chaque requête paginée sur sa propre session sticky pour que la séquence reste cohérente.
Faites cela et vous collectez la liste entière, exactement une fois, et vous savez que vous l’avez fait. Pointez la séquence vers un gateway résidentiel propre pour que la queue ne devienne pas un mur de blocages, et la tarification au Go vous laisse crawler la pagination profonde sans un compteur par requête qui travaille contre vous.