Aller au contenu
Retour au blog

Extraction de données Web en C# avec HttpClient, AngleSharp et Playwright

Sorin-Gabriel MaricaDernière mise à jour le 31 min read
Extraction de données Web en C# avec HttpClient, AngleSharp et Playwright
En bref : Pour le web scraping en C#, commencez sur .NET 10 avec le HttpClient intégré pour télécharger les pages et AngleSharp pour analyser le HTML avec des sélecteurs CSS. Choisissez HtmlAgilityPack si vous préférez XPath ou maintenez du code plus ancien, et ajoutez Playwright pour .NET lorsque JavaScript construit la page. Ce guide construit un scraper avec des relances respectueuses, un crawl borné, un export CSV et JSON et des tests hors ligne.

Le web scraping en C# part d’un problème bien connu : les données dont vous avez besoin se trouvent sur une page web, et vous souhaitez les obtenir sous forme d’objets C# typés sans sortir de l’environnement .NET. Sous .NET 10, la plateforme fournit déjà l’essentiel de ce dont vous avez besoin, et les quelques éléments manquants sont des packages NuGet éprouvés.

Ce guide vous explique, étape par étape, comment créer un petit scraper. Vous allez analyser une page avec AngleSharp (ou HtmlAgilityPack, si vous préférez XPath), récupérer les données de manière « polie » à l’aide d’un HttpClient qui gère correctement les réponses 429 en effectuant des tentatives de reconnexion, vous explorerez des listes paginées avec une concurrence limitée, puis vous exporterez les données au format CSV et JSON. Vous extrairez ensuite des données générées par JavaScript, vous rendrez des pages avec Playwright pour .NET, vous transmettrez les pages bloquées à une API de scraping sans modifier votre analyseur, et vous testerez le tout hors ligne. Les exemples utilisent deux sites publics d’entraînement conçus pour le scraping : Books to Scrape et la /js/ version de Quotes to Scrape.

Démarrage rapide - Installez le SDK .NET 10 et créez une application console avec dotnet new console. - Ajoutez un analyseur : dotnet add package AngleSharp. - Récupérez les données avec HttpClient.GetStringAsync(), sélectionnez avec QuerySelectorAll(), puis extrayez avec TextContent et GetAttribute(). - Utilisez Playwright pour .NET lorsque JavaScript génère la page.

De quoi avez-vous besoin pour le web scraping en C# ?

Vous avez besoin du SDK .NET 10, de HttpClient (intégré) pour télécharger les pages, et d’un analyseur HTML tel qu’AngleSharp ou HtmlAgilityPack pour extraire les données. Ajoutez Microsoft.Extensions.Http.Resilience pour les tentatives de reconnexion, CsvHelper pour les fichiers CSV, et Playwright pour .NET lorsque la page est générée par JavaScript. Tout le reste est fourni avec .NET.

Un scraper Web en C# télécharge une page, identifie les éléments qui vous intéressent et les enregistre. HttpClient et System.Text.Json sont fournis avec .NET, et les packages ci-dessous comblent les lacunes.

Commencez par .NET 10. Il s’agit de la version actuelle bénéficiant d’un support à long terme, dont la prise en charge s’étend jusqu’à fin 2028. .NET 8 et .NET 9 arriveront tous deux en fin de support le 10 novembre 2026, conformément à la politique de support de .NET ; les tutoriels qui s’y réfèrent sont donc sur le point de devenir obsolètes.

Bibliothèques C# de web scraping (versions NuGet en octobre 2026)

Bibliothèque

Fonction

À utiliser lorsque

NuGet (oct. 2026)

HttpClient

Téléchargement de pages et de réponses API

Toujours (intégré à .NET)

Fourni avec .NET 10

AngleSharp

Analyseur HTML conforme aux normes avec sélecteurs CSS

Vous copiez les sélecteurs depuis DevTools

1.8.6

HtmlAgilityPack

Analyseur HTML tolérant avec XPath et LINQ

Vous préférez XPath ou devez maintenir du code plus ancien

1.13.1

Microsoft.Extensions.Http.Resilience

Nouvelles tentatives, délais d'expiration, Retry-After pour HttpClient

Plus d'une poignée de requêtes

10.11.0

CsvHelper

Écriture et lecture de fichiers CSV

Exportation de lignes

34.0.0 (nouvelle API ; voir la section CSV)

Playwright pour .NET

Chromium, Firefox et WebKit en mode sans interface graphique

Pages créées en JavaScript ou interactives

1.64.1

PuppeteerSharp

Chromium en mode headless avec une API de type Puppeteer

Portage des scripts Node Puppeteer

25.15.0

Selenium.WebDriver

Automatisation du navigateur via WebDriver

Tests Selenium existants ou Grid

4.52.0

ScrapySharp, HtmlAgilityPack.CssSelectors.NetCore

Aides plus anciennes

Ne commencez pas de nouveau code avec eux

Dernières versions en 2018 et 2019

Deux noms de la dernière ligne apparaissent encore dans de nombreux tutoriels de scraping en C#. ScrapySharp a été mis à jour pour la dernière fois en 2018, et HtmlAgilityPack.CssSelectors.NetCore, l’extension qui apporte la prise en charge des sélecteurs CSS à HtmlAgilityPack, a été mis à jour pour la dernière fois en 2019. Aucun des deux n’est plus nécessaire : AngleSharp gère les sélecteurs CSS en natif et fait l’objet d’une maintenance active, tandis que HtmlAgilityPack lui-même est à jour. Les trois premières lignes, auxquelles s’ajoute Playwright, couvrent la quasi-totalité des projets.

Comment mettre en place un projet de scraping en C# ?

Créez une application console et ajoutez les quatre packages utilisés dans la partie statique de ce guide :

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

N'importe quel éditeur convient. Dans Visual Studio, choisissez le modèle « Console App » et ajoutez les paquets via « Gérer les paquets NuGet ». Dans VS Code, installez le C# Dev Kit et exécutez les commandes dans le terminal. Rider fonctionne également.

