---
title: "Web scraping com Cheerio no Node.js: tutorial passo a passo"
description: "O Cheerio analisa HTML no Node.js com seletores no estilo jQuery. Crie um scraper testado com fetch, paginação, limite de concorrência, retentativas e proxy."
url: https://proxynet.io/pt-br/blog/cheerio-web-scraping
date: 2026-09-28
author: "Acar Diveroli"
category: "Tutoriais, Web scraping"
lang: pt-BR
---

# Web scraping com Cheerio no Node.js: tutorial passo a passo

Sua equipe já trabalha com Node.js e alguém pede os títulos, preços e unidades em estoque de todos os livros de uma categoria de um catálogo. O código-fonte da página no navegador mostra os dados dentro de tags `<article>` comuns, então você não precisa de um navegador para lê-los. Você precisa de um jeito de baixar o HTML, escolher os elementos certos, seguir o link "next" e impedir que o script sobrecarregue o site ou pare no primeiro timeout. O caminho geral de uma página até um arquivo está em [Como extrair dados de um site](/pt-br/blog/extract-data-from-website); este tutorial faz isso em JavaScript com o Cheerio.

Vamos ver o que o Cheerio é e o que ele não é, como carregar HTML com `load` e `fromURL`, seletores e listas, o método `extract`, mais recente, paginação, um limite de concorrência, retentativas com backoff, gravação em JSON e o envio das requisições por um proxy com o `ProxyAgent` do undici. A última parte explica quando o Cheerio não é a ferramenta certa. Todos os exemplos foram executados em 28 de setembro de 2026 com cheerio 1.2.0, undici 8.11.2 e Node.js 24.11.1 contra o books.toscrape.com, um site de testes criado para praticar web scraping.

> **Nota: Resposta rápida**
>
> O Cheerio é uma biblioteca do Node.js que transforma HTML em uma árvore consultada com seletores CSS no estilo jQuery. Ele não executa JavaScript e, com exceção do helper `fromURL`, não baixa páginas. O padrão habitual é: baixar a página com `fetch`, passar o texto para `cheerio.load`, ler valores com `$(selector).text()` e `.attr()` e percorrer listas com `.map()` ou `.each()`. Para muitas páginas, siga o link "next", limite quantas requisições rodam ao mesmo tempo, repita timeouts e respostas 5xx ou 429 com uma espera crescente e salve as linhas em JSON. Para usar um proxy, importe `fetch` e `ProxyAgent` do mesmo pacote undici e passe o agente como `dispatcher`.

## O que é o Cheerio?

O Cheerio é um parser de HTML e XML para Node.js com uma API inspirada no jQuery. Você entrega a marcação, ele monta uma árvore do documento e você consulta essa árvore com `$("seletor css")`. Ele é rápido porque pula tudo o que um navegador faz depois do parsing: não calcula layout, não aplica CSS, não carrega imagens e não executa scripts.

