A melhor forma de fazer scraping de um feed de rolagem infinita geralmente é não rolar nada. Por trás da maioria dos feeds existe uma requisição paginada, muitas vezes baseada em cursor, e reproduzir essa requisição diretamente é mais rápido, mais barato e mais confiável do que controlar um navegador. Essa abordagem é detalhada em como fazer scraping de paginação e rolagem infinita de forma confiável.
Este guia é para os casos em que isso não funciona. A requisição é assinada com um token de curta duração. A resposta é um blob opaco que a página decodifica no lado do cliente. O feed só carrega quando um elemento entra na viewport. O botão “próxima página” está ligado a um estado que você não consegue reconstruir. Nesses casos, é preciso lidar com a rolagem e os cliques dentro de uma renderização real, e há algumas formas de isso dar errado.
Os exemplos usam a Shifter Web Scraping API, que executa a página em Chrome headless e aceita uma cadeia de ações de navegador antes da captura.
Quatro tipos de paginação dinâmica
Identifique qual você tem antes de escrever qualquer coisa, porque cada um exige uma técnica diferente.
| Padrão | O que aciona o próximo lote | Técnica |
|---|---|---|
| Feed acionado por rolagem | Um elemento perto do final entrando na viewport | Rolar até uma sentinela, esperar, repetir |
| Botão de carregar mais | Um clique em um botão que adiciona itens | Clicar, esperar novos itens, repetir |
| Paginação por estado de URL | A página atualiza a URL conforme você avança pelos resultados | Buscar essas URLs diretamente, sem rolagem |
| Lista virtualizada | Rolagem, mas linhas antigas são removidas do DOM conforme novas aparecem | Capturar em janelas, ou usar a requisição subjacente |
O terceiro vale a pena verificar primeiro porque é o mais barato de lidar. Role um feed em um navegador normal e observe a barra de endereço. Se um parâmetro de página ou offset muda, o site já forneceu uma URL paginada, e cada página é uma requisição comum.
O quarto é o que perde dados silenciosamente, e recebe sua própria seção mais abaixo.
Renderizando e esperando corretamente
Toda técnica dinâmica começa com os mesmos dois controles. render_js=1 executa a página em Chrome headless, pelo mesmo custo de um crédito por requisição bem-sucedida que uma busca estática. wait_for_css segura a captura até que um seletor exista no DOM, para que você nunca extraia de uma página que ainda não foi preenchida. Os controles de renderização estão documentados em rendering JavaScript.
Escolha o seletor de espera com cuidado. Esperar pelo container da lista não é suficiente, porque muitos frameworks renderizam um container vazio primeiro e o preenchem depois. Espere pelo primeiro item da lista.
Feeds acionados por rolagem: rolar até uma sentinela
Ações de navegador vão em js_instructions, um array JSON de passos executados em ordem antes da captura. As ações documentadas são scrollTo, click e wait.
O padrão para um feed acionado por rolagem é rolar até um elemento que fica depois da lista, esperar o próximo lote carregar e repetir. Como a lista cresce, esse elemento se move mais para baixo a cada vez, então rolar até ele novamente aciona o próximo carregamento.
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"]
Alguns detalhes aí são intencionais.
O banner de cookies é dispensado primeiro, porque uma sobreposição pode interceptar a rolagem ou o clique. A espera entre rolagens dá tempo para a requisição de rede se completar e o DOM ser atualizado; tempo curto demais e você captura antes do lote chegar. extract_rules com um tipo lista retorna cada card como um objeto JSON, então não há parser do seu lado, e requests codifica em URL ambos os parâmetros JSON para você. A sintaxe de extração está em extraction rules.
Teste a cadeia em uma página de amostra antes de executá-la em escala, e ajuste o número de passos de rolagem e a duração da espera de acordo com a rapidez com que aquele site realmente carrega.
Botões de carregar mais: clique, depois espere o crescimento
Um botão de carregar mais é o mesmo loop com um gatilho diferente:
[
{"action": "click", "selector": "button.load-more", "timeout": 3000},
{"action": "wait", "duration": 2000},
{"action": "click", "selector": "button.load-more", "timeout": 3000},
{"action": "wait", "duration": 2000}
]
Duas coisas costumam pegar as pessoas de surpresa. O seletor do botão frequentemente muda de estado enquanto carrega, ganhando uma classe desabilitada ou um spinner, então o segundo clique pode disparar antes que o botão fique clicável de novo, a menos que a espera seja longa o suficiente. E o botão geralmente desaparece quando não há mais nada para carregar, o que é um sinal útil de completude mas significa que sua cadeia precisa tolerar que os últimos cliques não tenham nada para acionar. De novo, teste no site real.
O orçamento de tempo de uma única renderização
Tudo acima acontece dentro de uma única sessão de navegador com um limite de tempo. wait_for_css expira após 30 segundos por padrão, e timeout limita quanto tempo o navegador pode gastar na página. Um feed com milhares de itens não terminará de carregar dentro de uma renderização, por mais passos de rolagem que você encadeie.
Então divida o problema em vez de alongar a cadeia.
Restrinja o conjunto de resultados. Filtros, ordenações e facetas de categoria geralmente produzem feeds menores. Vinte feeds restritos que carregam completamente cada um são mais confiáveis do que um feed enorme que nunca termina.
Pagine por URLs filtradas. Muitos sites combinam um filtro com estado de URL, o que transforma uma rolagem ilimitada em um conjunto finito de requisições comuns.
Recorra à requisição subjacente quando o feed for genuinamente longo e não puder ser restringido. Busque-o sem renderização; para endpoints que retornam JSON, auto_parser=1 retorna o corpo já interpretado.
Listas virtualizadas: a perda silenciosa de dados
Alguns feeds, principalmente os muito longos, usam virtualização de lista. Apenas as linhas próximas à viewport existem no DOM. Conforme você rola para baixo, as linhas do topo são removidas.
Role uma lista virtualizada até o final e capture, e você obtém a última tela de itens, não todos os itens pelos quais rolou. Nada dá erro. A extração retorna uma lista limpa, plausível e incompleta.
Detecte isso antes de confiar em qualquer resultado. Execute a cadeia de rolagem, depois verifique se os itens da primeira tela ainda estão presentes no resultado capturado. Se sumiram, a lista é virtualizada, e rolar-e-depois-capturar não vai funcionar. Use paginação por estado de URL ou a requisição subjacente em vez disso.
Paginação dinâmica entre requisições
Quando cada página é uma requisição separada que depende de estado do lado do servidor, como um cursor armazenado contra sua sessão, mantenha esse estado constante ao longo do percurso com session_id. Uma sessão persiste cookies, estado do navegador e o IP upstream entre requisições, e expira após 10 minutos de inatividade. Mantenha o country igual durante toda a vida de uma sessão, já que trocar no meio do percurso pode invalidar cookies vinculados ao locale. Os detalhes estão em sessões e proxies.
params.update({
"session_id": "shoes-walk-07",
"country": "de",
})
Mantenha um percurso em movimento. Uma sessão que fica ociosa enquanto seu código faz algo lento entre páginas vai expirar e quebrar o cursor.
Saber que você obteve tudo
Um scraper que para cedo demais parece exatamente com um scraper que terminou. Construa verificações de completude em cada execução.
- Compare com o total exibido. Muitos feeds mostram uma contagem de resultados. Se a página diz 1.284 e você extraiu 960, você parou cedo demais.
- Deduplique por uma chave estável, como o link ou o ID do item, nunca pela posição. Feeds de rolagem frequentemente re-renderizam itens, e a posição muda conforme o conteúdo é inserido.
- Verifique o marcador de fim. Um botão de carregar mais que desapareceu ou uma mensagem de fim de resultados é uma confirmação positiva. Sua ausência após o último passo significa que a cadeia foi curta demais.
- Acompanhe a completude ao longo do tempo. Uma categoria que rendeu 1.200 itens ontem e 400 hoje geralmente mudou seu markup ou seu comportamento de carregamento, não seu inventário.
Créditos e latência: escolhendo a abordagem por site
| Abordagem | Créditos | Latência | Risco de confiabilidade |
|---|---|---|---|
| Reproduzir a requisição subjacente | Nenhum se enviada diretamente; um por página via a API | Baixa | Requisições assinadas ou opacas |
| Paginação por estado de URL | Um por página | Baixa a moderada | Precisa de uma URL paginada |
| Cadeia de rolagem ou clique em uma renderização | Um para a cadeia inteira | Alta | Orçamento de tempo, virtualização |
Uma renderização que rola por vários lotes custa um crédito, enquanto reproduzir os mesmos lotes como requisições separadas custa um crédito cada. A troca é latência e o orçamento de tempo: cadeias longas são mais lentas e mais propensas a expirar. Use cadeias de rolagem para feeds moderados onde a requisição subjacente é impraticável, e recorra às outras duas para todo o resto.
Perguntas frequentes
Devo sempre renderizar JavaScript para rolagem infinita?
Não. Verifique primeiro se há paginação por estado de URL e uma requisição subjacente reproduzível. Renderizar é o recurso alternativo, não o padrão.
Quantos passos de rolagem a cadeia deveria ter?
Tantos quantos o site precisar para carregar os lotes que você quer dentro do orçamento de tempo. Meça em uma página de amostra, e se precisar de mais do que o orçamento permite, restrinja o feed em vez disso.
Por que minha extração retorna menos itens do que eu rolei?
Quase sempre virtualização de lista, onde linhas saem do DOM conforme você rola. Verifique se os itens da primeira tela sobrevivem até a captura.
Cada passo de rolagem custa um crédito?
Não. A cadeia inteira roda dentro de uma requisição, e uma requisição bem-sucedida é um crédito.
Conclusão
Rolagem infinita é paginação por cursor com um navegador na frente, e quando você pode alcançar o cursor diretamente, deveria fazê-lo. Quando não pode, lide com isso dentro da renderização: espere pelo primeiro item em vez do container, role até uma sentinela ou clique em carregar mais com espera suficiente entre os passos, respeite o orçamento de tempo restringindo o feed, teste para virtualização antes de confiar em uma captura, e verifique a completude contra os totais do próprio site.
Para decidir quando uma requisição de API renderizada é a ferramenta certa em primeiro lugar, veja quando você precisa de uma web scraping API para sites com muito JavaScript. O produto está na página web scraping API com renderização JS, com planos na página de preços.