Ensuite, enregistrez une copie de chaque page d’exercice dans un fixtures dossier, afin de pouvoir développer l’analyseur hors ligne. Ces mêmes fichiers serviront plus tard de fixtures de test :

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/

Dans Windows PowerShell, utilisez Invoke-WebRequest -OutFile fixtures/books-page1.html https://books.toscrape.com/index.html pour chaque page. Exécutez l'application depuis le dossier du projet afin que File.ReadAllText("fixtures/...") puisse les localiser.

Une autre fonctionnalité de .NET 10 : dotnet run app.cs exécute un seul .cs fichier sans fichier de projet, ce dont se sert l’exemple Playwright.

Comment extraire le contenu d’une page web en C# ?

Téléchargez le code HTML avec HttpClient, chargez-le dans le HtmlParser d’AngleSharp, sélectionnez les éléments récurrents avec QuerySelectorAll() et un sélecteur CSS, puis lisez le texte avec TextContent et les liens avec GetAttribute(). Convertissez chaque fiche en un enregistrement typé, analysez les prix en utilisant la culture invariante, et résolvez chaque lien par rapport à l’URL de la page d’où il provient.

Books to Scrape est une librairie d’entraînement proposant 1 000 livres répartis sur 50 pages de catalogue. Chaque livre est représenté par une <article class="product_pod"> fiche contenant le lien vers le titre, la note par étoiles, le prix et la mention de disponibilité. Commencez par définir la structure des données. Un sealed record vous fournit un type immuable avec égalité de valeurs, ce qui permet aux tests de comparer des lignes entières et un SequenceEqual appel suffit pour vérifier que deux analyseurs sont en accord.

// 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);

L’ [CsvRecord] attribut et le partial sont destinés à CsvHelper 34 (voir la section sur l’exportation). Passons maintenant à l’analyseur. AngleSharp analyse le HTML comme le ferait un navigateur, en respectant la norme HTML, et prend en charge les mêmes sélecteurs CSS que ceux que vous saisirez dans 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;
    }
}

Chaque champ recèle un petit piège :

  • Le sélecteur « card ». article.product_pod correspond à toutes les fiches de livres et à rien d’autre.
  • Le titre. Le texte visible du lien est tronqué (« Une lumière dans le… »), il faut donc lire le titre complet à partir de l’ title attribut.
  • Le prix. La page affiche £51.77. Supprimez le symbole monétaire et analysez-le comme un decimal avec CultureInfo.InvariantCulture. Sans la culture invariante, decimal.Parse utilise les paramètres régionaux de la machine, et sur une machine allemande ou roumaine, « 51,77 » échoue ou se transforme en 5 177.
  • La note. Les étoiles sont dessinées avec du CSS, et la seule donnée présente dans le code HTML est un nom de classe tel que star-rating Three. La méthode ClassList expose ces classes, et un petit dictionnaire associe chaque mot à un nombre compris entre 1 et 5. Toute valeur inattendue est convertie en 0 au lieu d’une estimation erronée.
  • Stock. La ligne d’indisponibilité devient une bool.
  • L'URL. Les liens sur ce site sont relatifs, et ils sont relatifs au dossier de la page elle-même, et non à la racine du site. Sur la page d'accueil, un href commence par catalogue/, ce qui n’est pas le cas sur la page 2. new Uri(pageUrl, href) résout chaque lien par rapport à la page d’où il provient, ce qui correspond au fonctionnement d’un navigateur. En rattachant les balises href à la racine du site, on obtient des liens qui fonctionnent sur la page d’accueil mais renvoient à une erreur 404 à partir de la page 2.

La if (link is null) continue; ligne empêche une carte atypique, telle qu’un emplacement publicitaire, de faire planter l’exécution. Les tests vérifient ensuite le nombre de lignes.

Lancez d’abord l’analyseur sur la copie enregistrée, afin qu’aucune opération n’affecte le réseau pendant que vous effectuez vos itérations :

// 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

Une fois que la sortie semble correcte, dirigez le même analyseur vers la page en ligne. L’ PoliteHttp.Create() fonction d’aide correspond au client configuré dans la section HttpClient ci-dessous. Pour un premier essai, une simple new HttpClient() fonctionne également.

// 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)

Comment procéder au scraping avec HtmlAgilityPack et XPath à la place ?

HtmlAgilityPack est l’analyseur HTML historique de .NET, et la plupart des anciens tutoriels de scraping en C# l’utilisent. Il est toujours maintenu, très tolérant vis-à-vis des balises erronées, et s’appuie sur des requêtes XPath. Si votre équipe l’utilise déjà, ou si vous raisonnez en XPath, voici la même extraction :

// 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();
    }
}

Étant donné que Book est un enregistrement, une seule comparaison suffit pour prouver que les deux analyseurs sont d’accord :

// 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

La version XPath utilise quatre modèles : //tag[contains(@class,'x')] pour trouver des éléments n'importe où par classe, .// pour rechercher à l’intérieur du nœud actuel, [@class='x'] pour une correspondance exacte d’attribut, et / pour un enfant direct. Pour tout ce qui va au-delà, gardez un guide de syntaxe XPath à portée de main.

Attention à un piège : SelectNodes() la méthode renvoie null, et non une collection vide, lorsqu’aucune correspondance n’est trouvée. Un code qui parcourt le résultat en boucle sans vérifier la valeur null provoque une NullReferenceException dès la première modification de la page. HtmlEntity.DeEntitize est présent car HtmlAgilityPack conserve des entités telles que &amp; dans les valeurs d’attributs.

Si vous préférez éviter complètement l'XPath, HtmlAgilityPack fonctionne également avec LINQ. doc.DocumentNode.Descendants("article").Where(n => n.HasClass("product_pod")) trouve les mêmes cartes, et vous pouvez enchaîner Select() à partir de là.

Utilisez AngleSharp si vous copiez des sélecteurs depuis les outils de développement du navigateur et que vous souhaitez un analyse syntaxique conforme aux normes. Utilisez HtmlAgilityPack si vous préférez XPath ou LINQ, ou si vous gérez du code qui les utilise déjà.

