De volta ao blog

C# Web Scraping: Um Guia Prático

Michael Lee
Michael Lee

Expert Network Defense Engineer

11-Aug-2026

TL;DR:

  • HttpClient mais AngleSharp é a combinação moderna de C#. AngleSharp aceita seletores CSS, então doc.QuerySelectorAll("div.quote") se comporta da mesma forma que o mesmo seletor se comporta em um console de navegador.
  • A API Universal Scraping da Scrapeless responde com um envelope JSON. A marcação chega dentro de data, então você lê o JSON primeiro e faz o parse do HTML em segundo lugar.
  • O primeiro passo usual do PuppeteerSharp baixa um navegador. Conectar-se a um remoto ignora isso completamente. Cada quickstart começa com BrowserFetcher().DownloadAsync(); Puppeteer.ConnectAsync nunca o chama. Medido: o projeto de verificação terminou em 7.6 MB sem Chromium no disco e sem cache de navegador PuppeteerSharp.
  • O caminho CDP remoto funciona sem cerimônia. ConnectAsync aceitou um endpoint wss:// carregando uma credencial de string de consulta e retornou title=Quotes to Scrape, quotes on page=10.
  • Comece grátis: o painel Scrapeless emite uma chave que funciona com cada exemplo abaixo.

O Que Você Precisa

Tudo abaixo foi executado no .NET SDK 8.0.129 contra quotes.toscrape.com, um site publicado para prática de scraping.

bash Copy
dotnet new console -o scraper
cd scraper
dotnet add package AngleSharp        # 1.7.1
dotnet add package PuppeteerSharp    # 25.5.0

HttpClient e System.Text.Json estão na biblioteca de classe base, então as únicas dependências são o parser e, mais tarde, o driver do navegador.

Sobre a escolha do parser: HtmlAgilityPack é o nome que a maioria do material de scraping em C# alcança, e funciona. AngleSharp é uma preferência recomendada para código novo porque implementa o DOM W3C e aceita seletores CSS diretamente, o que significa que seletores que você copia das ferramentas de desenvolvimento do navegador funcionam inalterados em vez de precisarem ser traduzidos para XPath.

Buscando e Fazendo Parse

csharp Copy
using AngleSharp.Html.Parser;
using System.Text;

var http = new HttpClient();
http.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (compatible; csharp-guide/1.0)");

var res  = await http.GetAsync("https://quotes.toscrape.com/");
var html = await res.Content.ReadAsStringAsync();
Console.WriteLine($"http={(int)res.StatusCode} bytes={Encoding.UTF8.GetByteCount(html)}");

var parser = new HtmlParser();
var doc    = await parser.ParseDocumentAsync(html);
var quotes = doc.QuerySelectorAll("div.quote");

Console.WriteLine($"quotes={quotes.Length}");
Console.WriteLine($"first_author={quotes[0].QuerySelector("small.author")!.TextContent}");
Console.WriteLine($"first_text={quotes[0].QuerySelector("span.text")!.TextContent[..40]}");
text Copy
http=200 bytes=11064
quotes=10
first_author=Albert Einstein
first_text=“The world as we have created it is a pr

Três detalhes merecem destaque.

Defina um agente de usuário. HttpClient não envia nenhum por padrão, e um pedido sem agente de usuário é uma das coisas mais baratas para um site tratar de forma diferente.

Encoding.UTF8.GetByteCount(html) não é html.Length. Strings do .NET são UTF-16, então Length conta unidades de código UTF-16, não bytes na rede. Nesta página, os dois diferem porque as citações usam aspas curvas. Relate o que você quer dizer, mas não confunda-os ao comparar uma busca direta contra uma resposta de API.

Limite as consultas filho à linha. quotes[0].QuerySelector(...) busca dentro desse elemento; chamar doc.QuerySelector(...) dentro de um loop retorna o valor da primeira citação em cada iteração, produzindo um conjunto de dados que parece completo e está uniformemente errado.

Onde O C# Simples Para

