Saltar para o conteúdo
Comece grátis
Voltar ao blogue

Web scraping em C# com HttpClient, AngleSharp e Playwright

Sorin-Gabriel MaricaÚltima atualização em 30 min read
Web scraping em C# com HttpClient, AngleSharp e Playwright
Resumo: Para web scraping em C#, comece no .NET 10 com o HttpClient integrado para descarregar páginas e o AngleSharp para analisar HTML com seletores CSS. Escolha o HtmlAgilityPack se preferir XPath ou mantiver código mais antigo, e acrescente o Playwright para .NET quando o JavaScript constrói a página. Este guia constrói um scraper com novas tentativas educadas, rastreio limitado, exportação para CSV e JSON e testes offline.

A extração de dados da Web em C# começa com um problema comum: os dados de que precisa encontram-se numa página Web e pretende obtê-los como objetos C# tipados, sem sair do .NET. No .NET 10, a plataforma já inclui a maior parte do que precisa, e as poucas peças que faltam são pacotes NuGet maduros.

Este guia mostra, passo a passo, como criar um pequeno scraper. Irá analisar uma página com o AngleSharp (ou o HtmlAgilityPack, se preferir o XPath), fazer a recolha de dados de forma educada com um HttpClient que repete as tentativas de resposta 429 da forma correta, percorrerá listas paginadas com concorrência limitada e exportará para CSV e JSON. Em seguida, irá extrair dados gerados por JavaScript, renderizar páginas com o Playwright para .NET, encaminhar páginas bloqueadas para uma API de scraper sem alterar o seu analisador e testar tudo isso offline. Os exemplos utilizam dois sites públicos de treino criados para a extração de dados: o «Books to Scrape» e a /js/ versão do Quotes to Scrape.

Início rápido — Instale o SDK do .NET 10 e crie uma aplicação de consola com dotnet new console. - Adicione um analisador: dotnet add package AngleSharp. - Obtenha os dados com HttpClient.GetStringAsync(), selecione com QuerySelectorAll(), e extraia com TextContent e GetAttribute(). - Utilize o Playwright para .NET quando o JavaScript construir a página.

O que é necessário para fazer web scraping em C#?

É necessário o SDK do .NET 10, o HttpClient (integrado) para descarregar páginas e um analisador de HTML, como o AngleSharp ou o HtmlAgilityPack, para extrair os dados. Adicione o Microsoft.Extensions.Http.Resilience para tentativas de repetição, o CsvHelper para ficheiros CSV e o Playwright para .NET quando a página for construída em JavaScript. Todo o resto já vem incluído no .NET.

Um scraper web em C# descarrega uma página, identifica as partes que lhe interessam e guarda-as. O HttpClient e o System.Text.Json vêm incluídos no .NET, e os pacotes abaixo preenchem as lacunas.

Comece com o .NET 10. Esta é a versão atual com suporte a longo prazo, com suporte até ao final de 2028. Tanto o .NET 8 como o .NET 9 chegam ao fim do suporte a 10 de novembro de 2026, de acordo com a política de suporte do .NET, pelo que os tutoriais dedicados a estas versões estão prestes a ficar desatualizados.

Bibliotecas de web scraping em C# (versões NuGet a partir de outubro de 2026)

Biblioteca

Função

Utilize-a quando

NuGet (outubro de 2026)

HttpClient

Descarregar páginas e respostas de API

Sempre (integrado no .NET)

Incluído no .NET 10

AngleSharp

Analisador HTML baseado em padrões com seletores CSS

Copie os seletores a partir do DevTools

1.8.6

HtmlAgilityPack

Analisador HTML tolerante com XPath e LINQ

Prefere o XPath ou mantém código mais antigo

1.13.1

Microsoft.Extensions.Http.Resilience

Re-tentativas, tempos de espera e Retry-After para o HttpClient

Mais do que um punhado de pedidos

10.11.0

CsvHelper

Escrever e ler CSV

Exportação de linhas

34.0.0 (nova API; consulte a secção CSV)

Playwright para .NET

Chromium, Firefox e WebKit sem interface gráfica

Páginas criadas em JavaScript ou interativas

1.64.1

PuppeteerSharp

Chromium sem interface gráfica com uma API ao estilo do Puppeteer

Portação de scripts do Node Puppeteer

25.15.0

Selenium.WebDriver

Automação do navegador através do WebDriver

Testes Selenium existentes ou Grid

4.52.0

ScrapySharp, HtmlAgilityPack.CssSelectors.NetCore

Helpers mais antigos

Não comece a escrever código novo com eles

Últimas versões de 2018 e 2019

Dois nomes na última linha ainda aparecem em muitos tutoriais de scraping em C#. O ScrapySharp foi lançado pela última vez em 2018 e o HtmlAgilityPack.CssSelectors.NetCore, o complemento que proporciona suporte a seletores CSS ao HtmlAgilityPack, foi lançado pela última vez em 2019. Já não é necessário utilizar nenhum deles: o AngleSharp lida com seletores CSS de forma nativa e é mantido ativamente, e o próprio HtmlAgilityPack está atualizado. As três primeiras linhas, juntamente com o Playwright, cobrem praticamente todos os projetos.

Como se configura um projeto de scraping em C#?

Crie uma aplicação de consola e adicione os quatro pacotes que a parte estática deste guia utiliza:

dotnet new console -n BookScraper
cd BookScraper
dotnet add package AngleSharp
dotnet add package HtmlAgilityPack
dotnet add package Microsoft.Extensions.Http.Resilience
dotnet add package CsvHelper

Qualquer editor serve. No Visual Studio, escolha o modelo «Aplicação de Consola» e adicione os pacotes através da opção «Gerir Pacotes NuGet». No VS Code, instale o C# Dev Kit e execute os comandos no terminal. O Rider também funciona.

Em seguida, guarde uma cópia de cada página de exercício numa fixtures pasta, para que possa desenvolver o analisador offline. Esses mesmos ficheiros servirão mais tarde como seus fixtures de teste:

mkdir fixtures
curl -o fixtures/books-page1.html https://books.toscrape.com/index.html
curl -o fixtures/books-page2.html https://books.toscrape.com/catalogue/page-2.html
curl -o fixtures/quotes-js.html https://quotes.toscrape.com/js/

No Windows PowerShell, utilize Invoke-WebRequest -OutFile fixtures/books-page1.html https://books.toscrape.com/index.html para cada página. Execute a aplicação a partir da pasta do projeto para que File.ReadAllText("fixtures/...") o programa as encontre.

Mais uma funcionalidade do .NET 10: dotnet run app.cs executa um único .cs ficheiro sem ficheiro de projeto, o que o exemplo do Playwright utiliza.

Como se faz o scraping de uma página web em C#?

Descarregue o HTML com o HttpClient, carregue-o no HtmlParser do AngleSharp, selecione os elementos repetidos com o QuerySelectorAll() e um seletor CSS e, em seguida, leia o texto com o TextContent e os links com o GetAttribute(). Converta cada cartão num registo tipado, analise os preços com a cultura invariante e resolva cada link em relação ao URL da página de onde provém.

O «Books to Scrape» é uma livraria de treino com 1 000 livros distribuídos por 50 páginas de catálogo. Cada livro está num <article class="product_pod"> cartão com o link do título, a classificação por estrelas, o preço e a linha de stock. Comece pela estrutura dos dados. Um sealed record fornece-lhe um tipo imutável com igualdade de valores, pelo que os testes podem comparar linhas inteiras e uma SequenceEqual única chamada pode comprovar que dois analisadores estão de acordo.

// Book.cs: one row of scraped data. [CsvRecord] lets CsvHelper 34 generate the CSV mapping at compile time.
using CsvHelper;

[CsvRecord]
public sealed partial record Book(string Title, decimal Price, int Rating, bool InStock, string Url);

O [CsvRecord] atributo e partial palavra-chave destinam-se ao CsvHelper 34 (consulte a secção sobre exportação). Agora, o analisador. O AngleSharp analisa HTML da mesma forma que um navegador, seguindo o padrão HTML, e suporta os mesmos seletores CSS que introduziria no DevTools.