Comment trouver les bons sélecteurs CSS ?

Ouvrez la page dans Chrome ou Firefox, faites un clic droit sur les données souhaitées, puis choisissez « Inspecter ». Recherchez une classe ou une balise qui se répète une fois par enregistrement, testez-la dans la console des outils de développement avec `document.querySelectorAll()`, puis collez ce même sélecteur dans la méthode `QuerySelectorAll()` d’AngleSharp. Privilégiez les noms de classes courts et stables plutôt que les longs chemins générés copiés depuis les outils de développement.

Sur Books to Scrape, un clic droit sur le titre d’un livre suivi de la sélection de « Inspecter » vous amène à un <a> à l’intérieur d’un <h3>, à l’intérieur <article class="product_pod">. Cet élément correspond à la limite d’un enregistrement : un par livre, vingt par page. Dans la console, document.querySelectorAll("article.product_pod").length renvoie 20.

Chaque sélecteur de ce guide utilise l’un des six modèles suivants :

Modèle

Exemple

Correspondances

Balise

article

Chaque <article> élément

de la classe

.product_pod

Tout élément ayant cette classe

Balise et classe

p.price_color

<p> Éléments ayant cette classe

Descendant

h3 a

Un <a> n'importe où à l'intérieur d'un <h3>

Attribut

a[href]

Liens comportant un href

Lien combiné

li.next a

Le lien à l’intérieur de l’élément « next » de la liste

Le sélecteur « Copier » de DevTools génère des chemins longs tels que #default > div > div > ... > li:nth-child(1) > article qui ne fonctionnent plus lorsque la mise en page intègre un élément d'encadrement div. Un sélecteur court sur une classe significative résiste aux refontes.

XPath est l’autre option, et il permet de faire des choses que le CSS ne peut pas faire, comme sélectionner un élément par son texte ou remonter vers un élément parent. Pour une comparaison côte à côte, voir les cas où XPath surpasse le CSS.

Astuce : si un sélecteur trouve des éléments dans DevTools mais renvoie zéro en C#, les données ne se trouvent probablement pas dans le code HTML brut. DevTools affiche la page après l'exécution du JavaScript, tandis que HttpClient ne voit que ce que le serveur a envoyé. Appuyez sur Ctrl+U (Afficher la source) et recherchez le texte. S'il n'y figure pas, passez à la section JavaScript ci-dessous.

Comment configurer HttpClient pour le web scraping ?

Créez un seul HttpClient et réutilisez-le pour chaque requête, définissez PooledConnectionLifetime afin que les changements de DNS soient pris en compte, envoyez un User-Agent valide, et ajoutez un pipeline de réessais qui s'interrompt en cas de réponses 408, 429 et 5xx et respecte l'en-tête Retry-After. Microsoft.Extensions.Http.Resilience vous permet d’implémenter ce comportement de réessai en une quinzaine de lignes.

Il existe deux erreurs classiques. La première consiste à créer un nouvel HttpClient par requête : chaque instance ouvre ses propres connexions, ce qui, sur quelques milliers de pages, peut épuiser les sockets de la machine. La seconde consiste à utiliser un client statique qui ne recycle jamais les connexions, ce qui fait qu’il continue de communiquer avec une ancienne adresse IP après un changement de DNS du site.

Les recommandations de Microsoft concernant HttpClient résolvent ces deux problèmes : utilisez un seul client à longue durée de vie et configurez PooledConnectionLifetime sur son gestionnaire pour que les connexions soient recyclées toutes les quelques minutes. Ces mêmes recommandations expliquent comment ajouter de la résilience à un client statique, ce qui correspond au modèle ci-dessous.

// 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;
    }
}

Voici le rôle de chaque composant :

  • SocketsHttpHandler est la pile réseau sous HttpClient. PooledConnectionLifetime = 5 minutes recycle les connexions pour que les changements de DNS prennent effet, et AutomaticDecompression gère les formats gzip, Brotli et deflate.
  • La stratégie de réessai provient de la bibliothèque de résilience HTTP de Microsoft, qui s’appuie sur Polly. HttpRetryStrategyOptions Elle sait déjà quelles erreurs méritent une nouvelle tentative : 408 Request Timeout, HTTP 429 Too Many Requests, erreurs serveur 5xx, exceptions réseau et délais d'attente dépassés. Elle applique un recul exponentiel, ajoute une gigue pour que les workers parallèles ne réessaient pas tous en même temps, et attend l' Retry-After en-tête lorsque le serveur en envoie un, c’est-à-dire lorsque le site vous indique la durée de la pause.
  • La stratégie de délai d’expiration accorde par défaut 30 secondes à chaque tentative. HttpClient.Timeout est défini sur « infini » car HttpClientla valeur par défaut de 100 secondes de [nom du pipeline] viendrait sinon interférer avec les tentatives de réessai. C’est le pipeline qui gère les délais d’expiration. Le paramètre facultatif attemptTimeout permet aux appels plus lents, tels que les requêtes API avec rendu, d’utiliser une limite plus longue.
  • L’en-tête User-Agent identifie votre scraper et fournit un moyen de vous contacter. Les sites peuvent ainsi vous joindre au lieu de vous bloquer. Remplacez l’adresse e-mail par la vôtre. Pour le reste des en-têtes de requête et pour savoir quand ils sont importants, consultez la section « Quels en-têtes sont importants pour le scraping ? ».

Trois tentatives suffisent. En faire davantage ne transforme que rarement un refus en succès ; enregistrez donc l’échec et passez à autre chose.

Dans ASP.NET Core ou un service de travail, ignorez l’assistant statique et laissez l’injection de dépendances gérer le client. Enregistrez un client typé avec builder.Services.AddHttpClient<BookFetcher>() et enchaînez .AddStandardResilienceHandler() pour bénéficier de tentatives de reconnexion, de délais d’expiration et d’un disjoncteur avec des valeurs par défaut raisonnables. IHttpClientFactory gère ensuite la durée de vie des connexions à votre place. Les classes d’analyse syntaxique de ce guide ne changent pas du tout. Seule l’origine de la HttpClient provient change.