A versão atual é a 1.2.0 ([cheerio no npm](https://www.npmjs.com/package/cheerio)) e exige Node.js 20.18.1 ou posterior. A versão 1.0, lançada em agosto de 2024, encerrou uma fase de release candidates que tinha começado em 2017. O pacote não tem exportação padrão, então você escreve `import * as cheerio from "cheerio"`. Tutoriais que chamam `require("cheerio").default` foram escritos para versões antigas.

O Cheerio só enxerga o HTML que o servidor enviou. Se a lista de produtos é preenchida depois por JavaScript, os dados não estão nesse HTML e nenhum seletor vai encontrá-los. [Páginas estáticas e dinâmicas](/pt-br/blog/static-vs-dynamic-pages) mostra como verificar que tipo de página você tem antes de escrever qualquer código.

## Como funciona um scraper com Cheerio?

Um scraper baseado no Cheerio repete os mesmos cinco passos em cada página:

1. **Baixar.** Um cliente HTTP (aqui, `fetch`) solicita a URL e recebe o HTML como texto.
2. **Analisar.** `cheerio.load(html)` monta a árvore e devolve uma função `$` ligada àquele documento.
3. **Selecionar.** `$("article.product_pod")` devolve todos os elementos correspondentes; `.find()`, `.text()` e `.attr()` leem dentro deles.
4. **Seguir.** O scraper lê a próxima URL na página (um link de paginação ou de detalhe) e a resolve em relação à URL atual.
5. **Armazenar.** As linhas se acumulam na memória e, no final, são gravadas em um arquivo ou banco de dados.

Os passos 2 e 3 nunca tocam a rede. Essa separação ajuda na depuração: se um seletor não retorna nada, salve o HTML em um arquivo e teste o seletor nele, sem enviar outra requisição.

## Cheerio vs jsdom vs Playwright

As três ferramentas mais comparadas para web scraping no Node.js fazem trabalhos diferentes:

| Ferramenta | O que faz | Executa o JavaScript da página | Custo por página | Uso indicado |
|---|---|---|---|---|
| Cheerio | Analisa HTML, consultas no estilo jQuery | Não | O menor: só parsing | HTML renderizado no servidor, grande volume de páginas |
| jsdom | Monta um DOM parecido com o do navegador no Node.js | Opcional, limitado | Maior que o Cheerio | Código que espera `document` e as APIs do DOM |
| Playwright | Controla um Chromium, Firefox ou WebKit de verdade | Sim | O maior: navegador completo | Páginas que montam o conteúdo com JavaScript, cliques, logins |

Uma configuração comum usa as duas pontas: Playwright para as poucas páginas que precisam de navegador e Cheerio para todo o resto. A escolha da linguagem é outra questão, tratada em [Extração de dados: JavaScript ou Python?](/pt-br/blog/web-scraping-javascript-vs-python).

## Instalando o Cheerio e carregando a primeira página

Crie um projeto e instale o pacote. Adicionar `"type": "module"` permite usar `import` e `await` no nível superior:

```bash
mkdir book-scraper && cd book-scraper
npm init -y
npm pkg set type=module
npm install cheerio
```

O Node.js 18 e versões posteriores já trazem `fetch`, então o primeiro script não precisa de mais nada:

```js
import * as cheerio from "cheerio";

const url = "https://books.toscrape.com/";
const response = await fetch(url, {
  headers: { "user-agent": "book-research/1.0 (+mailto:you@example.com)" },
});
if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);

const $ = cheerio.load(await response.text());

console.log($("title").text().trim());
console.log($("article.product_pod").length, "books on this page");

$("article.product_pod").slice(0, 3).each((i, el) => {
  const card = $(el);
  const title = card.find("h3 a").attr("title");
  const price = card.find(".price_color").text();
  console.log(i + 1, title, price);
});
```

```text
All products | Books to Scrape - Sandbox
20 books on this page
1 A Light in the Attic £51.77
2 Tipping the Velvet £53.74
3 Soumission £50.10
```

O título vem do atributo `title` do link, não do texto dele: neste site o texto visível do link aparece cortado ("In a Dark, Dark ...") e o atributo guarda o título completo. Confira os dois no código-fonte antes de escolher. O cabeçalho `user-agent` identifica o seu script e dá ao dono do site uma forma de falar com você.

### Métodos de carregamento

O Cheerio 1.x tem cinco formas de carregar um documento ([documentação de carregamento do Cheerio](https://cheerio.js.org/docs/basics/loading/)):

| Método | Entrada | Quando usar |
|---|---|---|
| `load(html)` | Uma string | Você mesmo baixou a página (o caso comum) |
| `loadBuffer(buffer)` | Bytes brutos | A codificação é desconhecida; o Cheerio a detecta |
| `stringStream(options, cb)` | Fluxo de texto decodificado | Arquivos grandes com codificação conhecida |
| `decodeStream(options, cb)` | Fluxo de bytes brutos | Arquivos grandes com codificação desconhecida |
| `fromURL(url, options)` | Uma URL | Scripts rápidos; o Cheerio baixa a página sozinho |

O `fromURL` é prático, mas abre o próprio cliente undici para a origem da página. No nosso teste, ele ignorou um `dispatcher` passado em `requestOptions` e se conectou diretamente, mesmo quando esse dispatcher apontava para um proxy que recusava todas as requisições. Para tudo o que precisa de proxy, retentativas ou timeouts, baixe com `fetch` e use `load`.

## Selecionando elementos e lendo valores

A maior parte do código de scraping usa uma pequena parte da API:

- `$(selector)` seleciona no documento inteiro; `el.find(selector)` busca dentro de um elemento.
- `.text()` devolve o texto combinado da seleção; `.attr("href")` devolve um atributo do primeiro elemento.
- `.each((i, el) => …)` percorre a seleção; `.map((i, el) => value).get()` transforma a seleção em um array comum.
- `.first()`, `.eq(n)` e `.slice(a, b)` restringem uma seleção.

Um seletor que não encontra nada não lança exceção. `.text()` devolve uma string vazia e `.attr()` devolve `undefined`, então um nome de classe alterado gera campos vazios em vez de um erro. Valide as linhas que você coleta (mais detalhes na lista de erros abaixo). A sintaxe dos seletores e o motivo de o Cheerio não ter XPath estão em [Seletor CSS vs XPath](/pt-br/blog/css-selector-vs-xpath).

### O método extract

O Cheerio 1.0 adicionou `$.extract()`, que descreve o registro inteiro como um único objeto ([documentação do extract do Cheerio](https://cheerio.js.org/docs/basics/extract/)). Uma string traz o texto da primeira correspondência, colchetes reúnem todas as correspondências e `{ selector, value }` lê uma propriedade ou executa uma função:

```js
import * as cheerio from "cheerio";

const $ = await cheerio.fromURL("https://books.toscrape.com/");

const data = $.extract({
  heading: "h1",
  books: [
    {
      selector: "article.product_pod",
      value: {
        title: { selector: "h3 a", value: "title" },
        price: ".price_color",
        link: { selector: "h3 a", value: "href" },
        rating: {
          selector: "p.star-rating",
          value: (el) => $(el).attr("class").replace("star-rating", "").trim(),
        },
      },
    },
  ],
});

console.log(data.heading, data.books.length);
console.log(data.books[0]);
```

```text
All products 20
{
  title: 'A Light in the Attic',
  price: '£51.77',
  link: 'catalogue/a-light-in-the-attic_1000/index.html',
  rating: 'Three'
}
```

Os seletores dentro de `value` são avaliados em relação a cada `article`, o que mantém juntos os campos de um mesmo livro. O link continua relativo, então resolva-o com `new URL(link, pageUrl)` antes de requisitá-lo.

## Um scraper completo: paginação, concorrência, retentativas e JSON

O script abaixo coleta todos os livros da categoria Mystery. Ele percorre as páginas da listagem seguindo o link "next", abre a página de detalhe de cada livro com no máximo quatro requisições em andamento, repete erros de rede, timeouts e respostas 429 e 5xx com backoff exponencial e grava `books.json`. Ele usa o `fetch` do undici para que o proxy opcional da próxima seção funcione sem mudanças. Instale com `npm install cheerio undici` (o undici 8 exige Node.js 22.19 ou posterior).

```js
import * as cheerio from "cheerio";
import { fetch, ProxyAgent } from "undici";
import { writeFile } from "node:fs/promises";

const START_URL =
  "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html";
const CONCURRENCY = 4;   // páginas de detalhe baixadas ao mesmo tempo
const MAX_RETRIES = 3;   // tentativas extras depois da primeira
const HEADERS = { "user-agent": "book-research/1.0 (+mailto:you@example.com)" };

// Proxy opcional: PROXY_URL=http://user:pass@pr.proxynet.io:8000
const dispatcher = process.env.PROXY_URL
  ? new ProxyAgent(process.env.PROXY_URL)
  : undefined;

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const backoff = (attempt) => 1000 * 2 ** (attempt - 1) + Math.random() * 250;

class HttpError extends Error {
  constructor(status, url) {
    super(`HTTP ${status} for ${url}`);
    this.status = status;
  }
}

async function fetchHtml(url) {
  for (let attempt = 1; ; attempt++) {
    let wait;
    try {
      const res = await fetch(url, {
        headers: HEADERS,
        dispatcher,
        signal: AbortSignal.timeout(15_000),
      });
      if (res.ok) return await res.text();
      const retryable = res.status === 429 || res.status >= 500;
      if (!retryable || attempt > MAX_RETRIES) throw new HttpError(res.status, url);
      const retryAfter = Number(res.headers.get("retry-after"));
      wait = retryAfter > 0 ? retryAfter * 1000 : backoff(attempt);
    } catch (err) {
      if (err instanceof HttpError || attempt > MAX_RETRIES) throw err;
      wait = backoff(attempt); // erro de rede ou timeout
    }
    console.warn(`retry ${attempt}/${MAX_RETRIES} in ${Math.round(wait)} ms: ${url}`);
    await sleep(wait);
  }
}

// Executa fn sobre items com no máximo `limit` chamadas em andamento.
async function mapLimit(items, limit, fn) {
  const results = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const i = next++;
      results[i] = await fn(items[i], i);
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
  return results;
}

function parseListPage(html, pageUrl) {
  const $ = cheerio.load(html);
  const books = $("article.product_pod")
    .map((_, el) => {
      const card = $(el);
      const link = card.find("h3 a");
      return {
        title: link.attr("title"),
        price: Number(card.find(".price_color").text().replace(/[^0-9.]/g, "")),
        rating: card.find("p.star-rating").attr("class").split(" ").pop(),
        url: new URL(link.attr("href"), pageUrl).href,
      };
    })
    .get();
  const nextHref = $("li.next a").attr("href");
  return { books, nextUrl: nextHref ? new URL(nextHref, pageUrl).href : null };
}

function parseDetailPage(html) {
  const $ = cheerio.load(html);
  const info = {};
  $("table.table-striped tr").each((_, row) => {
    info[$(row).find("th").text().trim()] = $(row).find("td").text().trim();
  });
  const stock = info["Availability"]?.match(/\((\d+) available\)/);
  return {
    upc: info["UPC"],
    inStock: stock ? Number(stock[1]) : 0,
    description: $("#product_description + p").text().trim(),
  };
}

// 1. Percorrer as páginas da listagem seguindo o link "next".
const listed = [];
for (let url = START_URL; url; ) {
  const { books, nextUrl } = parseListPage(await fetchHtml(url), url);
  listed.push(...books);
  console.log(`${url} -> ${books.length} books`);
  url = nextUrl;
}

// 2. Abrir cada página de detalhe, quatro por vez.
const books = await mapLimit(listed, CONCURRENCY, async (book) => {
  try {
    return { ...book, ...parseDetailPage(await fetchHtml(book.url)) };
  } catch (err) {
    console.error(`skipped ${book.url}: ${err.message}`);
    return { ...book, error: err.message };
  }
});

// 3. Salvar o resultado.
await writeFile(
  "books.json",
  JSON.stringify({ scrapedAt: new Date().toISOString(), count: books.length, books }, null, 2),
);
console.log(`saved ${books.length} books to books.json`);
```

```text
https://books.toscrape.com/catalogue/category/books/mystery_3/index.html -> 20 books
https://books.toscrape.com/catalogue/category/books/mystery_3/page-2.html -> 12 books
saved 32 books to books.json
```

Um registro de `books.json` (descrição encurtada):

```json
{
  "title": "Sharp Objects",
  "price": 47.82,
  "rating": "Four",
  "url": "https://books.toscrape.com/catalogue/sharp-objects_997/index.html",
  "upc": "e00eb4fd7b871a48",
  "inStock": 20,
  "description": "…"
}
```

O que cada parte faz:

- **Paginação.** O loop para quando a página não tem `li.next a`. O link na página 1 é `page-2.html`, relativo à pasta da categoria, e por isso toda URL passa por `new URL(href, pageUrl)`. Outros padrões (números de página na query string, cursores, APIs de "carregar mais") estão em [Paginação em web scraping](/pt-br/blog/pagination-web-scraping).
- **Limite de concorrência.** `mapLimit` inicia quatro workers que pegam o próximo item de um contador compartilhado. Um `Promise.all` sobre as 32 URLs enviaria 32 requisições de uma vez; com 1.000 URLs, o servidor veria isso como uma rajada. Quatro é um ponto de partida educado para um site pequeno.
- **Retentativas.** Só são repetidos os erros que podem se resolver sozinhos: falhas de rede, o timeout de 15 segundos, 429 e 5xx. Um 404 falha na hora. Um cabeçalho `Retry-After` numérico tem prioridade sobre a espera calculada; o backoff dobra a partir de mais ou menos um segundo e adiciona uma variação aleatória (jitter) para que os workers paralelos não repitam no mesmo instante. Por que o 429 acontece e como ler o cabeçalho está em [HTTP 429 Too Many Requests](/pt-br/blog/http-429-too-many-requests).
- **Falha parcial.** Uma página de detalhe que ainda falha depois de três retentativas vira uma linha com um campo `error`, em vez de interromper a execução. Depois você pode reprocessar só essas linhas.
- **JSON.** O arquivo traz `scrapedAt` e `count`, o que ajuda na comparação entre execuções. Para CSV, JSON Lines ou SQLite com upserts, veja [Como salvar dados de scraping em CSV, JSON e SQLite](/pt-br/blog/save-scraped-data-csv-json-sqlite).

## Usando um proxy com o Cheerio (undici ProxyAgent)

Neste script o Cheerio nunca abre uma conexão, então o proxy fica no cliente HTTP. Com o undici, você cria um `ProxyAgent` e o passa para o `fetch` como `dispatcher`. O script completo acima já faz isso quando `PROXY_URL` está definida:

```bash
PROXY_URL=http://user:pass@pr.proxynet.io:8000 node scrape-books.mjs
```

O undici monta o cabeçalho `Proxy-Authorization` a partir do usuário e da senha da URL e os decodifica antes, então caracteres especiais em uma senha precisam de codificação percentual ([documentação do ProxyAgent do undici](https://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md)). Para destinos HTTPS, o agente abre um túnel `CONNECT`, e o TLS até o site roda dentro dele.

Rodamos o script por um pequeno proxy local que exigia `user:pass` e registrava cada túnel. As 34 requisições (duas páginas de listagem e 32 páginas de detalhe) chegaram por um único `CONNECT books.toscrape.com:443`: o agente manteve o túnel aberto e o reutilizou. Com uma senha errada, o proxy respondeu 407 e o undici informou `Proxy response (407) !== 200 when HTTP Tunneling`. O script repetiu isso três vezes antes de desistir; uma senha errada nunca se corrige sozinha, então confira as credenciais em vez de aumentar o número de retentativas.

Se você preferir não adicionar o undici como dependência, o Node.js 24.5 e o 22.21 ganharam suporte nativo a proxy, que lê `HTTP_PROXY`, `HTTPS_PROXY` e `NO_PROXY` quando você define `NODE_USE_ENV_PROXY=1` ([suporte nativo a proxy do Node.js](https://nodejs.org/api/http.html#built-in-proxy-support)). A documentação o marca como em desenvolvimento ativo. No nosso teste com o Node.js 24.11.1, o `fetch` global comum passou pelo proxy local com esta configuração:

```bash
NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://user:pass@pr.proxynet.io:8000 node first-page.mjs
```

Axios e node-fetch usam agentes em vez de dispatchers; [Como usar proxy no Node.js](/pt-br/blog/nodejs-proxy) cobre os dois. Para trocar o IP de saída a cada requisição ou para sessões fixas que mantêm um IP por um tempo, [Proxies residenciais](https://proxynet.io/pt-br/residential-proxy) e [Proxies rotativos](https://proxynet.io/pt-br/rotating-proxy) aceitam a mesma URL `user:pass@host:port`.

## Quando o Cheerio não basta

O Cheerio não consegue clicar, rolar a página nem esperar uma requisição que a página faz depois de carregar. Sinais de que você precisa de um navegador:

- O código-fonte da página (Ctrl+U) não tem os dados que a página renderizada mostra.
- O HTML traz um contêiner vazio como `<div id="root"></div>` e um pacote de scripts grande.
- Os dados só aparecem depois de um formulário de login, de um banner de cookies ou de uma "rolagem infinita".

Antes de abrir um navegador, abra a aba Rede das ferramentas de desenvolvedor. Muitas páginas "dinâmicas" carregam os dados de um endpoint JSON, e requisitar esse endpoint com `fetch` é mais leve do que renderizar a página. Se você realmente precisar de um navegador, o [Playwright](/pt-br/blog/playwright-proxy) pode renderizar a página e entregar o HTML final ao Cheerio com `cheerio.load(await page.content())`, e o seu código de parsing continua o mesmo.

## Onde os scrapers com Cheerio são usados

- **Acompanhamento de preços:** ler preços de páginas de produto renderizadas no servidor em horários programados ([monitoramento de preços](/pt-br/price-monitoring)).
- **Catálogo e dados de mercado:** coletar linhas de produtos e níveis de estoque em várias lojas ([pesquisa de mercado](/pt-br/market-research)).
- **Visibilidade em buscadores:** verificar títulos, meta tags e cabeçalhos das suas próprias páginas ([proxy para SEO](/pt-br/seo-proxy)).
- **Pipelines de dados:** alimentar um crawler ou um job de ETL maior com linhas já analisadas ([extração de dados](/pt-br/data-scraping), [web crawler](/pt-br/web-crawler)).
- **Parsing de HTML salvo:** transformar páginas arquivadas em registros estruturados; a parte do parsing é explicada em [O que é parsing de dados?](/pt-br/blog/what-is-data-parsing).

## Erros comuns e como diagnosticá-los

- **Strings vazias por toda parte.** O seletor não encontrou nada, ou os dados são adicionados por JavaScript. Salve o HTML com `writeFile("page.html", html)` e procure nele um valor que você vê no navegador.
- **`require(...).default is not a function` ou `does not provide an export named 'default'`.** Estilo de importação antigo. Use `import * as cheerio from "cheerio"`.
- **`TypeError: fetch failed` com `invalid onRequestStart method`.** Você passou um `ProxyAgent` do pacote undici do npm para o `fetch` global do Node.js. O Node 24.11.1 embute o undici 7.16.0, e as duas versões não compartilham a interface de dispatcher. Importe `fetch` e `ProxyAgent` do mesmo pacote.
- **Um proxy que "não faz nada".** Você passou o agente para `cheerio.fromURL`, que usa o próprio cliente. Baixe com `fetch` e chame `cheerio.load`.
- **Links relativos falham.** `fetch("catalogue/…")` lança `Failed to parse URL`. Resolva com `new URL(href, pageUrl)`.
- **Requisições demais ao mesmo tempo.** `Promise.all(urls.map(fetch))` envia tudo em paralelo e atrai respostas 429. Use um limite como o `mapLimit`.
- **Desvio silencioso dos dados.** O site renomeia uma classe e os preços viram `NaN`. Verifique cada execução: conte as linhas, conte os preços `NaN` e pare se os números caírem de forma brusca.

Antes de aumentar a escala, leia o `robots.txt` e os termos do site, prefira uma API oficial quando houver e mantenha um ritmo de requisições moderado. [robots.txt explicado](/pt-br/blog/robots-txt) e [Web scraping é legal?](/pt-br/blog/is-data-web-scraping-legal) tratam das regras; [Web scraping sem bloqueios](/pt-br/blog/web-scraping-without-getting-blocked) trata do crawling educado.

## Guia de decisão

| Necessidade | Recomendação |
|---|---|
| Os dados estão no código-fonte da página | `fetch` + `cheerio.load` |
| Script avulso, sem proxy | `cheerio.fromURL` |
| Muitos registros com a mesma estrutura | `$.extract` com um descritor de array |
| Centenas de páginas | Um limite de concorrência de 2-5 mais retentativas com backoff |
| Requisições por um proxy | `fetch` do undici + `ProxyAgent`, ou `NODE_USE_ENV_PROXY=1` no Node.js 24.5+ |
| Os dados só aparecem depois que o JavaScript roda | Procure primeiro o endpoint JSON; se não houver, Playwright + Cheerio |
| O código espera um DOM completo (`document`, eventos) | jsdom |

## Perguntas frequentes

### O Cheerio ainda é mantido em 2026?

Sim. O registro do npm lista a versão 1.2.0, publicada em janeiro de 2026, como a mais recente, e o site de documentação cobre a API 1.x, incluindo `extract` e `fromURL`.

### O Cheerio executa JavaScript?

Não. Ele analisa a string HTML que você entrega e nada mais. Os scripts da página são tratados como texto. Para páginas que montam o conteúdo no navegador, use o Playwright ou encontre o endpoint de dados que a página chama.

### Preciso do Axios com o Cheerio?

Não. O Node.js 18 e versões posteriores incluem `fetch`, que cobre o que a maioria dos scrapers precisa. O Axios é questão de gosto; se você usá-lo, passe o corpo da resposta (`response.data`) para `cheerio.load`.

### Como fazer scraping de várias páginas com o Cheerio?

Leia o link da próxima página em cada página, resolva-o em relação à URL atual e repita o loop até o link não existir mais, como no script completo acima. Quando o número de páginas é conhecido, você também pode montar a lista de URLs antes e processá-la com um limite de concorrência.

### Como usar um proxy com o Cheerio?

Configure o proxy no cliente HTTP, não no Cheerio. Com o undici: `new ProxyAgent("http://user:pass@pr.proxynet.io:8000")`, passado como `dispatcher` para o `fetch` do undici. No Node.js 24.5 ou posterior, você pode em vez disso definir `NODE_USE_ENV_PROXY=1` e `HTTPS_PROXY`.

### O Cheerio é mais rápido que o Puppeteer ou o Playwright?

Para páginas cujos dados estão no HTML, sim, porque ele só analisa texto, enquanto um navegador também baixa recursos, executa scripts e calcula o layout da página. Não fizemos benchmark da diferença, e ela depende da página, então meça nos seus próprios alvos se os números importarem.

## Em resumo

O Cheerio transforma o HTML baixado em uma árvore que você consulta com seletores CSS e, na versão 1.x, acrescenta `fromURL` e `extract`. Um scraper confiável deixa o trabalho de rede fora do Cheerio: `fetch` com timeout, retentativas para 429, 5xx e erros de rede, um limite de concorrência pequeno e um arquivo JSON com data e hora. Quando as requisições precisam sair de outro IP ou de outro país, passe um `ProxyAgent` do undici como dispatcher e aponte-o para um [proxy da Proxynet](/pt-br/proxy).
