---
title: "O que é paginação e como percorrer todas as páginas?"
description: "A paginação divide uma lista longa em pedaços. Vemos os cinco tipos, as condições de parada e como retomar com SQLite uma coleta interrompida."
url: https://proxynet.io/pt-br/blog/pagination-web-scraping
date: 2026-09-19
author: "Acar Diveroli"
category: "Web scraping, Tutoriais"
lang: pt-BR
---

# O que é paginação e como percorrer todas as páginas?

O script que você escreveu para baixar um catálogo de mil produtos lê sem problemas os vinte produtos da primeira página. O trabalho de verdade começa depois: achar os endereços das 49 páginas restantes, perceber que a lista acabou, não salvar o mesmo produto duas vezes e não recomeçar do zero quando a conexão cai na página 31. A diferença entre o código que lê uma página e o que reúne a lista inteira chama-se paginação.

Neste artigo definimos a paginação em poucas linhas e depois olhamos para ela do lado de quem coleta: como reconhecer os cinco tipos nas ferramentas de desenvolvedor, de onde vem a próxima página, quando a coleta para, como filtrar os registros repetidos. No centro há uma fila de URLs sobre SQLite; uma coleta que matamos no meio do processo continuou, graças a essa fila, da página em que tinha parado. Rodamos o código nos sites de treino `books.toscrape.com` e `quotes.toscrape.com`.

> **Nota: Resposta rápida**
>
> Paginação é o servidor dividir uma lista longa em pedaços e enviar apenas um pedaço por requisição. Em web scraping tudo se resume a três decisões: de onde você vai saber a próxima página (um link "próxima", um número de página, `offset`, `cursor` ou o cabeçalho `Link`), quando você vai parar (sem link, página vazia, nenhum registro novo) e onde você vai anotar em que ponto parou. A resposta sólida para a última é uma tabela pequena de SQLite que atualiza os registros e a fila na mesma transação de banco de dados.

## O que é paginação?

Se um banco de dados tem dez mil registros, o servidor não manda todos em uma resposta só. A consulta fica pesada e a resposta incha com milhares de linhas que ninguém vai olhar. Em vez disso, a lista é dividida em pedaços de tamanho fixo e o cliente pede um pedaço de cada vez. Os links "1 2 3 ... 50" no rodapé de uma página de categoria, o parâmetro `?page=2` de uma API e os feeds que carregam conforme você desce são faces diferentes da mesma ideia.

Quem projeta a paginação pergunta qual método cansa menos o banco de dados. A pergunta de quem coleta é outra: qual método este site escolheu e como eu percebo isso de fora? O quadro geral da coleta em massa está na nossa página de [extração de dados](/pt-br/data-scraping), e o desenho dos rastreadores que vão de link em link, na de [web crawler](/pt-br/web-crawler).

## Quais são os tipos de paginação e como reconhecê-los?

O caminho para reconhecer é o mesmo em todos os casos: abra as ferramentas de desenvolvedor no navegador, limpe a aba Rede, passe para a segunda página e veja o que mudou. A barra de endereços mudou ou saiu uma requisição em segundo plano?

**1. URL com número de página.** O endereço muda para `?page=2`, `/page/2/` ou `page-2.html`. É o tipo mais fácil de reconhecer e o laço também é simples: você aumenta o número. O ponto fraco é que quase nunca se sabe qual é a última página.

**2. Link "próxima".** No rodapé da página há um elemento `<a>` apontando para a página seguinte. Você não gera o endereço, você o lê da página. Se não há link, a lista acabou. Seu código não quebra quando o site muda a estrutura de endereços; em listas HTML essa é a primeira escolha. A lógica dos seletores explicamos no artigo [Seletor CSS e XPath](/pt-br/blog/css-selector-vs-xpath).

**3. offset/limit.** Na requisição à API você vê dois parâmetros como `offset=40&limit=20`, ou `skip` e `take`: "pule os primeiros 40 registros e me dê 20". É o equivalente do número de página na API.

**4. Cursor.** Na resposta chega uma string de aparência sem sentido como `next_cursor`, `after` ou `next_page_token`, e na requisição seguinte você devolve essa string sem alterar. O cursor carrega a informação "o último registro que entreguei foi este"; você não tenta decifrá-lo, apenas o transporta.