Comment extraire le contenu de plusieurs pages en C# ?

Parsez la première page, lisez le lien vers la page suivante, résolvez-le par rapport à l’URL de la page actuelle avec `new Uri(pageUrl, href)`, et répétez l’opération jusqu’à ce que le lien disparaisse ou que vous atteigniez une limite de pages. Réutilisez le même HttpClient, attendez environ une seconde entre chaque requête, et laissez le pipeline de réessais gérer les réponses 429 et 503.

Books to Scrape répartit son catalogue sur 50 pages, et chaque page se termine par un paginateur dont l’élément « suivant » est <li class="next"><a href="page-2.html">next</a></li>. En le suivant jusqu’à ce qu’il disparaisse, vous n’aurez jamais à deviner le nombre de pages.

// 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 effectue quatre opérations à chaque boucle : récupérer la page, analyser ses livres avec le même BookParser, recherche li.next a, puis attend une seconde. La new Uri(url, next) ligne a également son importance ici. Le lien suivant sur la page 1 est page-2.html, par rapport à /catalogue/, et sa résolution par rapport à la page actuelle donne à chaque fois la bonne URL absolue. Chaque page est transmise à ParseListing avec sa propre URL, de sorte que chaque lien vers un livre, sur chaque page, est également résolu correctement.

Le maxPages paramètre sert de sécurité. Gardez-le faible pendant le développement, afin qu’une erreur de sélecteur ou un bug dans une boucle ne coûte que trois requêtes au lieu de trois mille. Augmentez-le une fois que le résultat s’affiche correctement.

Commencez par catalogue/page-1.html, et non de la page d’accueil, afin que toutes les URL de pages aient la même structure.

// 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

La ligne 21 correspond au premier livre de la page 2, et son URL renvoie un code 200, ce qui confirme que la résolution relative à la page fonctionne au-delà de la première page. C’est là que la jonction relative à la racine échoue généralement.

Cette boucle de liens suivants est un petit robot d’indexation, et elle ne peut pas visiter deux fois la même page car chaque page ne pointe que vers l’avant. Dès que vous suivez des liens pouvant créer une boucle, tels que les pages de catégories, les filtres ou les « articles associés », conservez une HashSet<string> liste des URL visitées et ignorez tout ce qui s’y trouve déjà. Si la différence entre les deux tâches n’est pas claire, voici en quoi le scraping et l’exploration diffèrent.

Le délai d’une seconde est auto-imposé. Sur les sites réels, commencez lentement, surveillez les codes 429 et considérez le fichier robots.txt et les conditions comme des limites à ne pas dépasser.

Comment extraire des pages simultanément en C# ?

Les pages de détail contiennent des informations supplémentaires telles que le code UPC et le nombre exact d’articles en stock. Les récupérer une par une est lent, et les récupérer toutes en même temps est inapproprié. Parallel.ForEachAsync se situe entre les deux : il exécute un corps asynchrone pour chaque article, avec au maximum MaxDegreeOfParallelism s'exécutant simultanément.

FetchDetailsAsync Dans le crawler ci-dessus, ce nombre est plafonné à trois. Choisissez un petit nombre fixe par site plutôt que Environment.ProcessorCount, la valeur à laquelle de nombreux exemples tendent. Le nombre de cœurs de votre processeur ne dit rien sur ce qu’un site web peut gérer, et le scraping se résume de toute façon presque entièrement à de l’attente sur le réseau.

Les résultats sont stockés dans un ConcurrentBag, auquel plusieurs tâches peuvent ajouter des éléments en toute sécurité simultanément, et ils sont triés à la fin car le traitement parallèle se termine sans ordre particulier.

// 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

Chaque tâche partage le même HttpClient, ce qui est sans danger et voulu : HttpClient est « thread-safe » pour l’envoi de requêtes, et le fait de le partager signifie que le pipeline de réessais et le pool de connexions s’appliquent simultanément à tous les workers.

Comment enregistrer des données extraites au format CSV ou JSON en C# ?

Pour le format JSON, sérialisez votre liste d’enregistrements à l’aide de System.Text.Json, fourni avec .NET. Pour le format CSV, utilisez CsvHelper : marquez l’enregistrement avec [CsvRecord], ouvrez un écrivain avec CsvWriter.ToFile(), puis appelez WriteRecords(). Par défaut, CsvHelper 34 écrit les nombres selon la culture invariante, ce qui garantit que les prix conservent un point décimal sur toutes les machines.

Les deux formats ne nécessitent que quelques lignes de code :

// 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

La ligne d’en-tête provient des noms de propriétés de l’enregistrement, et l’ordre des colonnes suit celui des paramètres de l’enregistrement. Le format JSON conserve les mêmes noms et types : les prix restent des nombres et InStock restent des booléens. Utilisez le format CSV pour les feuilles de calcul et les importations, et le format JSON pour les programmes et les données imbriquées.

CsvHelper 34 a modifié l’API. CsvHelper 34.0.0, publié le 28 octobre 2026, est une réécriture. Le code qui utilise new CsvWriter(writer, CultureInfo.InvariantCulture), ce que montrent la plupart des tutoriels de scraping en C#, ne compile plus avec le package actuel. Utilisez soit l’API v34 présentée ici, soit épinglez la version précédente à l’aide de dotnet add package CsvHelper --version 33.1.0 pour les anciens codes. Le guide de migration vers la v34 de CsvHelper répertorie le reste des modifications.

C’est également la raison pour laquelle Book est déclaré partial avec un [CsvRecord] . CsvHelper 34 génère le mappage entre l'enregistrement et ses colonnes au moment de la compilation, et le générateur de code source a besoin d'un partial type pour y ajouter ce code.