O script funciona porque o alvo renderiza do lado do servidor e não bloqueia chamadores. Duas coisas acabam com isso: conteúdo que existe apenas depois que o JavaScript é executado, e sites que se recusam a servir um cliente HTTP básico. Nenhum deles é uma limitação do .NET — nenhum cliente HTTP em nenhuma linguagem lida com isso.

A partir daqui, as escaladas são ferramentas diferentes em vez de melhores, e o caminho simples continua sendo o mais barato onde funciona. Para a forma geral do que um navegador adiciona, o explicador de automação de navegador cobre a categoria.

Escalada Um: A API Universal Scraping

Isto mantém seu código como um simples HttpClient e move a dificuldade para o lado do servidor.

csharp Copy
using System.Text;
using System.Text.Json;
using AngleSharp.Html.Parser;

var key = Environment.GetEnvironmentVariable("SCRAPELESS_API_KEY")!;

var payload = JsonSerializer.Serialize(new
{
    actor = "unlocker.webunlocker",
    input = new { url = "https://quotes.toscrape.com/", js_render = false }
});

using var req = new HttpRequestMessage(
    HttpMethod.Post, "https://api.scrapeless.com/api/v2/unlocker/request")
{
    Content = new StringContent(payload, Encoding.UTF8, "application/json")
};
req.Headers.Add("x-api-token", key);

var apiRes = await http.SendAsync(req);
using var json = JsonDocument.Parse(await apiRes.Content.ReadAsStringAsync());

var keys = string.Join(",", json.RootElement.EnumerateObject().Select(p => p.Name));
Console.WriteLine($"http={(int)apiRes.StatusCode} envelope_keys={keys}");

var apiHtml = json.RootElement.GetProperty("data").GetString()!;
var apiDoc  = await new HtmlParser().ParseDocumentAsync(apiHtml);
Console.WriteLine($"quotes={apiDoc.QuerySelectorAll("div.quote").Length}");
text Copy
http=200 envelope_keys=code,data
quotes=10

O envelope é a parte a ser internalizada. O corpo da resposta é JSON e a marcação vive em data. Entregar o corpo bruto a ParseDocumentAsync produz um documento sem elementos correspondentes e não lança nada — seletores tranquilamente retornam zero resultados. Leia o JSON, pegue data, então faça o parse, e mantenha esse unwrap em um único método.

Observe req.Headers.Add("x-api-token", key) em vez de um cabeçalho Authorization. É um cabeçalho personalizado, então vai em HttpRequestMessage.Headers diretamente; tentar expressá-lo como um cabeçalho de autenticação tipado é um desvio que não se aplica aqui.

Aqui é onde o C# se destaca notavelmente mais do que as instruções habituais sugerem. Cada PuppeteerSharp quickstart começa assim:

csharp Copy
// The standard first step — downloads a Chromium build before anything runs.
await new BrowserFetcher().DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true });

Puppeteer.ConnectAsync não precisa de nada disso. Você está se conectando a um navegador que já existe em outro lugar, então BrowserFetcher nunca está envolvido e nada chega ao disco:

csharp Copy
using PuppeteerSharp;

var key = Environment.GetEnvironmentVariable("SCRAPELESS_API_KEY")!;

await using var browser = await Puppeteer.ConnectAsync(new ConnectOptions
{
    BrowserWSEndpoint = $"wss://browser.scrapeless.com/api/v2/browser?token={key}"
});

var page = await browser.NewPageAsync();
await page.GoToAsync("https://quotes.toscrape.com/");

Console.WriteLine($"title={await page.GetTitleAsync()}");
var n = await page.EvaluateExpressionAsync<int>("document.querySelectorAll('.quote').length");
Console.WriteLine($"quotes on page={n}");
text Copy
title=Quotes to Scrape
quotes on page=10

Duas coisas que isso lhe proporciona além da conveniência.

