C# Web Scraping: Um Guia Prático
Expert Network Defense Engineer
TL;DR:
HttpClientmais AngleSharp é a combinação moderna de C#. AngleSharp aceita seletores CSS, entãodoc.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.ConnectAsyncnunca 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.
ConnectAsyncaceitou um endpointwss://carregando uma credencial de string de consulta e retornoutitle=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
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
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
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
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
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.
Escalada Dois: Um Navegador Remoto Sem Baixar Um
Aqui é onde o C# se destaca notavelmente mais do que as instruções habituais sugerem. Cada PuppeteerSharp quickstart começa assim:
csharp
// 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
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
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.