Dans les anciennes versions, il fallait passer une culture à chaque écrivain, et si on l’oubliait, une machine allemande écrivait 51,77 dans un fichier séparé par des virgules. La version 34 utilise par défaut la culture invariante.

Comment savoir si une page nécessite du JavaScript ?

Récupérez le code HTML brut avec HttpClient et appliquez-y votre sélecteur. S’il ne trouve rien mais que le navigateur affiche les données, c’est que JavaScript génère ce contenu. Avant de lancer un navigateur, parcourez le code source de la page et l’onglet « Réseau » des DevTools : de nombreux sites transmettent les enregistrements au format JSON, que System.Text.Json peut lire directement.

La /js/ version de « Quotes to Scrape » semble identique au site standard dans un navigateur : dix citations, chacune dans une div.quote contenant le texte, l’auteur et les balises. Le code HTML brut révèle une toute autre réalité. Le serveur envoie un conteneur vide ainsi qu’un <script> bloc contenant chaque citation sous forme de tableau JavaScript, et le navigateur en extrait les div.quote éléments à partir de celui-ci une fois la page chargée.

Vérifiez donc la réponse avant de vous tourner vers un navigateur. Si les enregistrements se trouvent dans une balise script, HttpClient et System.Text.Json suffisent.

// 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

La première ligne correspond à la vérification : zéro div.quote éléments dans le code HTML brut, même si le navigateur en affiche dix. AngleSharp identifie ensuite la balise script qui contient var data, une expression régulière générée dynamiquement extrait le tableau, et System.Text.Json le transforme en Quote enregistrements. [GeneratedRegex] compile le motif au moment de la compilation, et PropertyNameCaseInsensitive associe les clés en minuscules aux propriétés des enregistrements.

Le même principe s’applique à deux autres cas courants. De nombreuses pages de produits et d’articles intègrent des données structurées dans <script type="application/ld+json">, que vous pouvez sélectionner avec AngleSharp et désérialiser directement, sans avoir besoin d’expression régulière. De nombreux sites utilisant intensivement JavaScript chargent également leurs enregistrements à partir de leurs propres points de terminaison JSON. Ouvrez DevTools, accédez à l’onglet Réseau, filtrez par Fetch/XHR, puis rechargez la page. Si une requête renvoie les données que vous voyez à l’écran, appelez cette URL avec HttpClient.GetFromJsonAsync<T>() et ignorez complètement le HTML.

La lecture du JSON est plus rapide que le rendu et pose moins souvent de problèmes, car les formats de données changent moins souvent que les mises en page.

Comment extraire des données de pages rendues par JavaScript en C# ?

Utilisez Playwright pour .NET, la bibliothèque d’automatisation de navigateur de Microsoft. Elle télécharge Chromium, charge la page en mode headless, attend votre sélecteur et vous permet de lire les éléments ou le code HTML affiché. PuppeteerSharp et Selenium WebDriver fonctionnent également. Pour de nombreuses pages ou des sites bloqués, effectuez le rendu à distance à l’aide d’une API de scraping plutôt que de lancer vous-même des navigateurs.

Lorsqu’il n’y a pas de JSON à lire, ou que vous devez d’abord cliquer ou saisir du texte, utilisez un vrai navigateur. Playwright pilote Chromium, Firefox ou WebKit depuis C#, les exécute en mode sans interface graphique par défaut et intègre une fonctionnalité d’attente dans ses localisateurs. Si le concept de navigateur sans fenêtre est nouveau pour vous, voici comment fonctionnent les navigateurs sans interface graphique.

Pour que le navigateur ne fasse pas partie de votre projet principal, voici une application .NET 10 basée sur des fichiers qui déclare son propre package :

#: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

Les deux #: lignes en haut remplacent un fichier de projet. #:package permet de récupérer Playwright depuis NuGet. #:property PublishAot=false est nécessaire car les applications basées sur des fichiers utilisent par défaut les paramètres AOT natifs, et Playwright repose sur une sérialisation JSON basée sur la réflexion que l’AOT supprime. La première exécution avec install télécharge une version correspondante de Chromium. Sous Linux, Chromium peut également nécessiter des bibliothèques système ; consultez donc la documentation de Playwright pour .NET relative à votre système d’exploitation.

La ligne clé est await quotes.First.WaitForAsync(). Elle attend qu’au moins un élément div.quote existe, ce qui signifie que JavaScript a fini de construire la liste. Une valeur fixe Task.Delay(3000) est trop long sur les pages rapides et trop court sur les pages lentes.

Si vous préférez regrouper l’ensemble de l’analyse au même endroit, page.ContentAsync() renvoie le code HTML rendu sous forme de chaîne. Transmettez-la à BookParser.Load() et interrogez-le avec AngleSharp exactement comme vous l’avez fait pour les pages statiques.

PuppeteerSharp est un portage en C# de Puppeteer pour Node. Il est axé sur Chromium, et son API correspond presque ligne par ligne à celle de Puppeteer, ce qui en fait un choix idéal lorsque vous portez des scripts Node existants vers .NET.

Selenium WebDriver est l’option établie de longue date et convient aux équipes qui exécutent déjà des tests Selenium ou utilisent Selenium Grid. Pour le scraping, attendez-vous à écrire du code d’attente plus explicite qu’avec Playwright, car la plupart des éléments nécessitent un WebDriverWait avant de pouvoir être lus.

Les navigateurs locaux consomment de la mémoire et du CPU. Lorsque vous avez besoin de nombreuses sessions, ou d’étapes interactives telles que des clics et des remplissages de formulaires sur des sites qui résistent à l’automatisation, un navigateur automatisé hébergé décharge votre machine de ce travail.

Comment utiliser une API de web scraping depuis C# ?

Envoyez l’URL cible à l’API de scraping à l’aide de HttpClient, puis analysez le code HTML renvoyé avec votre code existant. Avec l’API de scraping de WebScrapingAPI, appelez un point de terminaison en indiquant votre api_key et la cible encodée en URL, puis ajoutez render_js=1 pour les pages JavaScript et country pour la localisation. Lisez la clé à partir d’une variable d’environnement, jamais à partir du code source.