// BookParser.cs: turn one Books to Scrape listing page into Book records (AngleSharp)
using System.Globalization;
using AngleSharp.Dom;
using AngleSharp.Html.Parser;

public static class BookParser
{
    static readonly HtmlParser Parser = new();

    static readonly Dictionary<string, int> RatingWords = new()
    {
        ["One"] = 1, ["Two"] = 2, ["Three"] = 3, ["Four"] = 4, ["Five"] = 5,
    };

    public static IDocument Load(string html) => Parser.ParseDocument(html);

    public static List<Book> ParseListing(IDocument page, Uri pageUrl)
    {
        var books = new List<Book>();
        foreach (var card in page.QuerySelectorAll("article.product_pod"))
        {
            var link = card.QuerySelector("h3 a");
            if (link is null) continue;                      // skip broken cards instead of crashing

            var priceText = card.QuerySelector("p.price_color")?.TextContent ?? "";
            var price = decimal.Parse(priceText.Trim().TrimStart('£'), CultureInfo.InvariantCulture);

            var ratingClass = card.QuerySelector("p.star-rating")?.ClassList
                .FirstOrDefault(c => RatingWords.ContainsKey(c));
            var stock = card.QuerySelector("p.availability")?.TextContent ?? "";

            books.Add(new Book(
                Title:   link.GetAttribute("title") ?? link.TextContent.Trim(),  // visible text is truncated
                Price:   price,
                Rating:  ratingClass is null ? 0 : RatingWords[ratingClass],
                InStock: stock.Contains("In stock", StringComparison.OrdinalIgnoreCase),
                Url:     new Uri(pageUrl, link.GetAttribute("href")).ToString()  // resolve against the page, not the site root
            ));
        }
        return books;
    }
}

Cada campo esconde uma pequena armadilha:

  • O seletor «card». article.product_pod corresponde a todos os cartões de livros e a mais nada.
  • O título. O texto visível do link está truncado («A Light in the...»), por isso leia o título completo a partir do title atributo.
  • O preço. A página apresenta £51.77. Retire o símbolo da moeda e analise-o como um decimal com CultureInfo.InvariantCulture. Sem a cultura invariante, decimal.Parse utiliza as definições regionais do computador e, num computador alemão ou romeno, «51,77» falha ou transforma-se em 5 177.
  • A classificação. As estrelas são desenhadas com CSS, e os únicos dados no HTML são nomes de classes, como star-rating Three. O método ClassList expõe as classes, e um pequeno dicionário mapeia a palavra para um número de 1 a 5. Qualquer coisa inesperada torna-se 0, em vez de uma estimativa errada.
  • Estoque. A linha de disponibilidade torna-se um bool.
  • O URL. Os links neste site são relativos e referem-se à própria pasta da página, não à raiz do site. Na página inicial, um href começa com catalogue/, e na página 2 não. new Uri(pageUrl, href) resolve cada link em relação à página de onde veio, que é o que um navegador faz. Ao ligar os hrefs à raiz do site, obtêm-se, em vez disso, links que funcionam na página inicial e dão erro 404 na página 2 e seguintes.

A if (link is null) continue; linha impede que um cartão invulgar, como um espaço publicitário, faça com que a execução falhe. Os testes verificam posteriormente o número de linhas.

Execute primeiro o analisador na cópia guardada, para que nada aceda à rede enquanto itera:

// Program.cs (parse a saved page)
var pageUrl = new Uri("https://books.toscrape.com/index.html");
var page = BookParser.Load(File.ReadAllText("fixtures/books-page1.html"));
var books = BookParser.ParseListing(page, pageUrl);
foreach (var b in books.Take(3)) Console.WriteLine(b);
Console.WriteLine($"rows: {books.Count}");
Book { Title = A Light in the Attic, Price = 51.77, Rating = 3, InStock = True, Url = https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html }
Book { Title = Tipping the Velvet, Price = 53.74, Rating = 1, InStock = True, Url = https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html }
Book { Title = Soumission, Price = 50.10, Rating = 1, InStock = True, Url = https://books.toscrape.com/catalogue/soumission_998/index.html }
rows: 20

Assim que a saída parecer correta, aponte o mesmo analisador para a página ativa. O PoliteHttp.Create() auxiliar é o cliente configurado na secção HttpClient abaixo. Para uma primeira tentativa, um new HttpClient() também funciona.

// Program.cs (same parser, live page)
using var http = PoliteHttp.Create();
var pageUrl = new Uri("https://books.toscrape.com/index.html");
var books = BookParser.ParseListing(BookParser.Load(await http.GetStringAsync(pageUrl)), pageUrl);
Console.WriteLine($"live rows: {books.Count}; first: {books[0].Title} ({books[0].Price})");
live rows: 20; first: A Light in the Attic (51.77)

Como é que se faz o scraping com o HtmlAgilityPack e o XPath?

O HtmlAgilityPack é o analisador HTML de longa data do .NET, e a maioria dos tutoriais mais antigos de extração em C# utiliza-o. Continua a ser mantido, é muito tolerante a marcação incorreta e baseia-se em consultas XPath. Se a sua equipa já o utiliza, ou se pensa em XPath, eis a mesma extração:

// BookParserXPath.cs: the same extraction with HtmlAgilityPack and XPath
using System.Globalization;
using HtmlAgilityPack;

public static class BookParserXPath
{
    public static List<Book> ParseListing(string html, Uri pageUrl)
    {
        var doc = new HtmlDocument();
        doc.LoadHtml(html);

        var cards = doc.DocumentNode.SelectNodes("//article[contains(@class,'product_pod')]");
        if (cards is null) return [];                       // SelectNodes returns null, not an empty list

        return cards.Select(card =>
        {
            var link = card.SelectSingleNode(".//h3/a")!;
            var price = card.SelectSingleNode(".//p[@class='price_color']")!.InnerText.Trim().TrimStart('£');
            var rating = card.SelectSingleNode(".//p[contains(@class,'star-rating')]")!
                .GetClasses().Last();                         // "star-rating Three" -> "Three"
            return new Book(
                Title:   HtmlEntity.DeEntitize(link.GetAttributeValue("title", "")),
                Price:   decimal.Parse(price, CultureInfo.InvariantCulture),
                Rating:  Array.IndexOf(["One", "Two", "Three", "Four", "Five"], rating) + 1,
                InStock: card.SelectSingleNode(".//p[contains(@class,'availability')]")!.InnerText.Contains("In stock"),
                Url:     new Uri(pageUrl, link.GetAttributeValue("href", "")).ToString());
        }).ToList();
    }
}

Como Book é um registo, basta uma comparação para comprovar que ambos os analisadores estão de acordo:

// Program.cs (compare with AngleSharp)
var books = BookParserXPath.ParseListing(File.ReadAllText("fixtures/books-page1.html"),
                                         new Uri("https://books.toscrape.com/index.html"));
Console.WriteLine($"rows: {books.Count}; first: {books[0]}");
var same = books.SequenceEqual(BookParser.ParseListing(
    BookParser.Load(File.ReadAllText("fixtures/books-page1.html")),
    new Uri("https://books.toscrape.com/index.html")));
Console.WriteLine($"identical to AngleSharp result: {same}");
rows: 20; first: Book { Title = A Light in the Attic, Price = 51.77, Rating = 3, InStock = True, Url = https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html }
identical to AngleSharp result: True

A versão em XPath utiliza quatro padrões: //tag[contains(@class,'x')] para encontrar elementos em qualquer lugar por classe, .// para pesquisar dentro do nó atual, [@class='x'] para uma correspondência exata de atributos e / para um filho direto. Para qualquer coisa além disso, mantenha uma referência à sintaxe do XPath à mão.

Tenha cuidado com uma armadilha: SelectNodes() os resultados null, e não uma coleção vazia, quando não há correspondências. O código que percorre o resultado num ciclo sem verificar se há valores nulos lança uma NullReferenceException na primeira vez que uma página é atualizada. HtmlEntity.DeEntitize está presente porque o HtmlAgilityPack mantém entidades como &amp; nos valores dos atributos.

Se preferir ignorar completamente o XPath, o HtmlAgilityPack também funciona com LINQ. doc.DocumentNode.Descendants("article").Where(n => n.HasClass("product_pod")) Encontra os mesmos cartões e pode encadear Select() a partir daí.

Utilize o AngleSharp se copiar seletores das DevTools do navegador e pretender uma análise baseada em padrões. Utilize o HtmlAgilityPack se preferir o XPath ou o LINQ, ou se estiver a manter código que já os utilize.

Como é que se encontram os seletores CSS certos?

Abra a página no Chrome ou no Firefox, clique com o botão direito do rato nos dados que pretende e selecione «Inspecionar». Encontre uma classe ou tag que se repita uma vez por registo, teste-a na consola das Ferramentas de Desenvolvimento com `document.querySelectorAll()` e, em seguida, cole o mesmo seletor no `QuerySelectorAll()` do AngleSharp. Prefira nomes de classe curtos e estáveis em vez de percursos longos gerados e copiados das Ferramentas de Desenvolvimento.

No site «Books to Scrape», ao clicar com o botão direito do rato no título de um livro e selecionar «Inspecionar», é apresentado um <a> dentro de um <h3>, dentro de <article class="product_pod">. Esse elemento é o limite do registo: um por livro, vinte por página. No console, document.querySelectorAll("article.product_pod").length retorna 20.

Todos os seletores deste guia utilizam um dos seis padrões:

Padrão

Exemplo

Corresponde a

Etiqueta

article

Todos os <article> elemento

Classe

.product_pod

Qualquer elemento com essa classe

Etiqueta e classe

p.price_color

<p> com essa classe

Descendente

h3 a

Um <a> em qualquer ponto dentro de um <h3>

Atributo

a[href]

Links que tenham um href

Combinado

li.next a

O link dentro do item «seguinte» da lista

O seletor «Copiar» do DevTools produz caminhos longos como #default > div > div > ... > li:nth-child(1) > article esses, que se quebram quando o layout ganha um elemento envolvente div. Um seletor curto numa classe significativa resiste às reformulações.

O XPath é a outra opção e permite fazer coisas que o CSS não consegue, como selecionar um elemento pelo seu texto ou subir até um elemento pai. Para uma comparação lado a lado, veja quando o XPath supera o CSS.

Dica: Se um seletor encontrar elementos no DevTools, mas devolver zero em C#, é provável que os dados não estejam no HTML bruto. O DevTools mostra a página após a execução do JavaScript, enquanto o HttpClient vê apenas o que o servidor enviou. Prima Ctrl+U (Ver Código-fonte) e procure pelo texto. Se não estiver lá, passe para a secção sobre JavaScript abaixo.

Como deve configurar o HttpClient para a extração de dados da Web?

Crie um HttpClient e reutilize-o para cada pedido, defina PooledConnectionLifetime para que as alterações de DNS sejam detetadas, envie um User-Agent válido e adicione um pipeline de repetição que recue perante respostas 408, 429 e 5xx e respeite o Retry-After. O Microsoft.Extensions.Http.Resilience permite-lhe obter esse comportamento de repetição em cerca de quinze linhas.

Existem dois erros clássicos. O primeiro é criar um novo HttpClient por cada pedido: cada instância abre as suas próprias ligações e, ao longo de alguns milhares de páginas, isso pode esgotar os sockets da máquina. O segundo é um cliente estático que nunca recicla ligações, pelo que continua a comunicar com um endereço IP antigo após alterações no DNS de um site.

As diretrizes do HttpClient da Microsoft resolvem ambos os problemas: utilize um cliente de longa duração e defina PooledConnectionLifetime no seu manipulador para que as ligações sejam recicladas a cada poucos minutos. As mesmas diretrizes mostram como adicionar resiliência a um cliente estático, que é o padrão apresentado abaixo.

// PoliteHttp.cs: one shared HttpClient with an honest user agent, timeouts, and retries
using System.Net;
using Microsoft.Extensions.Http.Resilience;
using Polly;

public static class PoliteHttp
{
    public static HttpClient Create(TimeSpan? attemptTimeout = null)
    {
        var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
            .AddRetry(new HttpRetryStrategyOptions
            {
                MaxRetryAttempts = 3,                        // retries 408, 429, 5xx and network errors
                BackoffType = DelayBackoffType.Exponential,  // waits longer each time, honors Retry-After
                UseJitter = true,
            })
            .AddTimeout(attemptTimeout ?? TimeSpan.FromSeconds(30))
            .Build();

        var handler = new ResilienceHandler(pipeline)
        {
            InnerHandler = new SocketsHttpHandler
            {
                PooledConnectionLifetime = TimeSpan.FromMinutes(5),
                AutomaticDecompression = DecompressionMethods.All,
            },
        };

        var http = new HttpClient(handler) { Timeout = Timeout.InfiniteTimeSpan }; // the pipeline owns timeouts
        http.DefaultRequestHeaders.UserAgent.ParseAdd("csharp-scraping-tutorial/1.0 (+mailto:you@example.com)");
        return http;
    }
}

Eis o que cada parte faz:

  • SocketsHttpHandler é a pilha de rede sob HttpClient. PooledConnectionLifetime = 5 minutes recicla as ligações para que as alterações no DNS surtam efeito, e AutomaticDecompression lida com gzip, Brotli e deflate.
  • A estratégia de repetição de tentativas provém da biblioteca de resiliência HTTP da Microsoft, que se baseia no Polly. HttpRetryStrategyOptions já sabe quais as falhas que vale a pena tentar novamente: 408 Request Timeout, HTTP 429 Too Many Requests, erros de servidor 5xx, exceções de rede e tempos de espera esgotados. Aplica um intervalo de espera exponencial, adiciona jitter para que os trabalhadores paralelos não repitam as tentativas em sincronia e aguarda o Retry-After cabeçalho quando o servidor o enviar, que é a indicação do site sobre quanto tempo deve esperar.
  • A estratégia de tempo limite atribui 30 segundos a cada tentativa, por predefinição. HttpClient.Timeout está definido como infinito porque HttpCliento próprio valor predefinido de 100 segundos, de outra forma, interferiria com as tentativas de repetição. Em vez disso, o pipeline controla os tempos de espera. O parâmetro opcional attemptTimeout permite que chamadas mais lentas, como pedidos de API renderizados, utilizem um limite mais longo.
  • O User-Agent identifica o seu scraper e fornece uma forma de o contactar. Os sites podem contactá-lo em vez de o bloquear. Substitua o e-mail pelo seu. Para saber mais sobre os restantes cabeçalhos de pedido e quando são relevantes, consulte quais os cabeçalhos importantes para o scraping.

Três tentativas são suficientes. Mais tentativas raramente transformam uma recusa num sucesso, por isso registe a falha e siga em frente.

No ASP.NET Core ou num serviço de trabalho, ignore o auxiliar estático e deixe que a injeção de dependências gere o cliente. Registe um cliente tipado com builder.Services.AddHttpClient<BookFetcher>() e encadeie .AddStandardResilienceHandler() para obter novas tentativas, tempos de espera e um disjuntor de circuito com valores predefinidos razoáveis. IHttpClientFactory depois trata da duração das ligações por si. As classes de análise neste guia não se alteram de todo. Apenas o local de onde o HttpClient provém.

Como se extrai várias páginas em C#?

Analise a primeira página, leia o link para a página seguinte, resolva-o em relação ao URL da página atual com new Uri(pageUrl, href) e repita até que o link desapareça ou atinja um limite de páginas. Reutilize o mesmo HttpClient, aguarde cerca de um segundo entre os pedidos e deixe que o pipeline de novas tentativas trate das respostas 429 e 503.

O «Books to Scrape» divide o seu catálogo por 50 páginas, e cada página termina com um paginador cujo item «seguinte» é <li class="next"><a href="page-2.html">next</a></li>. Segui-lo até desaparecer significa que nunca se adivinha o número de páginas.

// Crawler.cs: follow "next" links politely, then visit detail pages with bounded concurrency
using System.Collections.Concurrent;
using AngleSharp.Dom;

public static class Crawler
{
    public static async Task<List<Book>> CrawlCatalogAsync(HttpClient http, Uri start, int maxPages = 3)
    {
        var books = new List<Book>();
        Uri? url = start;
        for (var page = 1; url is not null && page <= maxPages; page++)
        {
            var html = await http.GetStringAsync(url);
            var doc = BookParser.Load(html);
            books.AddRange(BookParser.ParseListing(doc, url));

            var next = doc.QuerySelector("li.next a")?.GetAttribute("href");
            url = next is null ? null : new Uri(url, next);   // relative to the current page
            await Task.Delay(TimeSpan.FromSeconds(1));         // about one request per second
        }
        return books;
    }

    public sealed record BookDetail(string Url, string Upc, int Available);

    public static async Task<List<BookDetail>> FetchDetailsAsync(HttpClient http, IEnumerable<string> urls)
    {
        var results = new ConcurrentBag<BookDetail>();
        var options = new ParallelOptions { MaxDegreeOfParallelism = 3 };   // a small, fixed cap per site

        await Parallel.ForEachAsync(urls, options, async (url, ct) =>
        {
            var doc = BookParser.Load(await http.GetStringAsync(url, ct));
            var table = doc.QuerySelectorAll("table.table-striped tr")
                .ToDictionary(r => r.QuerySelector("th")!.TextContent.Trim(),
                              r => r.QuerySelector("td")!.TextContent.Trim());
            var available = new string(table["Availability"].Where(char.IsDigit).ToArray());
            results.Add(new BookDetail(url, table["UPC"], int.Parse(available)));
            await Task.Delay(TimeSpan.FromMilliseconds(500), ct);
        });

        return results.OrderBy(d => d.Url).ToList();
    }
}

CrawlCatalogAsync faz quatro coisas em cada iteração: recupera a página, analisa os seus livros com o mesmo BookParser, procura li.next ae aguarda um segundo. A new Uri(url, next) linha também é importante aqui. O próximo link na página 1 está page-2.html, relativo a /catalogue/, e a sua resolução em relação à página atual fornece sempre o URL absoluto correto. Cada página é passada para ParseListing com a sua própria URL, pelo que todos os links para livros em todas as páginas também são resolvidos corretamente.

O maxPages parâmetro é uma válvula de segurança. Mantém-no baixo enquanto desenvolves, para que um erro no seletor ou um bug num loop custe três pedidos em vez de três mil. Aumenta-o assim que a saída parecer correta.

Comece por catalogue/page-1.html, e não na página inicial, para que todas as URLs das páginas tenham o mesmo formato.

// Program.cs (crawl three pages)
using var http = PoliteHttp.Create();
var books = await Crawler.CrawlCatalogAsync(http, new Uri("https://books.toscrape.com/catalogue/page-1.html"), maxPages: 3);
Console.WriteLine($"pages: 3, rows: {books.Count}");
Console.WriteLine(books[20].Url);
using var check = await http.GetAsync(books[20].Url);
Console.WriteLine($"status of a parsed detail URL: {(int)check.StatusCode}");
pages: 3, rows: 60
https://books.toscrape.com/catalogue/in-her-wake_980/index.html
status of a parsed detail URL: 200

A linha 21 é o primeiro livro da página 2, e a sua URL devolve o código de estado 200, o que confirma que a resolução relativa à página funciona para além da primeira página. É aí que a ligação relativa à raiz costuma falhar.

Este ciclo de «próximo link» é um pequeno rastreador e não pode visitar a mesma página duas vezes, porque cada página aponta apenas para a frente. Assim que seguir links que possam criar um ciclo de retorno, como páginas de categorias, filtros ou «itens relacionados», mantenha uma HashSet<string> lista das URLs visitadas e ignore tudo o que já constar nela. Se a diferença entre as duas tarefas for pouco clara, eis como o scraping e o rastreamento diferem.

O atraso de um segundo é autoimposto. Em sites reais, comece devagar, esteja atento aos erros 429 e considere o ficheiro robots.txt e os termos como o limite máximo.

Como se faz o scraping de páginas em simultâneo em C#?

As páginas de detalhes contêm informações adicionais, como o UPC e a quantidade exata em stock. Obtê-las uma a uma é lento e processá-las todas de uma só vez é impróprio. Parallel.ForEachAsync fica no meio-termo: executa um corpo assíncrono para cada item, com, no máximo, MaxDegreeOfParallelism em execução ao mesmo tempo.

FetchDetailsAsync No rastreador acima, esse limite é de três. Escolha um número pequeno e fixo por site, em vez de Environment.ProcessorCount, o valor a que muitos exemplos recorrem. O número de núcleos do teu processador não diz nada sobre o que um site consegue suportar e, de qualquer forma, o rastreamento consiste quase inteiramente em espera na rede.

Os resultados vão para um ConcurrentBag, ao qual é seguro adicionar dados a partir de várias tarefas ao mesmo tempo, e são ordenados no final, uma vez que o trabalho paralelo termina sem uma ordem específica.

// Program.cs (detail pages, three at a time)
using var http = PoliteHttp.Create();
var page1 = new Uri("https://books.toscrape.com/catalogue/page-1.html");
var books = BookParser.ParseListing(BookParser.Load(await http.GetStringAsync(page1)), page1);
var details = await Crawler.FetchDetailsAsync(http, books.Take(6).Select(b => b.Url));
foreach (var d in details) Console.WriteLine($"{d.Upc}  {d.Available,3} available  {d.Url}");
a897fe39b1053632   22 available  https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html
4165285e1663650f   20 available  https://books.toscrape.com/catalogue/sapiens-a-brief-history-of-humankind_996/index.html
e00eb4fd7b871a48   20 available  https://books.toscrape.com/catalogue/sharp-objects_997/index.html
6957f44c3847a760   20 available  https://books.toscrape.com/catalogue/soumission_998/index.html
f77dbf2323deb740   19 available  https://books.toscrape.com/catalogue/the-requiem-red_995/index.html
90fa61229261140a   20 available  https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html

Todas as tarefas partilham o mesmo HttpClient, o que é seguro e intencional: HttpClient é seguro para threads no envio de pedidos, e a sua partilha significa que o pipeline de repetição e o conjunto de ligações se aplicam a todos os trabalhadores em simultâneo.

Como se guardam dados extraídos em CSV ou JSON em C#?

Para JSON, serialize a sua lista de registos com o System.Text.Json, que vem incluído no .NET. Para CSV, utilize o CsvHelper: marque o registo com [CsvRecord], abra um escritor com CsvWriter.ToFile() e chame WriteRecords(). Por predefinição, o CsvHelper 34 escreve números com a cultura invariante, pelo que os preços mantêm o ponto decimal em todas as máquinas.

Ambos os formatos requerem apenas algumas linhas:

// Export.cs: write results to CSV (CsvHelper 34) and JSON (System.Text.Json)
using System.Text.Json;
using CsvHelper;

public static class Export
{
    public static void ToCsv(IEnumerable<Book> books, string path)
    {
        using var csv = CsvWriter.ToFile(path);   // UTF-8, header row first, invariant culture by default
        csv.WriteRecords(books);
    }

    public static void ToJson(IEnumerable<Book> books, string path) =>
        File.WriteAllText(path, JsonSerializer.Serialize(books, new JsonSerializerOptions { WriteIndented = true }));
}
// Program.cs (save results)
var pageUrl = new Uri("https://books.toscrape.com/index.html");
var books = BookParser.ParseListing(BookParser.Load(File.ReadAllText("fixtures/books-page1.html")), pageUrl);
Export.ToCsv(books, "books.csv");
Export.ToJson(books, "books.json");
Console.WriteLine(string.Join(Environment.NewLine, File.ReadLines("books.csv").Take(3)));
Console.WriteLine($"wrote books.csv and books.json with {books.Count} rows");
Title,Price,Rating,InStock,Url
A Light in the Attic,51.77,3,True,https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html
Tipping the Velvet,53.74,1,True,https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html
wrote books.csv and books.json with 20 rows

A linha de cabeçalho provém dos nomes das propriedades do registo e a ordem das colunas segue os parâmetros do registo. O JSON mantém os mesmos nomes e tipos, pelo que os preços permanecem como números e InStock permanece como um booleano. Utilize CSV para folhas de cálculo e importações, e JSON para programas e dados aninhados.

O CsvHelper 34 alterou a API. O CsvHelper 34.0.0, lançado a 28 de outubro de 2026, é uma reescrita. O código que faz new CsvWriter(writer, CultureInfo.InvariantCulture), que é o que a maioria dos tutoriais de scraping em C# mostra, já não compila com o pacote atual. Utilize a API v34 aqui apresentada ou fixe a versão anterior com dotnet add package CsvHelper --version 33.1.0 para código mais antigo. O guia de migração da v34 do CsvHelper enumera o resto das alterações.

É também por isso que Book é declarado partial com um [CsvRecord] atributo. O CsvHelper 34 gera o mapeamento entre o registo e as suas colunas em tempo de compilação, e o gerador de código-fonte necessita de um partial tipo para onde adicionar esse código.

Nas versões anteriores, era necessário passar uma cultura a cada gravador, e esquecer-se disso significava que uma máquina alemã gravava 51,77 num ficheiro separado por vírgulas. A versão 34 utiliza por predefinição a cultura invariante.

Como saber se uma página precisa de JavaScript?

Recupere o HTML bruto com o HttpClient e execute o seu seletor nele. Se não encontrar nada, mas o navegador mostrar os dados, significa que o JavaScript gera esse conteúdo. Antes de iniciar um navegador, pesquise no código-fonte da página e no separador «Rede» das DevTools: muitos sites enviam os registos como JSON, que o System.Text.Json consegue ler diretamente.

A /js/ versão do «Quotes to Scrape» parece idêntica ao site normal num navegador: dez citações, cada uma num div.quote com o texto, o autor e as etiquetas. O HTML bruto revela uma realidade diferente. O servidor envia um contêiner vazio, juntamente com um <script> bloco que contém cada citação como uma matriz JavaScript, e o navegador constrói os div.quote elementos a partir dele depois de a página carregar.

Por isso, verifica a resposta antes de recorrer a um navegador. Se os registos estiverem numa tag de script, HttpClient e System.Text.Json são suficientes.

// ScriptData.cs: is the data in the HTML, or built by JavaScript? Read it from the <script> tag.
using System.Text.Json;
using System.Text.RegularExpressions;

public sealed record Author(string Name);
public sealed record Quote(string Text, Author Author, string[] Tags);

public static partial class ScriptData
{
    [GeneratedRegex(@"var data = (\[.*?\]);", RegexOptions.Singleline)]
    private static partial Regex DataArray();

    public static List<Quote> ReadQuotes(string html)
    {
        var doc = BookParser.Load(html);
        var script = doc.QuerySelectorAll("script")
            .Select(s => s.TextContent)
            .First(t => t.Contains("var data"));
        var json = DataArray().Match(script).Groups[1].Value;
        return JsonSerializer.Deserialize<List<Quote>>(json,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
    }
}
// Program.cs (check, then read the script tag)
var html = File.ReadAllText("fixtures/quotes-js.html");
Console.WriteLine($"div.quote elements in raw HTML: {BookParser.Load(html).QuerySelectorAll("div.quote").Length}");
var quotes = ScriptData.ReadQuotes(html);
foreach (var q in quotes.Take(2)) Console.WriteLine($"{q.Author.Name}: {q.Text[..60]}… [{string.Join(", ", q.Tags)}]");
Console.WriteLine($"quotes parsed from the script tag: {quotes.Count}");
div.quote elements in raw HTML: 0
Albert Einstein: “The world as we have created it is a process of our thinkin… [change, deep-thoughts, thinking, world]
J.K. Rowling: “It is our choices, Harry, that show what we truly are, far … [abilities, choices]
quotes parsed from the script tag: 10

A primeira linha é a verificação: zero div.quote elementos no HTML bruto, mesmo que o navegador mostre dez. O AngleSharp encontra então o script que contém var data, uma expressão regular gerada dinamicamente extrai o array e o System.Text.Json transforma-o em Quote registos. [GeneratedRegex] compila o padrão na fase de compilação e PropertyNameCaseInsensitive mapeia as chaves em minúsculas para as propriedades dos registos.

A mesma ideia abrange outros dois casos comuns. Muitas páginas de produtos e artigos incorporam dados estruturados em <script type="application/ld+json">, que pode selecionar com o AngleSharp e deserializar diretamente, sem necessidade de expressões regulares. Além disso, muitos sites com uso intensivo de JavaScript carregam os seus registos a partir dos seus próprios pontos finais JSON. Abra o DevTools, aceda à secção «Rede», filtre por «Fetch/XHR» e atualize a página. Se um pedido devolver os dados que vê no ecrã, chame essa URL com HttpClient.GetFromJsonAsync<T>() e ignore completamente o HTML.

Ler JSON é mais rápido do que renderizar e apresenta menos falhas, porque os formatos de dados mudam com menos frequência do que os layouts.

Como se extrai páginas renderizadas em JavaScript em C#?

Utilize o Playwright para .NET, a biblioteca de automação de navegadores da Microsoft. Esta biblioteca descarrega o Chromium, carrega a página em modo headless, aguarda o seu seletor e permite-lhe ler elementos ou o HTML renderizado. O PuppeteerSharp e o Selenium WebDriver também funcionam. Para muitas páginas ou sites bloqueados, renderize remotamente com uma API de scraper em vez de executar os navegadores você mesmo.

Quando não houver JSON para ler, ou se tiver de clicar ou digitar primeiro, utilize um navegador real. O Playwright controla o Chromium, o Firefox ou o WebKit a partir de C#, executa-os em modo headless por predefinição e tem a função de espera integrada nos seus localizadores. Se a ideia de um navegador sem janela for nova para si, veja aqui como funcionam os navegadores headless.

Para manter o navegador fora do seu projeto principal, esta é uma aplicação baseada em ficheiros do .NET 10 que declara o seu próprio pacote:

#:package Microsoft.Playwright@1.64.1
#:property PublishAot=false
// render.cs: a .NET 10 file-based app. First run: dotnet run render.cs -- install
using Microsoft.Playwright;

if (args is ["install"])
    return Microsoft.Playwright.Program.Main(["install", "chromium"]);

using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(new() { Headless = true });
var page = await browser.NewPageAsync(new() { UserAgent = "csharp-scraping-tutorial/1.0 (+mailto:you@example.com)" });

await page.GotoAsync("https://quotes.toscrape.com/js/");
var quotes = page.Locator("div.quote");
await quotes.First.WaitForAsync();                       // wait for JavaScript to build the list

foreach (var quote in (await quotes.AllAsync()).Take(3))
{
    var text = await quote.Locator("span.text").InnerTextAsync();
    var author = await quote.Locator("small.author").InnerTextAsync();
    Console.WriteLine($"{author}: {text[..50]}…");
}
Console.WriteLine($"quotes rendered: {await quotes.CountAsync()}");

// Hand the rendered HTML to the same AngleSharp code you already wrote, if you prefer
var html = await page.ContentAsync();
Console.WriteLine($"rendered HTML length: {html.Length}");
return 0;
dotnet run render.cs -- install   # once: downloads Chromium
dotnet run render.cs
Albert Einstein: “The world as we have created it is a process of o…
J.K. Rowling: “It is our choices, Harry, that show what we truly…
Albert Einstein: “There are only two ways to live your life. One is…
quotes rendered: 10
rendered HTML length: 8940

As duas #: linhas no topo substituem um ficheiro de projeto. #:package obtém o Playwright a partir do NuGet. #:property PublishAot=false é necessário porque as aplicações baseadas em ficheiros utilizam, por predefinição, as configurações AOT nativas, e o Playwright depende de uma serialização JSON baseada em reflexão que o AOT elimina. A primeira execução com install baixa uma compilação do Chromium compatível. No Linux, o Chromium também pode necessitar de bibliotecas do sistema; por isso, consulte a documentação do Playwright para .NET relativa ao seu sistema operativo.

A linha principal é await quotes.First.WaitForAsync(). Ela aguarda até que exista pelo menos um div.quote exista, o que significa que o JavaScript terminou de construir a lista. Um valor fixo Task.Delay(3000) é demasiado longo em páginas rápidas e demasiado curto em páginas lentas.

Se preferir manter toda a análise num único local, page.ContentAsync() devolve o HTML renderizado como uma string. Passa-a para BookParser.Load() e consulta-o com o AngleSharp exatamente como fizeste para páginas estáticas.

O PuppeteerSharp é uma versão em C# do Puppeteer do Node. Centra-se no Chromium e a sua API corresponde quase linha a linha à do Puppeteer, o que o torna uma boa opção quando se está a portar scripts Node existentes para o .NET.

O Selenium WebDriver é a opção há muito estabelecida e adequa-se a equipas que já executam testes Selenium ou um Selenium Grid. Para a extração de dados, espere ter de escrever código de espera mais explícito do que com o Playwright, uma vez que a maioria dos elementos necessita de um WebDriverWait antes de poderem ser lidos.

Os navegadores locais consomem memória e CPU. Quando precisar de muitas sessões ou de passos interativos, como cliques e preenchimento de formulários em sites que resistem à automatização, um navegador automatizado hospedado transfere esse trabalho para fora do seu computador.

Como se utiliza uma API de web scraping a partir de C#?

Envie o URL de destino para a API de scraping com o HttpClient e analise o HTML que esta devolve com o seu código existente. Com a API de scraping da WebScrapingAPI, chame um endpoint com a sua api_key e o destino codificado em URL; em seguida, adicione render_js=1 para páginas em JavaScript e country para a localização. Leia a chave a partir de uma variável de ambiente, nunca a partir do código-fonte.

A recuperação a partir da sua própria máquina é a predefinição correta. Estes sintomas indicam que se tornou o gargalo:

  • Respostas 403 ou 429 que continuam a surgir, mesmo a um ritmo moderado e após novas tentativas.
  • Páginas de autenticação ou de consentimento a aparecerem em vez do conteúdo que se vê num navegador.
  • Conteúdo que varia consoante o país, como preços, disponibilidade ou resultados de pesquisa, quando é necessária uma localização específica.
  • Mais renderização de JavaScript do que as instâncias locais do Chromium conseguem acompanhar.

Exclua primeiro as causas básicas. Estes quatro hábitos resolvem muitos bloqueios, e existem outras formas de reduzir os bloqueios para além destes:

  • Abrande: menos pedidos simultâneos e um intervalo entre eles.
  • Envie um User-Agent honesto e consistente.
  • Armazene em cache as páginas que já obteve, em vez de as descarregar novamente.
  • Recupere apenas as páginas e os campos de que necessita.

Se gerires o teu próprio conjunto de proxies, também podes encaminhar o HttpClient através de um proxy e manter tudo o resto como está.

Quando essas medidas não forem suficientes, deixe que uma API de scraper assuma a etapa de obtenção. A API de scraper da WebScrapingAPI aceita um URL e devolve a página, tratando de proxies, novas tentativas e renderização opcional em JavaScript. A partir de C#, basta uma solicitação GET, e uma pequena classe é toda a integração:

// WsaClient.cs: fetch pages through WebScrapingAPI's Scraper API; parsing stays the same
public sealed class WsaClient(HttpClient http)
{
    const string Endpoint = "https://api.webscrapingapi.com/v2";

    public static Uri BuildUri(string apiKey, string targetUrl, bool renderJs = false, string? country = null)
    {
        var query = new List<string>
        {
            "api_key=" + Uri.EscapeDataString(apiKey),
            "url=" + Uri.EscapeDataString(targetUrl),     // encode the target so its ? and & survive
        };
        if (renderJs) query.Add("render_js=1");
        if (country is not null) query.Add("country=" + Uri.EscapeDataString(country));
        return new Uri($"{Endpoint}?{string.Join("&", query)}");
    }

    public async Task<string> GetHtmlAsync(string targetUrl, bool renderJs = false, string? country = null,
                                           CancellationToken ct = default)
    {
        var apiKey = Environment.GetEnvironmentVariable("WSA_API_KEY");
        if (string.IsNullOrWhiteSpace(apiKey))
            throw new InvalidOperationException("Set the WSA_API_KEY environment variable first.");

        using var response = await http.GetAsync(BuildUri(apiKey, targetUrl, renderJs, country), ct);
        var body = await response.Content.ReadAsStringAsync(ct);
        if (!response.IsSuccessStatusCode)
            throw new HttpRequestException($"Scraper API returned {(int)response.StatusCode}: {body}");
        return body;
    }
}

BuildUri é pública, para que possa ver exatamente o que será enviado sem enviar nada:

// Program.cs (see what you'll send, without sending it)
var uri = WsaClient.BuildUri("YOUR_KEY", "https://quotes.toscrape.com/js/?page=1&tag=life", renderJs: true, country: "us");
Console.WriteLine(uri.AbsoluteUri);
try { await new WsaClient(new HttpClient()).GetHtmlAsync("https://books.toscrape.com/"); }
catch (InvalidOperationException ex) { Console.WriteLine($"without a key: {ex.Message}"); }
https://api.webscrapingapi.com/v2?api_key=YOUR_KEY&url=https%3A%2F%2Fquotes.toscrape.com%2Fjs%2F%3Fpage%3D1%26tag%3Dlife&render_js=1&country=us
without a key: Set the WSA_API_KEY environment variable first.

Observe o url= valor. O próprio ? e & tornou-se %3F e %26. Sem Uri.EscapeDataString(), a API interpretaria tag=life como um dos seus próprios parâmetros e iria buscar a página errada.

A chave nunca aparece no código. Defina-a uma vez por shell ou máquina:

export WSA_API_KEY=your-key-here        # macOS and Linux
setx WSA_API_KEY "your-key-here"        # Windows (open a new terminal afterward)

Numa aplicação ASP.NET Core, dotnet user-secrets mantenha a chave fora do seu repositório durante o desenvolvimento, e o armazenamento de segredos do seu host faz o mesmo em produção. Se a variável estiver em falta, WsaClient interrompe-se com uma mensagem clara antes de enviar um pedido, como mostra a segunda linha de saída. Se a chave estiver errada, a API responde com um código HTTP 401 com {"error":"Invalid API key"}, e WsaClient apresenta esse corpo num HttpRequestException, para que veja o motivo em vez de um resultado vazio.

Dois parâmetros cobrem a maioria das necessidades. renderJs: true adiciona render_js=1, que renderiza a página num navegador antes de a devolver. country: "us" adiciona country=us, que recupera a página a partir desse local. A renderização em JavaScript e a segmentação geográfica não implicam créditos adicionais. Os pedidos renderizados demoram mais tempo do que os simples, por isso atribua ao cliente um tempo limite por tentativa mais longo com PoliteHttp.Create(TimeSpan.FromSeconds(120)).

A resposta é HTML, pelo que vai para o mesmo BookParser que escreveu no início, sem alterações:

// Program.cs (fetch through the Scraper API)
using var http = PoliteHttp.Create(attemptTimeout: TimeSpan.FromSeconds(120));
var wsa = new WsaClient(http);
var target = new Uri("https://books.toscrape.com/catalogue/page-2.html");
var books = BookParser.ParseListing(BookParser.Load(await wsa.GetHtmlAsync(target.ToString(), country: "us")), target);
Console.WriteLine($"rows via Scraper API: {books.Count}; first: {books[0].Title}");
var rendered = BookParser.Load(await wsa.GetHtmlAsync("https://quotes.toscrape.com/js/", renderJs: true));
Console.WriteLine($"div.quote after render_js: {rendered.QuerySelectorAll("div.quote").Length}");

Exemplo de saída:

rows via Scraper API: 20; first: In Her Wake
div.quote after render_js: 10

ParseListing continua a receber o URL de destino, e não o ponto final da API. Os links são resolvidos em relação à página de onde o HTML provém, pelo que todos os URLs dos livros permanecem corretos, independentemente da forma como a página foi obtida. A página «Citações» renderizada contém os mesmos div.quote elementos que o Playwright viu.

Que abordagem de scraping em C# deve utilizar?

Utilize o HttpClient com o AngleSharp ou o HtmlAgilityPack para páginas estáticas. Adicione o pipeline de resiliência e um limite de concorrência quando rastrear muitas páginas. Leia o JSON da tag script ou o ponto final JSON do site quando o JavaScript carregar os dados. Utilize o Playwright para algumas páginas interativas e uma API de scraping quando bloqueios ou o volume tornem a obtenção local pouco fiável.

Situação

Utilize

Porquê

HTML estático, algumas páginas

HttpClient + AngleSharp ou HtmlAgilityPack

Menos componentes móveis

Muitas páginas ou um rastreamento

HttpClient partilhado com o pipeline de resiliência, atraso, Parallel.ForEachAsync limite

de tentativas, Retry-After, taxa de acesso educada

Dados em <script> JSON ou num ponto final JSON

HttpClient + System.Text.Json

Mais rápido e mais estável do que a renderização

página criada em JavaScript, pequeno volume

Playwright para .NET

Navegador real, com espera integrada

Cliques, inícios de sessão ou fluxos de várias etapas que pode automatizar

Playwright (ou Selenium, se a sua equipa já o utilizar)

Suporte à interação

Erros 403/429 que as tentativas de repetição não resolvem, conteúdo específico de cada país ou renderização em grande volume

API de scraper via HttpClient

Proxies geridos, tentativas de repetição e renderização; analisador inalterado

Se não tiver a certeza de onde uma página se enquadra, responda a estas perguntas por ordem e pare na primeira resposta afirmativa:

  1. Os dados estão em «Ver código-fonte»? Recupere-os com o HttpClient e analise-os com o AngleSharp ou o HtmlAgilityPack.
  2. Precisa de muitas páginas? Adicione o pipeline de resiliência, um atraso e um pequeno limite de simultaneidade.
  3. Os dados estão numa tag de script ou numa solicitação JSON no separador «Rede»? Leia-os com o System.Text.Json.
  4. A página requer um navegador, e apenas algumas vezes? Utilize o Playwright para .NET.
  5. Está bloqueado, precisa de um país específico ou está a renderizar em grande volume? Envie a solicitação através da API do Scraper e mantenha o seu analisador.

Como se testa um scraper web em C#?

Guarde uma cópia de cada tipo de página como um ficheiro HTML, adicione um projeto de testes xUnit que analise esses ficheiros offline e verifique o número de linhas, os valores tipados e os URLs absolutos. Quando o site alterar a sua marcação, um teste falhará em segundos, em vez de o seu scraper devolver silenciosamente resultados vazios em produção.

Os scrapers falham silenciosamente: um site renomeia uma classe e a tarefa cria um ficheiro vazio que parece ter sido bem-sucedido. As páginas que guardou durante a configuração já são os seus fixtures de teste.

Crie um projeto de teste ao lado do scraper e faça referência a ele:

dotnet new xunit -n BookScraper.Tests
cd BookScraper.Tests
dotnet add reference ../BookScraper/BookScraper.csproj

Adicione ao BookScraper.Tests.csproj para que as páginas guardadas sejam copiadas ao lado dos binários de teste:

<ItemGroup>
  <None Include="../fixtures/*.html" Link="fixtures/%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

Em seguida, teste o comportamento do qual depende, e não a implementação:

// BookParserTests.cs: run the parser against a saved page, so tests never touch the network
public class BookParserTests
{
    static readonly Uri Page2 = new("https://books.toscrape.com/catalogue/page-2.html");

    static List<Book> ParseFixture() =>
        BookParser.ParseListing(BookParser.Load(File.ReadAllText("fixtures/books-page2.html")), Page2);

    [Fact]
    public void Finds_every_book_on_the_page() => Assert.Equal(20, ParseFixture().Count);

    [Fact]
    public void Resolves_links_against_the_page_not_the_site_root()
    {
        var first = ParseFixture()[0];
        Assert.Equal("https://books.toscrape.com/catalogue/in-her-wake_980/index.html", first.Url);
    }

    [Fact]
    public void Reads_price_and_rating_as_numbers()
    {
        var first = ParseFixture()[0];
        Assert.Equal(12.84m, first.Price);
        Assert.InRange(first.Rating, 1, 5);
    }
}
Passed!  - Failed:     0, Passed:     3, Skipped:     0, Total:     3, Duration: 147 ms - BookScraper.Tests.dll (net10.0)

Os três testes verificam os três aspetos mais suscetíveis de falhar: a contagem de linhas (o seletor de cartões continua a corresponder), o URL na página 2 (os links continuam a resolver para a página) e os valores introduzidos (o preço e a classificação continuam a ser analisados). A página 2 é propositadamente o fixture, porque é onde surgem os erros de URL relativos à raiz.

Aplique a mesma ideia em produção com a regra «falhar de forma evidente»: se uma página de listagem devolver zero cartões, lance uma exceção em vez de gravar um ficheiro vazio. Quando falhar, guarde uma cópia recente da página, corrija o seletor e execute novamente. Considere também adicionar uma ScrapedAt coluna de data e hora aos teus registos, para que cada linha mostre quando foi recolhida e possas comparar as execuções mais tarde.

Como fazer scraping de forma responsável com C#?

Leia os termos do site e o ficheiro robots.txt antes de iniciar o rastreio, mantenha as taxas de pedidos baixas, identifique o seu scraper com um agente de utilizador que inclua detalhes de contacto e recolha apenas os campos de que necessita. Evite dados pessoais, a menos que tenha uma base legal, e não faça scraping em áreas que exijam login sem autorização. Dê preferência a uma API oficial, quando esta existir.

O `robots.txt` é um ficheiro de texto simples na raiz de um site que indica aos rastreadores quais os caminhos a ignorar, e o seu formato é definido pela norma `robots.txt` (RFC 9309). A sua leitura requer apenas algumas linhas com o cliente que já possui:

// Robots.cs: read a site's robots.txt before you crawl (RFC 9309)
using System.Net;

public static class Robots
{
    public static async Task<List<string>> ReadRulesAsync(HttpClient http, Uri site)
    {
        using var response = await http.GetAsync(new Uri(site, "/robots.txt"));
        if (response.StatusCode == HttpStatusCode.NotFound)
            return ["No robots.txt: no crawl rules published"];
        response.EnsureSuccessStatusCode();

        var text = await response.Content.ReadAsStringAsync();
        return text.Split('\n')
            .Select(line => line.Trim())
            .Where(line => line.StartsWith("User-agent:", StringComparison.OrdinalIgnoreCase)
                        || line.StartsWith("Disallow:", StringComparison.OrdinalIgnoreCase)
                        || line.StartsWith("Allow:", StringComparison.OrdinalIgnoreCase)
                        || line.StartsWith("Crawl-delay:", StringComparison.OrdinalIgnoreCase))
            .ToList();
    }
}
// Program.cs (read robots.txt first)
using var http = PoliteHttp.Create();
foreach (var line in await Robots.ReadRulesAsync(http, new Uri("https://books.toscrape.com"))) Console.WriteLine(line);
No robots.txt: no crawl rules published

O Books to Scrape não publica nenhum ficheiro robots.txt e, de acordo com a norma, a ausência deste ficheiro significa que não se aplicam regras de rastreamento. Em sites reais, respeite as Disallow linhas relativas ao seu agente de utilizador e quaisquer Crawl-delay.

Tenha em mente que o robots.txt é uma convenção dos rastreadores, não uma autorização legal. Os termos de serviço de um site, o tipo de dados e o local onde você e o site operam também são importantes; por isso, informe-se sobre se a extração de dados da Web é legal antes de extrair qualquer conteúdo comercial. Recolha apenas o que precisa, armazene-o de forma segura e evite dados pessoais, a menos que tenha uma base legal clara. Para conhecer os padrões a que nos submetemos, consulte os nossos princípios de utilização responsável de dados da Web.

Perguntas frequentes

Qual é a melhor biblioteca C# para web scraping?

Não existe uma biblioteca única para todas as tarefas. Utilize o HttpClient para descarregar páginas, o AngleSharp ou o HtmlAgilityPack para analisar HTML e o Playwright para .NET quando o conteúdo for criado em JavaScript. Para tarefas de grande dimensão ou bloqueadas, mantenha o analisador e substitua a etapa de descarregamento por uma API de scraping. A maioria dos scrapers de produção combina duas ou três destas opções.

Devo utilizar o HtmlAgilityPack ou o AngleSharp?

Ambas são mantidas e analisam bem o HTML do mundo real. O AngleSharp segue o padrão HTML e suporta seletores CSS de forma nativa, pelo que os seletores copiados das DevTools do navegador funcionam tal como estão. O HtmlAgilityPack é mais antigo, muito tolerante a marcação incorreta e foi desenvolvido com base no XPath e no LINQ. Escolha o estilo de consulta que a sua equipa preferir. Mudar mais tarde afeta apenas o analisador.

O C# consegue extrair dados de sites renderizados em JavaScript?

Não apenas com o HttpClient, porque este apenas recebe o HTML que o servidor envia. O Playwright para .NET, o PuppeteerSharp ou o Selenium podem executar a página num navegador sem interface gráfica e fornecer-lhe o conteúdo renderizado. Verifique primeiro o código-fonte da página e as solicitações de rede em busca de JSON, o que geralmente é mais rápido e mais fiável do que a renderização.

O Playwright ou o Selenium são melhores para a extração de dados em C#?

Para quem está a dar os primeiros passos na extração de dados, o Playwright para .NET é normalmente mais simples: instala os seus próprios navegadores, aguarda os elementos automaticamente e suporta o Chromium, o Firefox e o WebKit a partir de uma única API. O Selenium WebDriver continua a fazer sentido quando a sua equipa já executa testes com o Selenium ou utiliza o Selenium Grid. Ambos são mais lentos e pesados do que simples pedidos HTTP, por isso utilize-os apenas quando necessário.

Existe um equivalente ao Scrapy para C#?

Nenhuma estrutura .NET é tão amplamente utilizada como o Scrapy em Python. A maioria das equipas de C# reúne elas próprias os mesmos componentes: o HttpClient com um pipeline de resiliência para a obtenção de dados, o AngleSharp ou o HtmlAgilityPack para a análise, o Parallel.ForEachAsync ou canais para a concorrência e uma fila ou base de dados para os URLs. Essa pilha cobre o que a maioria dos projetos de scraping necessita.

Como lido com o erro HTTP 429 «Too Many Requests» em C#?

Abrande o ritmo e tente novamente mais tarde. Adicione um pipeline de repetição com o Microsoft.Extensions.Http.Resilience, que repete as tentativas para respostas 429 e 5xx com recuo exponencial e aguarda o cabeçalho «Retry-After» quando o servidor o enviar. Reduza a concorrência e adicione um atraso entre os pedidos. Se os erros 429 continuarem a ocorrer a um ritmo moderado, o site está a pedir-lhe para parar.

Como posso evitar ser bloqueado ao fazer scraping com C#?

Reutilize um único HttpClient, envie um User-Agent válido, mantenha a concorrência baixa, armazene em cache as páginas que já obteve e tente novamente as respostas 429 apenas após o atraso solicitado pelo servidor. Se um site continuar a responder com erros 403 ou páginas de autenticação, encaminha os pedidos através de uma API de scraping que gere proxies e renderização, e mantém o teu analisador inalterado.

Como devo armazenar uma chave de API numa aplicação .NET?

Mantenha-a fora do código-fonte. Leia-a a partir de uma variável de ambiente, como WSA_API_KEY, com Environment.GetEnvironmentVariable, utilize o dotnet user-secrets durante o desenvolvimento local do ASP.NET Core e utilize o armazenamento de segredos do seu host em produção. Nunca cole chaves em URLs que partilhe, não as submeta ao Git nem registe o URL completo do pedido.

Como posso extrair dados de várias páginas em paralelo em C#?

Utilize o Parallel.ForEachAsync com o MaxDegreeOfParallelism definido para um número pequeno, como três a cinco por site, e partilhe um único HttpClient entre todas as tarefas. Recolha os resultados numa coleção segura para threads, como o ConcurrentBag. O limite deve refletir a capacidade do site, e não o número de núcleos de CPU que o seu computador possui.

A extração de dados publicamente disponíveis é frequentemente lícita, mas depende dos termos do site, do tipo de dados e da legislação local, especialmente no que diz respeito a dados pessoais e conteúdos protegidos por direitos de autor. A linguagem de programação não altera isso. Respeite o ficheiro robots.txt, evite dados pessoais sem fundamento legal e procure aconselhamento jurídico antes de proceder à extração de dados para um projeto comercial.

Mantenha o seu analisador C#, deixe as páginas difíceis para outros

O seu código AngleSharp ou HtmlAgilityPack não precisa de ser alterado quando um site começa a bloqueá-lo ou requer JavaScript. Aponte WsaClient para a API Scraper da WebScrapingAPI e continue a analisar em .NET. A renderização em JavaScript e a segmentação geográfica não têm custos adicionais, os pedidos falhados não são faturados e o período de teste gratuito inclui 5 000 pedidos.

Sobre o autor

Sorin-Gabriel Marica, Desenvolvedor Full-Stack @ WebScrapingAPI

Sorin-Gabriel Marica

Desenvolvedor Full-Stack

Sorin Marica é engenheiro Full Stack e DevOps na WebScrapingAPI, onde desenvolve funcionalidades do produto e mantém a infraestrutura que garante o bom funcionamento da plataforma.

Web Scraping com AWS Lambda: Guia para Python e Java 2026
Guias

Web Scraping com AWS Lambda: Guia para Python e Java 2026

TL;DR: Resumo: A extração de dados da Web com o AWS Lambda funciona melhor quando cada invocação é curta, delimitada e pode ser repetida de forma independente. Comece com HTTP direto, AWS SAM e S3 e, só depois, adicione SQS, contentores, renderização no navegador, proxies ou uma camada de recuperação gerida, apenas quando a carga de trabalho demonstrar que precisa deles.

Suciu Dan32 min read
Ler artigo
Como utilizar o GoSpider: rastrear, limpar URLs e extrair dados
Guias

Como utilizar o GoSpider: rastrear, limpar URLs e extrair dados

TL;DR: Resumo: O GoSpider é um rastreador de linha de comandos destinado a descobrir URLs, não um extrator completo de dados estruturados. Este guia sobre como utilizar o GoSpider mostra como realizar um rastreio limitado, gerir resultados de forma organizada, fazer a transferência de dados do Colly para CSV e seguir um percurso de diagnóstico para respostas 403 ou páginas que exijam renderização em JavaScript.

Suciu Dan23 min read
Ler artigo
Como fazer o Scrape Redfin: Guia Python para Dados de Propriedade
Guias

Como fazer o Scrape Redfin: Guia Python para Dados de Propriedade

TL;DR: A Redfin expõe pontos de extremidade de API ocultos que retornam JSON estruturado para listagens de propriedades, tornando possível ignorar totalmente a análise HTML frágil. Este guia orienta-o na construção de um scraper Python que extrai dados de aluguer e venda, pesquisa por localização, monitoriza novas listagens através de sitemaps XML e exporta resultados limpos para CSV ou JSON.

Suciu Dan14 min read
Ler artigo

Comece a construir

Pronto para expandir a sua recolha de dados?

Junte-se a mais de 2.000 empresas que utilizam a WebScrapingAPI para extrair dados da Web à escala empresarial, sem quaisquer custos de infraestrutura.