🎯 Um navegador em nuvem personalizável e anti-detecção alimentado por Chromium desenvolvido internamente, projetado para rastreadores web e agentes de IA. 👉Experimente agora
De volta ao blog

Web Scraping com TypeScript: Extração Tipada com Cheerio e Node

Isabella Garcia
Isabella Garcia

Web Data Collection Specialist

21-Jul-2026

TL;DR:

  • O Node 22 executa TypeScript diretamente com node --experimental-strip-types, portanto, um scraper não precisa de etapa de build e nem de empacotador.
  • O fetch está embutido no runtime, deixando o cheerio como a única dependência para parse de HTML e seleção de CSS.
  • Tipar o registro que você extrai é o que torna um scraper sustentável: o compilador indica um campo renomeado no ponto em que é consumido, em vez de depois que os dados já foram processados.
  • Os tipos descrevem a forma que você espera, não a página que você recebeu — uma página renderizada pelo cliente ainda retorna uma resposta válida que se analisa em zero registros.
  • A Scrapeless Universal Scraping API renderiza a página primeiro, e os mesmos seletores cheerio inalterados retornam todos os 10 registros.
  • Comece no plano gratuito da Scrapeless e aponte o exemplo de pivô para seu próprio alvo.

O TypeScript ganha seu lugar em um scraper por uma razão: os dados que você extrai têm uma forma, e essa forma muda. Um site renomeia um campo, um seletor começa a retornar uma string vazia, e um scraper JavaScript simples transporta o problema silenciosamente para quem quer que o consuma. Um registro tipado transforma isso em um erro de compilação.

O que mudou recentemente é o custo de configuração. O Node 22 remove tipos nativamente, então não há etapa tsc, nem empacotador, e nem ts-node na árvore de dependências.

O Que Você Precisa

O scraping web com TypeScript em 2026 precisa do Node 22 ou posterior e uma única dependência. As versões abaixo são com as quais esses exemplos foram executados:

Componente Versão Função
Node.js 22.22.3 Runtime, fetch nativo, remoção de tipos nativa
cheerio 1.2.0 Análise de HTML e seletores CSS

O fetch está embutido no runtime, então não precisa de importação e nenhuma biblioteca HTTP. O cheerio fornece uma API com formato de jQuery sobre um documento analisado, que é a coisa mais próxima a um padrão para consultas HTML no lado do servidor no ecossistema Node. Ele analisa de acordo com a especificação de análise de HTML em vez de tratar marcação como texto para corresponder a padrões.

Instalar

Crie o projeto e adicione a única dependência:

bash Copy
mkdir ts-scraper && cd ts-scraper
npm init -y
npm pkg set type=module
npm install cheerio@1.2.0

A configuração type=module é importante: os exemplos abaixo usam await de nível superior, que requer sintaxe de módulo ES.

Extrair um Registro Tipado

Declare a forma primeiro, então faça a extração produzi-la. O compilador irá garantir isso:

typescript Copy
import * as cheerio from "cheerio";

interface Quote {
  text: string;
  author: string;
  tags: string[];
}

const res = await fetch("https://quotes.toscrape.com/");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const $ = cheerio.load(await res.text());

const quotes: Quote[] = $("div.quote")
  .map((_, el) => ({
    text: $(el).find("span.text").text(),
    author: $(el).find("small.author").text(),
    tags: $(el).find("a.tag").map((_, t) => $(t).text()).get(),
  }))
  .get();

console.log(`quotes parsed: ${quotes.length}`);
console.log(JSON.stringify(quotes[0], null, 2));

Execute com nenhuma etapa de build:

bash Copy
node --experimental-strip-types static.ts
text Copy
quotes parsed: 10
{
  "text": "“O mundo como o criamos é um processo do nosso pensamento. Ele não pode ser mudado sem mudar nosso pensamento.”",
  "author": "Albert Einstein",
  "tags": [
    "mudança",
    "pensamentos profundos",
    "pensamento",
    "mundo"
  ]
}

Três coisas estão fazendo trabalho real ali.

A anotação Quote em quotes é o que torna a callback do .map() verificada pelo tipo. Retorne um objeto faltando tags, ou escreva como tag, e o erro aparecerá naquela linha em vez de surgir mais tarde como undefined em quem consome o array.