**5. Rolagem infinita e "carregar mais".** A barra de endereços não muda. Quando você chega ao fim da página ou aperta o botão, aparece uma nova requisição Fetch/XHR na aba Rede. Ao olhar essa requisição, quase sempre você vê um dos três tipos acima. A rolagem infinita não é um método à parte, é uma interface montada em cima da paginação de uma API.

A isso é preciso somar a marca `rel="next"`. Ela aparece em dois lugares. O primeiro é o elemento `<link rel="next" href="...">` no `<head>` do HTML. [O Google escreve claramente que não usa mais essa tag](https://developers.google.com/search/docs/specialty/ecommerce/pagination-and-incremental-page-loading), mas muitos sites continuam imprimindo, e quando você a encontra ela é uma cópia limpa do link "próxima". O segundo é o cabeçalho `Link` da resposta HTTP; o formato é definido pela [RFC 8288](https://www.rfc-editor.org/rfc/rfc8288), e serviços como o GitHub entregam nesse cabeçalho o endereço completo da próxima página.

| Tipo | Como reconhecer | De onde vem a próxima página | Condição de parada | Armadilha |
|---|---|---|---|---|
| Número de página | `page=2`, `/page/2/` no endereço | Você aumenta o número | 404, lista vazia ou conteúdo repetido | O que acontece depois da última página varia por site |
| Link "próxima" | `<a>` no rodapé, `rel="next"` no `<head>` | Lido da página | Não há link | Esquecer de transformar o endereço relativo em absoluto |
| offset/limit | `offset`, `limit`, `skip` na requisição à API | `offset += limit` | Pedaço incompleto ou vazio | Os registros deslizam enquanto a lista muda |
| Cursor | `next_cursor`, `after`, um token na resposta | Transportado da resposta sem alterar | Cursor vazio ou ausente | O cursor expira e não dá para começar pelo meio |
| Rolagem infinita, "carregar mais" | O endereço não muda, sai uma requisição XHR | A regra da API que está por baixo | A regra da API ou a ausência de cartões novos | Rolar com o navegador sai caro sem necessidade |

## De que passos é feito um laço de paginação?

Seja qual for o tipo, o laço segue os mesmos seis passos:

1. **Coloque o endereço inicial na fila.** A página de categoria ou a primeira requisição da API.
2. **Pegue o próximo endereço e verifique se ele é permitido.** Se o `robots.txt` proíbe aquele caminho, a requisição nem sai.
3. **Espere e então envie a requisição.** Deixe um intervalo fixo entre duas requisições ao mesmo site.
4. **Extraia os registros.** Dê a cada registro uma chave que o identifique de forma única.
5. **Encontre a próxima página.** Link, número, offset ou cursor.
6. **Grave juntos os registros, o próximo endereço e a marca "esta página terminou".** Depois volte ao passo dois.

A palavra "juntos" do sexto passo é o assunto do resto do artigo. Primeiro, a condição de parada.

## Quando a coleta deve parar?

Perceber que a lista acabou é mais difícil do que parece, porque os sites se comportam de formas diferentes depois da última página. Nos sites de treino vimos três comportamentos distintos lado a lado. O `books.toscrape.com` tem 50 páginas e a requisição de `page-51.html` devolve `404`. Já `quotes.toscrape.com/page/11/` devolve com código `200` uma página sem nenhuma citação dentro. A API JSON do mesmo site escreve `"has_next": false` na última página e, se você pedir a página 11, recebe uma lista vazia. Em sites reais um quarto comportamento é mais comum: mostrar em silêncio de novo a primeira ou a última página quando o número está fora do intervalo.

Por isso não confie em uma única condição, use várias juntas:

- **Sinal explícito:** não há link "próxima", `has_next` é falso, o cursor está vazio, no cabeçalho `Link` não há `rel="next"`.
- **Pedaço vazio ou incompleto:** a página não tem nenhum registro, ou chegam menos registros que o valor de `limit`.
- **Nenhum registro novo:** todos os registros da página já foram vistos. É a condição que pega os sites que mostram a última página repetidas vezes.
- **Limite superior:** um número de páginas que não será ultrapassado de jeito nenhum. Um link "próxima" quebrado ou um cursor que se repete não conseguem jogar seu script num laço infinito.
- **Número total:** se a API dá `total` ou `total_pages`, use para conferir, não para parar.

Ao avançar por número de página, um `404` pode significar tanto "a lista acabou" quanto "a estrutura de endereços mudou". Se você recebe `404` logo na primeira página, é a segunda hipótese.

## Como filtrar os registros repetidos?

O mesmo registro chegar duas vezes durante a paginação não é um defeito, é o esperado. A causa mais frequente é a lista mudar enquanto você a percorre. Se, numa lista ordenada por "mais recentes", cinco produtos novos entram no topo justo quando você lê a terceira página, todos os registros descem cinco posições e os cinco primeiros registros da quarta página são os que você acabou de ver. Se um registro é apagado acontece o contrário: um sobe uma posição e você nunca chega a vê-lo. offset/limit e número de página estão sujeitos a esse deslize; o cursor não, porque diz "os que vêm depois deste registro". Produtos patrocinados e produtos listados em duas categorias também geram repetição.

A solução é filtrar a repetição no banco de dados, não no código:

- **Dê a cada registro uma chave estável.** O identificador do produto, o endereço do produto ou o campo `id` da API. Se não houver nenhum, gere um hash a partir dos campos que não mudam. O número de ordem na lista não serve como chave.
- **Faça da chave a chave primária e insira com `INSERT OR IGNORE`.** Se a mesma chave chega uma segunda vez, o SQLite pula a linha em silêncio. Esse comportamento está definido na documentação das [regras de conflito do SQLite](https://www.sqlite.org/lang_conflict.html). Se você quiser atualizar um campo que muda, como o preço, usa `ON CONFLICT ... DO UPDATE`.
- **Fixe a ordenação.** Se o site oferece opções de ordem, escolha um campo que não muda (por nome ou identificador em vez de data de inclusão). O deslize diminui.

## Paginação de API: offset, cursor e o cabeçalho Link

Os laços dos três tipos de API se parecem muito; a diferença está em como a próxima requisição é montada. As três funções abaixo produzem os registros um a um (`yield`), de modo que o código que chama não precisa saber de que tipo de paginação se trata. Os nomes dos campos (`items`, `next_cursor`) mudam de API para API; consulte a documentação do seu alvo.

```python
import time

import requests

def crawl_offset(session, url, limit=100, max_pages=500, delay=1.0):
    """offset/limit: para quando chega uma pagina incompleta ou vazia."""
    offset = 0
    for _ in range(max_pages):
        response = session.get(url, params={"offset": offset, "limit": limit}, timeout=20)
        response.raise_for_status()
        batch = response.json()["items"]
        yield from batch
        if len(batch) < limit:
            break
        offset += limit
        time.sleep(delay)

def crawl_cursor(session, url, max_pages=500, delay=1.0):
    """cursor: o cursor da resposta vai sem alteracao para a requisicao seguinte."""
    cursor, seen = None, set()
    for _ in range(max_pages):
        response = session.get(url, params={"cursor": cursor} if cursor else {}, timeout=20)
        response.raise_for_status()
        payload = response.json()
        yield from payload["items"]
        cursor = payload.get("next_cursor")
        if not cursor or cursor in seen:  # sem cursor ou ele se repete
            break
        seen.add(cursor)
        time.sleep(delay)

def crawl_link_header(session, url, max_pages=500, delay=1.0):
    """Cabecalho Link: o endereco rel="next" chega pronto, nada e calculado."""
    for _ in range(max_pages):
        response = session.get(url, timeout=20)
        response.raise_for_status()
        yield from response.json()
        url = response.links.get("next", {}).get("url")
        if not url:
            break
        time.sleep(delay)
```

Testamos as três contra uma API falsa local com 250 registros: cada uma reuniu os 250 registros em três requisições e sem repetição. Contra um endpoint quebrado que devolvia sempre o mesmo cursor, `crawl_cursor` parou depois da segunda requisição; sem o conjunto `seen` teria enviado 500 requisições. A função `crawl_link_header` rodamos também sobre a lista de tags de um repositório público do GitHub e recebemos 200 registros em duas páginas. O Requests analisa o cabeçalho `Link` por conta própria e o deixa no dicionário `response.links`. [A documentação de paginação do GitHub](https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api) recomenda o mesmo: não monte o endereço na mão, siga o endereço `rel="next"`.

## Como percorrer a rolagem infinita e o botão "carregar mais"?

O primeiro movimento não é a automação de navegador, é a aba Rede. A página `quotes.toscrape.com/scroll` é um bom exemplo: conforme você desce chegam citações novas e a cada vez sai uma requisição para `/api/quotes?page=2`, `page=3`. A resposta é JSON e tem dentro um campo `has_next`. Chamar essa requisição direto é mais rápido que rolar e pesa menos no site; imagens, fontes e scripts não são baixados. O exemplo de fila logo abaixo reúne as citações por esse caminho. O detalhe de como achar a requisição está na seção "Achar primeiro a requisição de API/XHR" do nosso artigo [Páginas estáticas e dinâmicas](/pt-br/blog/static-vs-dynamic-pages).

Se a requisição não pode ser repetida (carrega um parâmetro assinado, ou a resposta vem como fragmento HTML e é processada pelo script da página), passa-se para a automação de navegador. Ali a condição de parada vira "o número de cartões parou de crescer":

```python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://quotes.toscrape.com/scroll")
    page.wait_for_selector(".quote")

    count, idle = 0, 0
    while idle < 3 and count < 500:  # parar apos tres voltas sem cartoes novos ou ao atingir o limite
        page.mouse.wheel(0, 10000)
        page.wait_for_timeout(1000)
        current = page.locator(".quote").count()
        idle = idle + 1 if current == count else 0
        count = current

    print(count, "cartoes carregados")
    browser.close()
```

Esse script carregou os 100 cartões do site de treino. Com o botão "carregar mais" o laço é o mesmo, você só clica no botão em vez de usar a roda e também para quando o botão some da página. No Selenium o mesmo trabalho é feito com a chamada `execute_script("window.scrollTo(0, document.body.scrollHeight)")` e recontando os cartões; as diferenças entre as duas ferramentas estão no artigo [Playwright e Selenium](/pt-br/blog/playwright-vs-selenium). A rolagem tem um limite: em feeds longos a página mantém milhares de elementos na memória e fica lenta. Se você precisa de mais do que algumas centenas de cartões, vale procurar a requisição da API.

## Como retomar uma coleta interrompida: a fila de URLs em SQLite

Em listas curtas basta guardar o próximo endereço numa variável; a função que percorre categorias no nosso artigo [Monitoramento de preços da concorrência no e-commerce](/pt-br/blog/competitor-price-tracking) funciona assim, e para uma categoria de duas páginas é a escolha certa. Quando a lista chega a centenas de páginas, a variável não basta: a conexão cai, o computador hiberna, o servidor reinicia e o endereço guardado na variável some junto com o processo. Você precisa gravar em disco em que ponto parou.

Para isso não é preciso um servidor de filas separado. O SQLite, que vem com o Python, resolve com duas tabelas: `queue` guarda os endereços a percorrer e o estado deles (`pending`, `done`, `failed`, `blocked`), e `items` guarda os registros coletados. O truque é uma regra só: **os registros de uma página, o próximo endereço aprendido nessa página e a marca `done` da página são gravados na mesma transação de banco de dados.** A transação chega ao disco inteira ou não chega. Se o processo morre exatamente nesse momento, a página fica `pending` na fila e é lida de novo na execução seguinte. Não existe página que pareça "concluída" com os registros incompletos.

O script a seguir percorre dois tipos de paginação diferentes na mesma fila: o catálogo de livros seguindo o link "próxima" e as citações pela API JSON que está por trás da rolagem infinita.

```python
import hashlib
import json
import sqlite3
import time
from urllib.parse import urljoin, urlsplit
from urllib.robotparser import RobotFileParser

import requests
from bs4 import BeautifulSoup

DB_PATH = "crawl.db"
BOT_NAME = "ExampleCrawler"
USER_AGENT = f"{BOT_NAME}/1.0 (+https://example.com/bot)"
PROXY = None  # exemplo: "http://user:pass@pr.proxynet.io:8000"
DELAY = 1.0  # intervalo minimo entre duas requisicoes ao mesmo site (segundos)
MAX_PAGES = 200  # limite superior contra uma cadeia "proxima" quebrada
MAX_ATTEMPTS = 3

SEEDS = [
    ("https://books.toscrape.com/", "books"),
    ("https://quotes.toscrape.com/api/quotes?page=1", "quotes"),
]

SCHEMA = """
CREATE TABLE IF NOT EXISTS queue (
    url      TEXT PRIMARY KEY,
    kind     TEXT NOT NULL,
    status   TEXT NOT NULL DEFAULT 'pending',  -- pending | done | failed | blocked
    attempts INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS items (
    item_key TEXT PRIMARY KEY,
    kind     TEXT NOT NULL,
    data     TEXT NOT NULL,
    page_url TEXT NOT NULL
);
"""

def parse_books(response):
    """Lista HTML: os registros vem dos cartoes e a proxima pagina do link 'next'."""
    soup = BeautifulSoup(response.content, "html.parser")
    items = []
    for card in soup.select("article.product_pod"):
        link = card.select_one("h3 a")
        url = urljoin(response.url, link["href"])
        items.append((url, {"title": link["title"], "price": card.select_one(".price_color").text}))
    next_link = soup.select_one("li.next a")
    return items, urljoin(response.url, next_link["href"]) if next_link else None

def parse_quotes(response):
    """API JSON (a requisicao por tras da rolagem infinita): para quando has_next acaba."""
    payload = response.json()
    items = []
    for quote in payload["quotes"]:
        key = hashlib.sha1(quote["text"].encode("utf-8")).hexdigest()
        items.append((key, {"text": quote["text"], "author": quote["author"]["name"]}))
    next_url = None
    if payload["has_next"]:
        next_url = urljoin(response.url, f"?page={payload['page'] + 1}")
    return items, next_url

PARSERS = {"books": parse_books, "quotes": parse_quotes}
robots_cache = {}
last_request = {}

def allowed(session, url):
    """Le o robots.txt uma vez por site. 4xx: sem restricao, 5xx: nenhum caminho e percorrido (RFC 9309)."""
    root = "{0.scheme}://{0.netloc}".format(urlsplit(url))
    if root not in robots_cache:
        parser = RobotFileParser()
        response = session.get(root + "/robots.txt", timeout=20)
        if response.status_code >= 500:
            parser.disallow_all = True
        elif response.status_code >= 400:
            parser.allow_all = True
        else:
            parser.parse(response.text.splitlines())
        robots_cache[root] = parser
    return robots_cache[root].can_fetch(BOT_NAME, url)

def polite_get(session, url):
    """Nao pede ao mesmo site com intervalo menor que DELAY (ou que o Crawl-delay do robots.txt)."""
    host = urlsplit(url).netloc
    root = "{0.scheme}://{0.netloc}".format(urlsplit(url))
    delay = max(DELAY, float(robots_cache[root].crawl_delay(BOT_NAME) or 0))
    wait = last_request.get(host, 0) + delay - time.monotonic()
    if wait > 0:
        time.sleep(wait)
    try:
        return session.get(url, timeout=20)
    finally:
        last_request[host] = time.monotonic()

def crawl():
    db = sqlite3.connect(DB_PATH)
    db.executescript(SCHEMA)
    with db:  # na primeira execucao insere os enderecos iniciais, depois nao mexe
        db.executemany("INSERT OR IGNORE INTO queue (url, kind) VALUES (?, ?)", SEEDS)

    session = requests.Session()
    session.headers["User-Agent"] = USER_AGENT
    if PROXY:
        session.proxies = {"http": PROXY, "https": PROXY}

    for _ in range(MAX_PAGES):
        row = db.execute(
            "SELECT url, kind FROM queue WHERE status = 'pending' ORDER BY attempts, rowid LIMIT 1"
        ).fetchone()
        if row is None:
            break  # fila vazia: a coleta terminou
        url, kind = row

        if not allowed(session, url):
            with db:
                db.execute("UPDATE queue SET status = 'blocked' WHERE url = ?", (url,))
            continue

        try:
            response = polite_get(session, url)
            response.raise_for_status()
            items, next_url = PARSERS[kind](response)
        except (requests.RequestException, KeyError, ValueError) as exc:
            with db:
                db.execute(
                    "UPDATE queue SET attempts = attempts + 1,"
                    " status = CASE WHEN attempts + 1 >= ? THEN 'failed' ELSE 'pending' END"
                    " WHERE url = ?",
                    (MAX_ATTEMPTS, url),
                )
            print(f"ERRO  {url}: {exc}")
            continue

        # Os registros, o proximo endereco e a marca "pagina concluida" sao gravados numa transacao so.
        # Se o processo morre no meio deste bloco, nada e gravado e a pagina fica 'pending'.
        with db:
            before = db.total_changes
            db.executemany(
                "INSERT OR IGNORE INTO items (item_key, kind, data, page_url) VALUES (?, ?, ?, ?)",
                [(key, kind, json.dumps(data, ensure_ascii=False), url) for key, data in items],
            )
            new_items = db.total_changes - before
            # Condicao de parada: pagina vazia ou registros todos ja vistos
            if next_url and items and new_items:
                db.execute("INSERT OR IGNORE INTO queue (url, kind) VALUES (?, ?)", (next_url, kind))
            db.execute("UPDATE queue SET status = 'done' WHERE url = ?", (url,))
        print(f"OK    {url}: {len(items)} registros, {new_items} novos")

    for kind, status, count in db.execute(
        "SELECT kind, status, COUNT(*) FROM queue GROUP BY kind, status ORDER BY kind, status"
    ):
        print(f"fila     {kind:7} {status:8} {count}")
    for kind, count in db.execute("SELECT kind, COUNT(*) FROM items GROUP BY kind ORDER BY kind"):
        print(f"registro {kind:7} {count}")
    db.close()

if __name__ == "__main__":
    crawl()
```

Para rodar o script basta `pip install requests beautifulsoup4`. A retomada testamos assim: iniciamos o script e matamos o processo diretamente no segundo 14 (não com Ctrl+C). Naquele momento o banco tinha 20 páginas concluídas, 200 livros, 100 citações e um único endereço no estado `pending`: `page-11.html`. Rodamos de novo sem mudar nada; a coleta seguiu com `page-11.html` e terminou em menos de um minuto com 50 páginas de catálogo e 1.000 livros. Para a API de citações não foi nenhuma requisição, as dez páginas dela já estavam `done`. A terceira execução não achou nenhum endereço pendente e escreveu só o resumo.

Pontos do código que merecem atenção:

- **O bloco `with db:` é o limite da transação.** No módulo `sqlite3` do Python, quando a conexão é usada como gerenciador de contexto, a transação é confirmada se o bloco termina sem erro e desfeita se surge uma exceção. O detalhe está na [documentação do módulo](https://docs.python.org/3/library/sqlite3.html#sqlite3-connection-context-manager).
- **A chave primária da fila é o próprio endereço.** Se o mesmo endereço é descoberto uma segunda vez, `INSERT OR IGNORE` o pula; é assim que os links circulares se resolvem.
- **A condição "nenhum registro novo" não briga com a retomada.** Como os registros da página interrompida nunca foram gravados, ao ser lida de novo todos são novos e a corrente não quebra.
- **A página que falha vai para o fim da fila.** Graças ao `ORDER BY attempts`, os endereços nunca tentados são pegos primeiro; uma página que não pode ser lida em três tentativas vira `failed` e a coleta segue sem ela. O contador é propositalmente simples: o código de nova tentativa que decide em qual código de estado esperar e em qual parar, e que trata o cabeçalho `Retry-After`, já está pronto no nosso artigo [Códigos de status HTTP em web scraping](/pt-br/blog/http-status-codes-web-scraping) e pode entrar no lugar de `polite_get`.
- **Adicionar um site novo é escrever um analisador.** Você escreve uma função que devolve a lista de registros e o próximo endereço e a adiciona ao dicionário `PARSERS`; a fila, a espera e a lógica de retomada não mudam.

Essa fila é para um processo só. Se vários trabalhadores forem ler da mesma fila, o que pega um endereço precisa levá-lo para um estado intermediário como `claimed`, e esse estado precisa voltar a `pending` por tempo limite. Quando a coleta simultânea realmente ganha velocidade está no nosso artigo [Concorrência e paralelismo](/pt-br/blog/concurrency-vs-parallelism). Conforme o número de trabalhadores cresce, um framework pronto dá menos trabalho: o [Scrapy](/pt-br/blog/scrapy-proxy) já traz fila, filtro de repetidos e retomada por dentro.

## robots.txt, limite de requisições e proxy

A paginação é o trabalho em que você envia mais requisições seguidas a um mesmo site. A função `allowed` do script lê uma vez o `robots.txt` de cada site e pergunta a ele sobre cada endereço. Os casos em que o arquivo não é encontrado são regulados pela [RFC 9309](https://www.rfc-editor.org/rfc/rfc9309): uma resposta `4xx` conta como "não há arquivo, não há restrição", e diante de uma resposta `5xx` o rastreador é obrigado a considerar todos os caminhos proibidos. Nos dois sites de treino a requisição devolveu `404`. O motivo de buscarmos o arquivo com `session` é que assim a requisição sai com o User-Agent do script e, se houver, pelo proxy. O `urllib.robotparser` não aceita curingas (`*`, `$`); os limites estão no nosso artigo [O que é o arquivo robots.txt e como lê-lo](/pt-br/blog/robots-txt).

O `polite_get` deixa pelo menos `DELAY` segundos entre duas requisições ao mesmo site e, se o site indica `Crawl-delay`, toma esse valor como base; a espera é contada por site. Se você começar a ver `429`, a reação certa não é trocar de IP e sim aumentar o valor de `DELAY`; os motivos estão no nosso artigo [429 Too Many Requests](/pt-br/blog/http-429-too-many-requests). Escrever um User-Agent que apresenta o seu bot também faz parte do trabalho: [O que é User-Agent](/pt-br/blog/what-is-user-agent).

O proxy entra nesse quadro em dois pontos. O primeiro é a localização: para ver o catálogo e os preços mostrados a quem visita de Türkiye, a requisição precisa sair de Türkiye. O segundo é a distribuição de carga: em coletas longas espalhadas por vários sites usam-se [Proxies rotativos](https://proxynet.io/pt-br/rotating-proxy) para que o tráfego não se acumule em um único endereço. Aqui há uma armadilha. Resultados de busca e listas filtradas costumam depender de uma sessão no servidor; se o IP muda no meio da lista, o site pode te devolver para a primeira página ou entregar os mesmos registros de novo. Para percorrer uma lista do começo ao fim com o mesmo endereço de saída, abra uma sessão de [Proxies de sessão fixa](https://proxynet.io/pt-br/sticky-proxy) e troque de identidade quando a lista acabar. O mecanismo está no nosso artigo [O que é rotação de IP e como funciona](/pt-br/blog/ip-rotation-explained). Também preenchemos a linha `PROXY` e rodamos o script por um proxy de teste local com autenticação; todo o tráfego, inclusive o do `robots.txt`, passou pelo proxy e o resultado não mudou.

## Casos de uso

- **Coleta de categorias e catálogos:** reunir os endereços de produto das categorias da concorrência é o primeiro passo do monitoramento de preços; o fluxo completo está no nosso artigo [Monitoramento de preços da concorrência no e-commerce](/pt-br/blog/competitor-price-tracking) e a parte de infraestrutura, na página de [monitoramento de preços](/pt-br/price-monitoring).
- **Listagens de marketplace:** listas de vendedores e produtos chegam a milhares de páginas e ali a fila retomável é obrigatória. Para o ajuste de localização e sessão, veja nossa página de [proxy para e-commerce](/pt-br/e-commerce-proxy).
- **Dados em massa de APIs oficiais:** o cursor e o cabeçalho `Link` aparecem principalmente aqui. Como a sessão é transportada em APIs que exigem login está no nosso artigo [Sessões e cookies em Python](/pt-br/blog/python-login-session-cookies).
- **Trabalhos pequenos e pontuais:** não monte uma fila para uma tabela de cinquenta linhas; as opções sem código estão no nosso artigo [Como extrair dados de um site](/pt-br/blog/extract-data-from-website).

## Erros comuns

- **Deixar o número da última página fixo no código.** O catálogo cresce, 50 páginas viram 53 e as três últimas ficam de fora em silêncio. Escreva a condição de parada, não o número.
- **Não colocar limite superior.** Um elemento "próxima" que aponta para si mesmo ou um cursor que se repete faz seu script girar horas na mesma página.
- **Juntar um link relativo na mão.** No site de treino o link "próxima" é `catalogue/page-2.html` na primeira página e `page-3.html` na segunda. Um endereço montado somando strings quebra na segunda página; `urljoin` resolve os dois corretamente.
- **Gravar o ponto em que parou separado dos registros.** O código que escreve primeiro "página concluída" e só depois insere os registros perde aquela página de vez se morrer no meio. Inverter a ordem leva a repetições. As duas coisas precisam estar na mesma transação.
- **Filtrar repetições com um conjunto em memória.** Quando o processo reinicia, o conjunto está vazio; a unicidade é tarefa do banco de dados.
- **Seguir links escondidos.** O código que, procurando o link de paginação, joga todos os elementos `<a>` da página na fila também entra em links-armadilha invisíveis para pessoas. Selecione só o elemento de paginação; o detalhe está no nosso artigo [Armadilhas honeypot](/pt-br/blog/honeypot-traps).

## Guia de decisão

| Situação | Recomendação |
|---|---|
| A página tem um link "próxima" | Siga o link, use `urljoin` |
| Só há números de página e a última é desconhecida | Aumente o número; use juntas página vazia, `404` e "nenhum registro novo" |
| Aparece uma requisição JSON na aba Rede | Largue o navegador e chame a requisição direto |
| A API dá um cursor | Transporte o cursor sem alterar e guarde os já vistos num conjunto |
| A API dá um cabeçalho `Link` | Siga o endereço `response.links["next"]` |
| A requisição não pode ser repetida, há rolagem infinita | Role com Playwright ou Selenium e pare quando o número de cartões não crescer mais |
| A lista passa de 50 páginas ou a coleta leva minutos | Monte uma fila SQLite e grave registro e estado na mesma transação |
| A lista muda enquanto você a percorre | Chave estável, `INSERT OR IGNORE`, ordem fixa e uma segunda volta se preciso |
| Lista filtrada ou ligada a sessão, com proxy | Sessão sticky durante toda a lista e nova identidade no fim dela |

## Perguntas frequentes

### O que é paginação por cursor e qual a diferença para offset?

offset diz "pule tantos registros a partir do começo"; o cursor diz "me dê os que vêm depois deste registro". Com offset você pode saltar para a página que quiser, mas se a lista muda os registros deslizam e surgem repetições ou lacunas. Com cursor não há salto, você só avança em ordem; em troca, o ponto em que parou fica fixo mesmo se a lista mudar. A diferença prática: uma coleta com offset pode ser retomada da página 40, já com um cursor expirado talvez você tenha de começar do zero.

### O que é rolagem infinita?

É o JavaScript enviar uma nova requisição em segundo plano quando o fim da página se aproxima e acrescentar os registros que chegam ao pé da lista. Quem navega não vê números de página, mas a API por trás quase sempre trabalha com número de página, offset ou cursor. Em web scraping, o alvo é essa requisição de API.

### É possível saber de antemão qual é a última página?

Às vezes. A linha "Página 1 de 50" no rodapé, o campo `total_pages` na resposta da API ou o endereço `rel="last"` no cabeçalho `Link` dizem isso. Use essa informação para mostrar progresso e conferir o resultado. Ainda assim, encerre o laço pelas condições de parada, porque o total pode mudar durante a coleta.

### É possível percorrer páginas em paralelo?

Nos tipos número de página e offset sim, porque você consegue gerar os endereços de antemão. Nos tipos link "próxima" e cursor cada página depende da anterior e a corrente avança em ordem; o paralelismo só se monta entre listas diferentes (categorias). O paralelismo não elimina o limite de requisições: continue limitando a velocidade total de requisições ao mesmo site.

### Por que SQLite e não um arquivo CSV ou JSON?

Acrescentar a um arquivo é simples, mas não dá três coisas: controle de unicidade, gravação de "tudo ou nada" e uma fila consultável. O SQLite oferece as três num único arquivo, sem instalação. Terminada a coleta, despejar a tabela `items` em CSV são poucas linhas.

### O que acontece se o site mudar o número de registros por página durante a coleta?

Nos tipos link "próxima" e cursor não acontece nada, porque é o site que informa a próxima página. Nos tipos número de página e offset os limites deslizam e alguns registros podem chegar duas vezes enquanto outros nunca chegam. O filtro pela chave resolve o primeiro problema; para o segundo é preciso comparar com o número total e, se necessário, dar uma segunda volta.

## Em resumo

Na coleta com paginação o código responde a três perguntas: onde está a próxima página, quando a lista acabou e onde está anotado o ponto em que parei. Na primeira, siga o sinal que o site dá (link "próxima", cursor, cabeçalho `Link`) em vez de inventar endereços; na rolagem infinita, procure antes a requisição de API por trás. Na segunda, não confie em uma única condição e acrescente sempre "nenhum registro novo" e um limite de páginas. Na terceira, grave os registros e o estado da fila na mesma transação do SQLite; onde quer que o processo morra, a coleta continua da página em que parou. Mantenha a velocidade das requisições baixa e siga as regras do `robots.txt`. As opções de localização e distribuição de carga estão nos nossos [serviços de proxy](/pt-br/proxy).
