Paginação parece ser a parte trivial de um scrape. Incrementa page=2, repete o loop até não haver botão de próxima página, pronto. É também onde os scrapers silenciosamente perdem dados com mais frequência do que em qualquer outro lugar, e a perda é silenciosa: você coleta as páginas um a oito, perde nove a quarenta porque o site limitou você, e nada gera erro. Ou você conta itens em duplicidade porque a lista mudou sob seus pés. Ou você para cedo porque o carregamento pausou e você confundiu isso com o fim. Obter o último item, exatamente uma vez, e saber que você o obteve, é todo o jogo.
Existem duas formas do problema, paginação clássica e rolagem infinita, e ambas compartilham uma questão central: como coleto tudo exatamente uma vez e sei quando realmente terminei? Veja como responder isso para as duas.
Paginação clássica: saiba qual tipo você tem
Antes de escrever um loop, identifique o mecanismo de paginação, porque dois dos três tipos comuns têm armadilhas que corrompem seus dados.
Paginação por offset ou número de página (?page=5 ou ?offset=100&limit=25) é a mais comum e a mais perigosa. Duas coisas dão errado. Primeiro, o drift (deslocamento): se a lista subjacente muda entre as requisições, e em um site ativo isso acontece, novos itens inseridos no topo empurram tudo para baixo, então a página dois agora se sobrepõe à página um e um item escapa pelas fendas. Nunca faça deduplicação por posição; faça deduplicação por um ID único e estável. Segundo, os limites de paginação profunda: muitos sites recusam-se a servir além da página 100 ou do offset 10.000, então o final simplesmente se torna inalcançável por offset. Quando você bate nesse muro, não é possível avançar página por página até o fim, você precisa particionar os dados de outra forma, por intervalo de data, categoria ou faixa de preço, e paginar dentro de cada fatia para que nenhuma fatia exceda o limite.
Paginação por cursor ou keyset (?after=<token>) é o tipo robusto. A resposta entrega um cursor para o próximo lote, você o segue, e para quando ele estiver ausente. É imune ao drift porque o cursor aponta para uma posição estável nos dados, não um offset que se desloca. Prefira-o sempre que o site o oferecer, e nunca tente construir ou adivinhar um cursor, trate-o como opaco e siga apenas o que foi fornecido.
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 by stable id, never by position seen.add(item["id"]); yield item cursor = resp.get("next_cursor") if not cursor: # absent cursor is the real end signal breakA ação mais útil aqui é olhar para a camada de rede, não para o HTML renderizado. Abra o alvo em um navegador, observe as requisições XHR/fetch, e você frequentemente encontrará um endpoint JSON limpo com paginação por cursor por trás da página. Chamar esse endpoint diretamente é mais rápido, mais leve em termos de largura de banda, e muito mais confiável do que fazer scraping do HTML página por página.
Rolagem infinita: geralmente é paginação por cursor disfarçada
A rolagem infinita quase nunca exige um navegador real. Por baixo dos panos, a rolagem apenas dispara o mesmo fetch paginado, e se você encontrar essa requisição subjacente na aba de rede, poderá chamá-la diretamente com seu cursor exatamente como uma API e pular o navegador por completo, o que é dramaticamente mais barato em largura de banda e tempo.
Quando o site genuinamente exige renderização, controle-o com Playwright ou Puppeteer, e respeite três armadilhas que pegam todo mundo.
Primeiro, saiba o que dispara um carregamento: posição de rolagem, um sentinela IntersectionObserver perto do final, ou um botão “carregar mais”. Dispare o gatilho certo.
Segundo, espere o novo lote realmente chegar, não um temporizador fixo. Um sleep(2) é uma condição de corrida: às vezes o conteúdo carregou, às vezes não, e em um salto de proxy lento frequentemente não carregou. Espere até que a contagem de itens aumente ou que a rede fique ociosa.
prev = 0while True: page.mouse.wheel(0, 20000) # trigger the next batch page.wait_for_function( # wait for real arrival, not a timer "n => document.querySelectorAll('.item').length > n", arg=prev) items = page.query_selector_all('.item') if len(items) == prev: # no growth after a real wait break # ...but see termination below prev = len(items)Terceiro, e o que silenciosamente trunca mais dados: listas virtualizadas. Bibliotecas como react-window removem linhas fora da tela do DOM para manter a velocidade, então se você rolar até o final e só então fizer o scraping do DOM, obtém apenas a última janela visível e nada mais. Você precisa extrair os itens conforme eles aparecem durante a rolagem, não de uma vez ao final.
Sabendo quando você realmente terminou
A terminação é a parte mais difícil, porque “o carregamento parou” tem duas causas muito diferentes que parecem idênticas: você chegou ao fim genuíno, ou o site limitou sua taxa no meio da sequência e silenciosamente parou de servir mais. Tratar a segunda como a primeira é exatamente como você acaba entregando um conjunto de dados ao qual falta a cauda.
Três defesas. Tente novamente um carregamento travado algumas vezes antes de declarar o fim, para que um lote lento não seja confundido com conclusão. Compare com um total quando o site expõe um, um cabeçalho “1.240 resultados” é um checksum: se você coletou 900, foi truncado, não terminou. E trate um carregamento que para logo após uma rajada de requisições como um suspeito soft block em vez do fim, e tente novamente essa cauda com uma identidade nova em vez de aceitar dados parciais. Este é o mesmo problema de falha silenciosa que as verificações de taxa de preenchimento e contagem esperada do monitoramento de pipeline foram construídas para capturar em toda uma execução.
Deduplicação e completude, sempre
Dois hábitos fazem a diferença entre um conjunto de dados completo e um incompleto que parece plausível. Faça deduplicação por um ID único e estável, nunca por número de página, ordem, ou hash de conteúdo de uma linha inteira, porque a ordenação muda e as linhas são levemente editadas. E acompanhe as contagens coletadas versus esperadas sempre que o site fornecer um total, para que o truncamento apareça como um número que não fecha em vez de uma lacuna que ninguém percebe até muito depois.
Onde os proxies se encaixam
Uma sequência paginada é geralmente uma sessão lógica única, e deveria parecer uma. Use uma sessão sticky para que toda página de uma única consulta saia pelo mesmo IP: rotacionar no meio da sequência pode disparar sistemas antibot que esperam que um visitante pagine de forma coerente, e em sites personalizados ou com variação geográfica pode até retornar resultados inconsistentes entre páginas. Dê a cada consulta sua própria sessão sticky e rotacione entre consultas, não dentro de uma.
Paginação profunda também significa muitas requisições a um único host em uma janela curta de tempo, que é precisamente onde você é limitado por taxa e bloqueado. Ritme a sequência, recue diante de 429s em vez de insistir, e apoie-se em um pool limpo para ser desafiado com menos frequência desde o início. Como um bloqueio no meio da sequência se disfarça de fim, uma melhor reputação de IP cumpre um papel duplo aqui: reduz o truncamento e, combinada com verificações de contagem esperada, torna visível o truncamento que você acaba sofrendo, em vez de silencioso.
Conclusão
Paginação não é a parte trivial, é onde a completude é ganha ou perdida. Identifique o mecanismo real e prefira paginação por cursor ou o endpoint JSON subjacente em vez de offset e HTML. Faça deduplicação por ID estável, nunca por posição. Para rolagem infinita, chame o fetch subjacente quando puder, e quando precisar renderizar, espere carregamentos reais em vez de temporizadores e extraia os itens conforme aparecem para que listas virtualizadas não devorem seus dados. Acima de tudo, trate “o carregamento parou” como suspeito até que uma verificação de contagem esperada ou uma cauda repetida prove que realmente era o fim, e execute cada consulta paginada em sua própria sessão sticky para que a sequência permaneça coerente.
Faça isso e você coletará a lista inteira, exatamente uma vez, e saberá que o fez. Direcione a sequência para um gateway residencial limpo para que a cauda não se transforme em uma parede de bloqueios, e o preço por GB permite rastrear paginação profunda sem um medidor por requisição trabalhando contra você.