res.ok é a verificação que as pessoas costumam ignorar. O fetch não lança um erro em um 404 ou um 403 — ele resolve normalmente com ok definido como false, e a página de erro analisa em zero correspondências exatamente como um resultado vazio. As classes de status que se comportam dessa maneira estão definidas em a especificação de semântica HTTP.

O .map(...).get() aninhado é o idiom do cheerio para transformar uma seleção em um verdadeiro array. A chamada interna coleta as strings das tags, então tags chega como string[] ao invés de um objeto cheerio.

Onde os Tipos Deixam de Ajudar

Um tipo descreve o registro que você espera, não a página que você recebeu. Nem fetch nem cheerio executam JavaScript, então em uma página que constrói seu conteúdo no navegador, os seletores não correspondem a nada e os tipos são satisfeitos por um array vazio.

O site acima publica um par renderizado pelo cliente dos mesmos dados em /js/. O mesmo código de análise, apontado para ele:

typescript Copy
import * as cheerio from "cheerio";

const res = await fetch("https://quotes.toscrape.com/js/");

se (!res.ok) throw new Error(HTTP ${res.status});
const html = await res.text();
const $ = cheerio.load(html);

console.log(bytes de html: ${html.length});
console.log(citações analisadas: ${$("div.quote").length});

text Copy
bytes de html: 5806
citações analisadas: 0

A solicitação foi bem-sucedida, res.ok era verdadeiro, e 5.806 caracteres de HTML válido foram analisados sem reclamações. Quote[] é um array vazio perfeitamente tipado. Este é o modo de falha que vale a pena projetar em torno, porque nada no sistema de tipos ou na camada HTTP o reporta — a marcação de citação é escrita no DOM após um script ser executado.

Renderizar Primeiro, Depois Analisar

A Scrapeless Universal Scraping API fecha essa lacuna renderizando a página em um navegador na nuvem e retornando o HTML resultante, de forma que o lado TypeScript fica uma chamada fetch tipada.

Defina sua chave:

bash Copy
export SCRAPELESS_API_KEY="sua_chave_api_aqui"

Apenas a camada de fetch muda — a interface Quote e o código seletor são idênticos ao primeiro exemplo:

typescript Copy
import * as cheerio from "cheerio";

interface Quote {
  text: string;
  author: string;
  tags: string[];
}