La récupération depuis votre propre machine est le bon choix par défaut. Ces symptômes indiquent qu’elle est devenue le goulot d’étranglement :

  • des réponses 403 ou 429 qui reviennent sans cesse, même à un rythme raisonnable et après plusieurs tentatives.
  • Des pages de vérification ou de consentement s’affichent à la place du contenu que vous voyez dans un navigateur.
  • Un contenu qui varie selon le pays, comme les prix, la disponibilité ou les résultats de recherche, alors que vous avez besoin d’une localisation spécifique.
  • Un rendu JavaScript trop important pour que les instances Chromium locales puissent suivre.

Commencez par écarter les causes élémentaires. Ces quatre bonnes pratiques permettent de résoudre de nombreux blocages, et il existe d’autres moyens de les réduire :

  • Ralentissez : réduisez le nombre de requêtes simultanées et prévoyez un délai entre chacune d’elles.
  • Envoyez un User-Agent honnête et cohérent.
  • Mettez en cache les pages que vous avez déjà récupérées au lieu de les télécharger à nouveau.
  • Ne récupérez que les pages et les champs dont vous avez besoin.

Si vous gérez votre propre pool de proxys, vous pouvez également acheminer HttpClient via un proxy tout en conservant le reste tel quel.

Lorsque ces solutions ne suffisent pas, confiez l'étape de récupération à une API de scraping. L'API de scraping de WebScrapingAPI accepte une URL et renvoie la page, en gérant les proxys, les tentatives de reconnexion et le rendu JavaScript (facultatif). En C#, cela se résume à une seule requête GET, et une petite classe suffit pour l'intégration complète :

// 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 est publique, ce qui vous permet de voir exactement ce qui sera envoyé sans rien envoyer :

// 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.

Regardez la url= valeur. La balise ? et & est devenu %3F et %26. Sans Uri.EscapeDataString(), l’API interpréterait tag=life comme l’un de ses propres paramètres et récupérerait la mauvaise page.

La clé n'apparaît jamais dans le code. Définissez-la une seule fois par shell ou par machine :

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

Dans une application ASP.NET Core, dotnet user-secrets gardez la clé hors de votre référentiel pendant le développement, et le magasin de secrets de votre hébergeur s'en charge en production. Si la variable est manquante, WsaClient l'application s'arrête en affichant un message clair avant d'envoyer une requête, comme le montre la deuxième ligne de sortie. Si la clé est incorrecte, l'API renvoie un code HTTP 401 avec {"error":"Invalid API key"}, et WsaClient affiche ce corps de réponse dans un HttpRequestException, ce qui vous permet de voir la raison du problème au lieu d’un résultat vide.

Deux paramètres couvrent la plupart des besoins. renderJs: true ajoute render_js=1, qui affiche la page dans un navigateur avant de la renvoyer. country: "us" ajoute country=us, qui récupère la page à cet emplacement. Le rendu JavaScript et le ciblage géographique ne coûtent pas de crédits supplémentaires. Les requêtes avec rendu prennent plus de temps que les requêtes simples ; il est donc conseillé de définir un délai d’expiration plus long par tentative pour le client avec PoliteHttp.Create(TimeSpan.FromSeconds(120)).

La réponse est au format HTML ; elle est donc placée dans le même BookParser que vous avez écrit au début, sans modification :

// 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}");

Exemple de résultat :

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

ParseListing reçoit toujours l’URL cible, et non le point de terminaison de l’API. Les liens sont résolus par rapport à la page d’origine du code HTML ; ainsi, chaque URL de livre reste correcte, quelle que soit la manière dont la page a été récupérée. La page « Citations » rendue contient les mêmes div.quote éléments que Playwright a vus.

Quelle approche de scraping en C# devriez-vous utiliser ?

Utilisez HttpClient avec AngleSharp ou HtmlAgilityPack pour les pages statiques. Ajoutez le pipeline de résilience et une limite de concurrence lorsque vous explorez de nombreuses pages. Lisez le JSON de la balise script ou le point de terminaison JSON du site lorsque JavaScript charge les données. Utilisez Playwright pour quelques pages interactives, et une API de scraping lorsque des blocages ou le volume rendent la récupération locale peu fiable.

Situation

Utilisez

Pourquoi

HTML statique, quelques pages

HttpClient + AngleSharp ou HtmlAgilityPack

Le moins de composants possible

Nombreuses pages ou exploration

HttpClient partagé avec le pipeline de résilience, délai, Parallel.ForEachAsync limite

de tentatives, délai entre les tentatives,

Données au format <script> JSON ou un point de terminaison JSON

HttpClient + System.Text.Json

Plus rapide et plus stable que le rendu

page générée en JavaScript, faible volume

Playwright pour .NET

Véritable navigateur, avec attente intégrée

Clics, connexions ou flux en plusieurs étapes que vous pouvez automatiser