O projeto permanece pequeno. Após executar todos os exemplos neste artigo, o projeto de verificação mediu 7.6 MB no disco, não continha Chromium e não deixou nenhum cache de navegador PuppeteerSharp. Um fluxo de trabalho LaunchAsync adiciona uma construção de navegador a cada máquina e a cada imagem de contêiner que executa o código.
EvaluateExpressionAsync<T> desserializa para você. Pedir <int> retorna um int, então não há JsonElement para descompactar e nenhuma análise manual entre o navegador e sua variável.

Vale a pena saber: ConnectAsync aceitou um wss:// endpoint carregando sua credencial na string de consulta sem nenhuma configuração adicional. Isso não é universal entre os clientes CDP de diferentes linguagens — alguns descartam a string de consulta ou omitem a negociação do nome do servidor TLS — então, se você está portando de outra pilha, este trecho provavelmente será mais fácil do que o que você deixou.

Escolhendo Entre os Três

abordagem use quando custo
HttpClient + AngleSharp HTML renderizado no servidor, alvo permissivo mais baixo; um pacote
API de Scraping Universal bloqueado ou desafiado, sem JS necessário uma chamada HTTP, envelope para descompactar
PuppeteerSharp + um navegador remoto conteúdo requer execução de JavaScript uma sessão por trabalho, mas sem navegador no disco

Trabalhe para baixo na lista em vez de para cima. Uma página que parece exigir um navegador muitas vezes incorpora seus dados em uma tag <script>, e um QuerySelector mais JsonDocument.Parse supera uma sessão do navegador em todos os eixos.

Conclusão

C# é uma linguagem de scraping confortável e a biblioteca padrão cobre mais do que os tutoriais do ecossistema sugerem. HttpClient e AngleSharp lidam com páginas renderizadas no servidor com seletores CSS que você pode colar das devtools, e as duas armadilhas que vale a pena lembrar são contar unidades UTF-16 quando você quis dizer bytes, e consultar o documento quando você quis dizer a linha.

A história do navegador é melhor do que a documentação sugere. A etapa de download em cada rápido início é uma propriedade de lançar um navegador local, não de PuppeteerSharp, e ConnectAsync a remove junto com a pegada em disco — um projeto de 7,6 MB que ainda aciona um Chrome real.

Pronto para Extrair com C#?

Crie uma chave no painel do Scrapeless e execute o exemplo HttpClient contra uma página que você já coleta. Se retornar o que você espera, você está terminado — um pacote, sem navegador. A API de Scraping Universal cobre as páginas onde ela não o faz, e as tarifas atuais estão na página de preços.

FAQ

P: AngleSharp ou HtmlAgilityPack?

Ambos analisam HTML do mundo real bem. AngleSharp implementa o DOM W3C e aceita seletores CSS, então os seletores de devtools são portados sem alteração; HtmlAgilityPack é XPath-primeiro e tem uma história mais longa. Para código novo, AngleSharp geralmente significa menos trabalho de tradução.

P: Preciso baixar o Chromium para usar PuppeteerSharp?

Apenas se você lançar um navegador local. BrowserFetcher().DownloadAsync() pertence ao caminho LaunchAsync; ConnectAsync se conecta a um navegador que já existe e nunca o toca. O projeto de verificação aqui ficou em 7,6 MB sem navegador em disco.

P: Por que minha análise não retorna nada quando a chamada da API foi bem-sucedida?

Você está analisando o envelope. A resposta é JSON com a marcação dentro data, então AngleSharp recebe uma string JSON, não encontra nada e não lança nada. Leia data do JSON primeiro.

P: Por que a contagem de bytes difere do comprimento da string?

As strings do .NET são UTF-16, então string.Length conta unidades de código enquanto Encoding.UTF8.GetByteCount conta bytes como transferidos. Qualquer página com caracteres não-ASCII — incluindo aspas tipográficas — mostrará uma diferença.

P: Devo reutilizar HttpClient?

Sim. Crie um e compartilhe-o, ou use IHttpClientFactory em uma aplicação hospedada. Construir um novo HttpClient por solicitação esgota soquetes sob carga — as próprias diretrizes HttpClient da Microsoft explicam por quê — e isso se aplica a scrapers exatamente como se aplica a qualquer outra carga de trabalho HTTP do .NET.

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