const res = await fetch("https://api.scrapeless.com/api/v2/unlocker/request", {
  method: "POST",
  headers: {
    "x-api-token": process.env.SCRAPELESS_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    actor: "unlocker.webunlocker",
    input: {
      url: "https://quotes.toscrape.com/js/",
      js_render: true,
      headless: true,
    },
  }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);

const envelope: { data: string } = await res.json();
const $ = cheerio.load(envelope.data);

const quotes: Quote[] = $("div.quote")
  .map((_, el) => ({
    text: $(el).find("span.text").text(),
    author: $(el).find("small.author").text(),
    tags: $(el).find("a.tag").map((_, t) => $(t).text()).get(),
  }))
  .get();

console.log(`bytes de html: ${envelope.data.length}`);
console.log(`citações analisadas: ${quotes.length}`);
console.log(`primeiro autor: ${quotes[0]?.author}`);
text Copy
bytes de html: 8940
citações analisadas: 10
primeiro autor: Albert Einstein

Mesma página, mesmos seletores, mesma interface. A contagem de registros muda de 0 para 10 e o payload cresce de 5.806 para 8.940 caracteres, e a única diferença é qual camada buscou o HTML.

Dois detalhes do TypeScript naquela chamada valem a pena ser copiados. Anotar o envelope como { data: string } impede que await res.json() espalhe any pelo resto do arquivo, que é onde a segurança de tipo geralmente vaza em um scraper. E quotes[0]?.author respeita o fato de que um índice de array pode ser undefined — com noUncheckedIndexedAccess habilitado, o compilador exige isso.

O campo data mantém o documento renderizado como uma string, razão pela qual vai direto para cheerio.load. As opções de renderização estão cobertas na documentação do Scrapeless, e o mesmo comportamento de js_render é explorado mais detalhadamente no guia de renderização JS.

Solução de Problemas

ERR_UNKNOWN_FILE_EXTENSION em um arquivo .ts. A flag --experimental-strip-types está ausente, ou o Node é mais antigo que 22. A remoção de tipos retira anotações no momento do carregamento; não verifica tipos, então execute tsc --noEmit separadamente quando você quiser a opinião do compilador.

Cannot use import statement outside a module. O pacote está faltando "type": "module". await de nível superior precisa de módulos ES.

A remoção de tipos rejeita um enum ou uma propriedade de parâmetro. Esses construtos emitem código de execução real em vez de serem apagáveis, então a remoção não pode lidar com eles. Use uma união de literais de string em vez de um enum e atribua campos de construtor explicitamente.

Os seletores correspondem no navegador, mas não no script. Compare contra a visualização do código-fonte em vez do inspetor. O inspetor mostra o DOM após os scripts terem sido executados, que não é o que o fetch recebeu — imprima o comprimento da resposta primeiro, como o exemplo acima faz.

Antes de apontar isso para um alvo ao vivo, verifique os termos do site e suas diretrizes /robots.txt, que seguem o padrão do protocolo de exclusão de robôs, e mantenha a coleta de dados somente públicos a um volume que o site possa atender confortavelmente.

Conclusão

TypeScript fornece a um scraper um contrato: declare o registro, e o compilador lhe diz quando a extração para de atender a ele. Com o Node 22 removendo tipos de forma nativa e fetch embutido, esse contrato custa uma dependência e nenhum passo de construção.
O que os tipos não podem lhe dizer é se a página que você buscou continha os dados. Essa verificação precisa ser explícita — um array vazio bem tipado é o que uma página renderizada pelo cliente retorna, e se parece exatamente com uma página sem resultados. Medir a diferença é um hábito que vale a pena manter: 10 registros renderizados pelo servidor, 0 no gêmeo JavaScript, 10 novamente uma vez que algo renderiza a página antes que o cheerio a veja.

Comece com o plano gratuito do Scrapeless para executar a etapa de renderização em seus próprios alvos e verifique a precificação atual do Scrapeless quando você dimensiona um trabalho.

Perguntas Frequentes

P: Eu preciso compilar TypeScript para raspar com ele?

Não. O Node 22 e versões posteriores executam arquivos .ts diretamente com node --experimental-strip-types, o que remove anotações de tipo no momento da carga. Isso significa que não há etapa de construção e nenhum empacotador para um scraper. A remoção de tipos não verifica tipos, então execute tsc --noEmit no CI quando você quiser que o compilador os verifique realmente.

P: Qual biblioteca de análise HTML devo usar com TypeScript?

O cheerio cobre a maior parte do trabalho — ele analisa com um analisador compatível com a especificação HTML e expõe uma API de seletor estilo jQuery com definições TypeScript incluídas. Busque um navegador sem cabeça ou uma API de renderização apenas quando o conteúdo for escrito no DOM por scripts, que nenhum analisador pode recuperar por conta própria.

P: O fetch lança um erro 404 no Node?

Não, e isso confunde as pessoas. O fetch resolve normalmente para qualquer resposta HTTP e só rejeita em caso de falha na rede, então você precisa verificar res.ok por conta própria. Sem essa verificação, uma página de erro é analisada para zero correspondências e é indistinguível de uma página que legitimamente não teve resultados.

P: Como mantenho os tipos honestos quando a resposta é JSON?

Anote na fronteira. await res.json() retorna any, então atribuí-lo a uma variável tipada como const envelope: { data: string } é o que impede que any se espalhe pelo resto do arquivo. Para dados não confiáveis da fonte upstream, valide em tempo de execução com uma biblioteca de esquema em vez de confiar apenas na anotação.

P: O TypeScript pode raspar uma página que renderiza no navegador?

Não por si só. A linguagem não influencia se o JavaScript é executado — o fetch retorna os bytes que o servidor enviou, e o exemplo acima mostra esses bytes sendo analisados para zero registros em uma página renderizada pelo cliente. A renderização deve acontecer em outro lugar, seja em um navegador sem cabeça que você opera ou através de uma API que retorna o DOM renderizado.

Na Scorretless, acessamos apenas dados disponíveis ao público, enquanto cumprem estritamente as leis, regulamentos e políticas de privacidade do site aplicáveis. O conteúdo deste blog é apenas para fins de demonstração e não envolve atividades ilegais ou infratoras. Não temos garantias e negamos toda a responsabilidade pelo uso de informações deste blog ou links de terceiros. Antes de se envolver em qualquer atividade de raspagem, consulte seu consultor jurídico e revise os termos de serviço do site de destino ou obtenha as permissões necessárias.

Artigos mais populares

Catálogo