Playwright (ou Selenium si votre équipe l'utilise déjà)

Prise en charge des interactions

Erreurs 403/429 que les tentatives de réessai ne résolvent pas, contenu spécifique à un pays ou rendu à grande échelle

API de scraping via HttpClient

Proxys gérés, tentatives de réessai et rendu ; analyseur syntaxique inchangé

Si vous ne savez pas à quelle catégorie appartient une page, passez en revue ces questions dans l’ordre et arrêtez-vous dès la première réponse positive :

  1. Les données se trouvent-elles dans « Afficher la source » ? Récupérez-les avec HttpClient et analysez-les avec AngleSharp ou HtmlAgilityPack.
  2. Avez-vous besoin de nombreuses pages ? Ajoutez le pipeline de résilience, un délai et une petite limite de concurrence.
  3. Les données se trouvent-elles dans une balise script ou dans une requête JSON dans l'onglet Réseau ? Lisez-les avec System.Text.Json.
  4. La page nécessite-t-elle un navigateur, et ce seulement à quelques reprises ? Utilisez Playwright pour .NET.
  5. Êtes-vous bloqué, avez-vous besoin d’un pays spécifique ou effectuez-vous un rendu à grande échelle ? Envoyez la requête via l’API Scraper et conservez votre analyseur.

Comment tester un scraper web en C# ?

Enregistrez une copie de chaque type de page sous forme de fichier HTML, ajoutez un projet de tests xUnit qui analyse ces fichiers hors ligne, puis vérifiez le nombre de lignes, les valeurs typées et les URL absolues. Lorsque le site modifie son balisage, un test échoue en quelques secondes, au lieu que votre scraper renvoie silencieusement des résultats vides en production.

Les scrapers échouent en silence : un site renomme une classe, et le script génère un fichier vide qui donne l’impression d’un succès. Les pages que vous avez enregistrées lors de la configuration constituent déjà vos fixtures de test.

Créez un projet de test à côté du scraper et référencez-le :

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

Ajoutez-le à BookScraper.Tests.csproj afin que les pages enregistrées soient copiées à côté des binaires de test :

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

Testez ensuite le comportement dont vous dépendez, et non l’implémentation :

// 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)

Les trois tests vérifient les trois éléments les plus susceptibles de poser problème : le nombre de lignes (le sélecteur de fiches correspond toujours), l’URL de la page 2 (les liens renvoient toujours vers la page) et les valeurs saisies (le prix et la note s’analysent toujours correctement). La page 2 sert délibérément de test, car c’est là que se manifestent les bogues liés aux URL relatives à la racine.

Appliquez le même principe en production en suivant la règle « signaler clairement les échecs » : si une page de liste ne renvoie aucune carte, déclenchez une exception au lieu d’écrire un fichier vide. En cas d’échec, enregistrez une nouvelle copie de la page, corrigez le sélecteur, puis relancez le test. Pensez également à ajouter une ScrapedAt colonne d’horodatage à vos enregistrements, afin que chaque ligne indique la date et l’heure de sa collecte et que vous puissiez comparer les exécutions ultérieurement.

Comment effectuer un scraping responsable avec C# ?

Lisez les conditions d’utilisation du site et le fichier robots.txt avant de lancer l’exploration, maintenez un faible taux de requêtes, identifiez votre scraper à l’aide d’un agent utilisateur incluant vos coordonnées, et ne collectez que les champs dont vous avez besoin. Évitez les données à caractère personnel sauf si vous disposez d’une base légale, et ne scrapez pas les pages protégées par une connexion sans autorisation. Privilégiez une API officielle lorsqu’elle existe.

Le fichier robots.txt est un fichier en texte brut situé à la racine d’un site qui indique aux robots d’exploration les chemins à ignorer ; son format est défini par la norme robots.txt (RFC 9309). Pour le lire, quelques lignes de code suffisent avec le client dont vous disposez déjà :

// 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

Books to Scrape ne publie pas de fichier robots.txt ; selon la norme, l’absence de fichier signifie qu’aucune règle d’exploration ne s’applique. Sur les sites réels, respectez les Disallow lignes concernant votre agent utilisateur et toute Crawl-delay.

Gardez à l’esprit que le fichier robots.txt relève d’une convention entre les robots d’exploration et ne constitue pas une autorisation légale. Les conditions d’utilisation d’un site, le type de données et les lieux où vous et le site opérez ont également leur importance ; renseignez-vous donc sur la légalité du web scraping avant de scraper tout contenu commercial. Ne collectez que ce dont vous avez besoin, stockez les données en toute sécurité et évitez les données à caractère personnel, sauf si vous disposez d’une base légale claire. Pour connaître les normes auxquelles nous nous conformons, consultez nos principes relatifs à l’utilisation responsable des données Web.

Foire aux questions

Quelle est la meilleure bibliothèque C# pour le web scraping ?

Il n’existe pas de bibliothèque universelle adaptée à toutes les tâches. Utilisez HttpClient pour télécharger les pages, AngleSharp ou HtmlAgilityPack pour analyser le code HTML, et Playwright pour .NET lorsque le contenu est généré par JavaScript. Pour les tâches volumineuses ou bloquées, conservez l’analyseur et remplacez l’étape de téléchargement par une API de scraping. La plupart des outils de scraping en production combinent deux ou trois de ces solutions.

Dois-je utiliser HtmlAgilityPack ou AngleSharp ?

Les deux sont activement maintenues et analysent bien le code HTML réel. AngleSharp respecte la norme HTML et prend en charge les sélecteurs CSS de manière native ; ainsi, les sélecteurs copiés depuis les outils de développement du navigateur fonctionnent tels quels. HtmlAgilityPack est plus ancien, très tolérant vis-à-vis des balises incorrectes, et s’appuie sur XPath et LINQ. Choisissez le style de requête que votre équipe préfère. Un changement ultérieur n’affectera que l’analyseur.

Le C# permet-il de scraper des sites web rendus en JavaScript ?

Pas avec HttpClient seul, car il ne reçoit que le code HTML envoyé par le serveur. Playwright pour .NET, PuppeteerSharp ou Selenium peuvent exécuter la page dans un navigateur sans interface graphique et vous fournir le contenu affiché. Vérifiez d’abord le code source de la page et les requêtes réseau à la recherche de données JSON, ce qui est généralement plus rapide et plus fiable que le rendu.

Quel est le meilleur choix entre Playwright et Selenium pour le scraping en C# ?

Pour les débutants en extraction de données, Playwright pour .NET est généralement plus simple : il installe ses propres navigateurs, attend automatiquement les éléments et prend en charge Chromium, Firefox et WebKit à partir d’une seule API. Selenium WebDriver reste pertinent si votre équipe exécute déjà des tests Selenium ou utilise Selenium Grid. Ces deux solutions sont plus lentes et plus lourdes que de simples requêtes HTTP ; utilisez-les donc uniquement lorsque cela est nécessaire.

Existe-t-il un équivalent de Scrapy pour C# ?

Aucun framework .NET n’est aussi largement utilisé que Scrapy en Python. La plupart des équipes C# assemblent elles-mêmes les mêmes composants : HttpClient avec un pipeline de résilience pour la récupération, AngleSharp ou HtmlAgilityPack pour l’analyse, Parallel.ForEachAsync ou des canaux pour la concurrence, et une file d’attente ou une base de données pour les URL. Cette pile couvre les besoins de la plupart des projets de scraping.

Comment gérer l’erreur HTTP 429 « Too Many Requests » en C# ?

Ralentissez le rythme et réessayez plus tard. Ajoutez un pipeline de réessais avec Microsoft.Extensions.Http.Resilience, qui réessaie les réponses 429 et 5xx avec un recul exponentiel et attend l’en-tête Retry-After lorsque le serveur en envoie un. Réduisez votre concurrence et ajoutez un délai entre les requêtes. Si les erreurs 429 persistent à un rythme raisonnable, cela signifie que le site vous demande d’arrêter.

Comment éviter d'être bloqué lors d'un scraping avec C# ?

Réutilisez un seul HttpClient, envoyez un User-Agent authentique, maintenez un faible niveau de concurrence, mettez en cache les pages que vous avez déjà récupérées et ne réessayez les réponses 429 qu’après le délai demandé par le serveur. Si un site continue de renvoyer des erreurs 403 ou des pages d’authentification, acheminez les requêtes via une API de scraping qui gère les proxys et le rendu, et ne modifiez pas votre analyseur.

Comment stocker une clé API dans une application .NET ?

Ne l’incluez pas dans le code source. Lisez-la à partir d’une variable d’environnement telle que WSA_API_KEY avec Environment.GetEnvironmentVariable, utilisez dotnet user-secrets lors du développement local sous ASP.NET Core, et utilisez le magasin de secrets de votre hébergeur en production. Ne collez jamais de clés dans les URL que vous partagez, ne les commitez pas dans Git et n’enregistrez pas l’URL complète de la requête.

Comment extraire les données de plusieurs pages en parallèle en C# ?

Utilisez Parallel.ForEachAsync en définissant MaxDegreeOfParallelism sur un petit nombre, par exemple trois à cinq par site, et partagez un seul HttpClient entre toutes les tâches. Collectez les résultats dans une collection thread-safe telle que ConcurrentBag. La limite doit refléter la capacité du site web, et non le nombre de cœurs de processeur de votre machine.

Le scraping Web avec C# est-il légal ?

Le scraping de données accessibles au public est souvent légal, mais cela dépend des conditions d’utilisation du site, du type de données et de la législation locale, en particulier pour les données à caractère personnel et les contenus protégés par le droit d’auteur. Le langage de programmation n’y change rien. Respectez le fichier robots.txt, évitez de collecter des données à caractère personnel sans fondement juridique et demandez un avis juridique avant de procéder au scraping dans le cadre d’un projet commercial.

Conservez votre parseur C#, confiez-lui les pages difficiles

Vous n’avez pas besoin de modifier votre code AngleSharp ou HtmlAgilityPack lorsqu’un site commence à vous bloquer ou nécessite l’utilisation de JavaScript. Dirigez WsaClient vers l’API Scraper de WebScrapingAPI et continuez l’analyse en .NET. Le rendu JavaScript et le ciblage géographique ne coûtent pas de crédits supplémentaires, les requêtes échouées ne sont pas facturées et l’essai gratuit inclut 5 000 requêtes.

À propos de l'auteur

Sorin-Gabriel Marica, Développeur full-stack @ WebScrapingAPI

Sorin-Gabriel Marica

Développeur full-stack

Sorin Marica est ingénieur Full Stack et DevOps chez WebScrapingAPI ; il développe des fonctionnalités pour les produits et assure la maintenance de l'infrastructure qui garantit le bon fonctionnement de la plateforme.

Web scraping avec AWS Lambda : guide pour Python et Java 2026
Guides

Web scraping avec AWS Lambda : guide pour Python et Java 2026

TL;DR: En bref : le web scraping avec AWS Lambda fonctionne mieux lorsque chaque invocation est courte, bien délimitée et peut faire l'objet d'une nouvelle tentative de manière indépendante. Commencez par utiliser les requêtes HTTP directes, AWS SAM et S3, puis ajoutez SQS, des conteneurs, le rendu dans un navigateur, des proxys ou une couche de récupération gérée uniquement lorsque la charge de travail démontre qu'elle en a besoin.

Suciu Dan33 min read
Lire l'article
Comment utiliser GoSpider : exploration, nettoyage des URL et extraction de données
Guides

Comment utiliser GoSpider : exploration, nettoyage des URL et extraction de données

TL;DR: En bref : GoSpider est un robot d'exploration en ligne de commande destiné à découvrir des URL, et non un outil complet de collecte de données structurées. Ce guide d'utilisation de GoSpider présente comment effectuer une exploration limitée, gérer correctement les résultats, transférer les données de Colly vers un fichier CSV, ainsi qu'une procédure de diagnostic pour les réponses 403 ou les pages nécessitant un rendu JavaScript.

Suciu Dan24 min read
Lire l'article
Comment récupérer les données de Redfin : Guide Python des données immobilières
Guides

Comment récupérer les données de Redfin : Guide Python des données immobilières

TL;DR: Redfin expose des points d'extrémité d'API cachés qui renvoient du JSON structuré pour les listes de propriétés, ce qui permet d'ignorer complètement l'analyse HTML fragile. Ce guide vous accompagne dans la construction d'un scraper Python qui extrait les données de location et de vente, effectue des recherches par emplacement, surveille les nouvelles inscriptions via des sitemaps XML et exporte des résultats propres au format CSV ou JSON.

Suciu Dan15 min read
Lire l'article

Commencez à créer

Prêt à faire évoluer votre système de collecte de données ?

Rejoignez plus de 2 000 entreprises qui utilisent WebScrapingAPI pour extraire des données Web à l'échelle de l'entreprise, sans aucun coût d'infrastructure.