Zum Inhalt springen
Zurück zum Blog

Web-Scraping in C# mit HttpClient, AngleSharp und Playwright

Sorin-Gabriel MaricaZuletzt aktualisiert am 27 min read
Web-Scraping in C# mit HttpClient, AngleSharp und Playwright
Kurzfassung: Für Web-Scraping in C# starten Sie mit .NET 10, dem integrierten HttpClient zum Herunterladen von Seiten und AngleSharp zum Parsen von HTML mit CSS-Selektoren. Wählen Sie HtmlAgilityPack, wenn Sie XPath bevorzugen oder älteren Code pflegen, und ergänzen Sie Playwright für .NET, wenn JavaScript die Seite aufbaut. Dieser Leitfaden baut einen Scraper mit höflichen Wiederholungen, begrenztem Crawling, CSV- und JSON-Export sowie Offline-Tests.

Web-Scraping in C# beginnt mit einem bekannten Problem: Die benötigten Daten befinden sich auf einer Webseite, und Sie möchten sie als typisierte C#-Objekte erhalten, ohne die .NET-Umgebung zu verlassen. Unter .NET 10 enthält die Plattform bereits das meiste, was Sie benötigen, und die wenigen fehlenden Teile sind ausgereifte NuGet-Pakete.

In dieser Anleitung erstellen Sie Schritt für Schritt einen kleinen Scraper. Sie werden eine Seite mit AngleSharp (oder HtmlAgilityPack, falls Sie XPath bevorzugen) parsen, die Daten höflich mit einem HttpClient , das 429-Antworten auf die richtige Weise erneut versucht, paginierte Listen mit begrenzter Parallelität durchforsten und die Daten als CSV und JSON exportieren. Anschließend extrahieren Sie Daten, die von JavaScript generiert werden, rendern Seiten mit Playwright für .NET, leiten blockierte Seiten an eine Scraper-API weiter, ohne Ihren Parser zu verändern, und testen das Ganze offline. Die Beispiele verwenden zwei öffentliche Übungsseiten, die speziell für das Scraping erstellt wurden: „Books to Scrape“ und die /js/ Version von „Quotes to Scrape“.

Schnellstart – Installiere das .NET 10 SDK und erstelle eine Konsolenanwendung mit dotnet new console. – Fügen Sie einen Parser hinzu: dotnet add package AngleSharp. – Abrufen mit HttpClient.GetStringAsync(), wählen Sie mit QuerySelectorAll()und extrahieren Sie mit TextContent und GetAttribute(). – Verwenden Sie Playwright für .NET, wenn JavaScript die Seite aufbaut.

Was benötigen Sie für das Web-Scraping in C#?

Sie benötigen das .NET 10 SDK, HttpClient (integriert) zum Herunterladen von Seiten und einen HTML-Parser wie AngleSharp oder HtmlAgilityPack, um die Daten zu extrahieren. Fügen Sie „Microsoft.Extensions.Http.Resilience“ für Wiederholungsversuche, „CsvHelper“ für CSV-Dateien und „Playwright für .NET“ hinzu, wenn die Seite mit JavaScript erstellt wird. Alles andere ist bereits in .NET enthalten.

Ein C#-Web-Scraper lädt eine Seite herunter, findet die für Sie relevanten Teile und speichert sie. HttpClient und System.Text.Json sind in .NET enthalten, und die unten aufgeführten Pakete schließen die Lücken.

Beginnen Sie mit .NET 10. Dies ist die aktuelle Version mit langfristigem Support, dessen Laufzeit bis Ende 2028 reicht. Sowohl für .NET 8 als auch für .NET 9 endet der Support gemäß der .NET-Support-Richtlinie am 10. November 2026, sodass Tutorials, die sich auf diese Versionen beziehen, bald veraltet sein werden.

C#-Web-Scraping-Bibliotheken (NuGet-Versionen Stand Oktober 2026)

Bibliothek

Aufgabe

Verwenden Sie sie, wenn

NuGet (Okt. 2026)

HttpClient

Seiten und API-Antworten herunterladen

Immer (in .NET integriert)

Im Lieferumfang von .NET 10 enthalten

AngleSharp

Standardbasierter HTML-Parser mit CSS-Selektoren

Sie kopieren Selektoren aus den DevTools

1.8.6

HtmlAgilityPack

Toleranter HTML-Parser mit XPath und LINQ

Sie bevorzugen XPath oder pflegen älteren Code

1.13.1

Microsoft.Extensions.Http.Resilience

Wiederholungsversuche, Timeouts, „Retry-After“ für HttpClient

Mehr als eine Handvoll Anfragen

10.11.0

CsvHelper

CSV schreiben und lesen

Zeilen exportieren

34.0.0 (neue API; siehe Abschnitt „CSV“)

Playwright für .NET

Headless Chromium, Firefox, WebKit

Mit JavaScript erstellte oder interaktive Seiten

1.64.1

PuppeteerSharp

Headless Chromium mit einer API im Puppeteer-Stil

Portierung von Node-Puppeteer-Skripten

25.15.0

Selenium.WebDriver

Browser-Automatisierung über WebDriver

Bestehende Selenium-Tests oder Grid

4.52.0

ScrapySharp, HtmlAgilityPack.CssSelectors.NetCore

Ältere Hilfsfunktionen

Verwenden Sie diese nicht für neuen Code

Letzte Veröffentlichungen 2018 und 2019

Zwei Namen in der letzten Zeile tauchen immer noch in vielen C#-Scraping-Tutorials auf. ScrapySharp wurde zuletzt 2018 veröffentlicht, und HtmlAgilityPack.CssSelectors.NetCore, das Add-on, das HtmlAgilityPack Unterstützung für CSS-Selektoren bietet, wurde zuletzt 2019 veröffentlicht. Beides wird nicht mehr benötigt: AngleSharp unterstützt CSS-Selektoren nativ und wird aktiv weiterentwickelt, und HtmlAgilityPack selbst ist auf dem neuesten Stand. Die ersten drei Zeilen sowie Playwright decken nahezu jedes Projekt ab.

Wie richtet man ein C#-Scraping-Projekt ein?

Erstellen Sie eine Konsolenanwendung und fügen Sie die vier Pakete hinzu, die im statischen Teil dieses Leitfadens verwendet werden:

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

Jeder Editor ist geeignet. Wählen Sie in Visual Studio die Vorlage „Konsolenanwendung“ aus und fügen Sie die Pakete über „NuGet-Pakete verwalten“ hinzu. Installieren Sie in VS Code das C# Dev Kit und führen Sie die Befehle im Terminal aus. Rider funktioniert ebenfalls.

Speichere anschließend eine Kopie jeder Übungsseite in einem fixtures Ordner, damit du den Parser offline entwickeln kannst. Dieselben Dateien dienen später als Test-Fixtures:

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/

Verwenden Sie in der Windows PowerShell Invoke-WebRequest -OutFile fixtures/books-page1.html https://books.toscrape.com/index.html für jede Seite. Führen Sie die App aus dem Projektordner aus, damit File.ReadAllText("fixtures/...") sie gefunden werden.

Noch eine Funktion von .NET 10: dotnet run app.cs führt eine einzelne .cs Datei ohne Projektdatei aus, was im Playwright-Beispiel genutzt wird.

Wie kann man eine Webseite in C# auslesen?

Laden Sie den HTML-Code mit HttpClient herunter, laden Sie ihn in den HtmlParser von AngleSharp, wählen Sie die sich wiederholenden Elemente mit QuerySelectorAll() und einem CSS-Selektor aus und lesen Sie dann Text mit TextContent und Links mit GetAttribute(). Wandeln Sie jede Karte in einen typisierten Datensatz um, analysieren Sie Preise unter Berücksichtigung der invarianten Kultur und lösen Sie jeden Link anhand der Seiten-URL auf, von der er stammt.

„Books to Scrape“ ist eine Übungsbuchhandlung mit 1.000 Büchern auf 50 Katalogseiten. Jedes Buch befindet sich in einer <article class="product_pod"> Karte mit dem Titel-Link, der Sternebewertung, dem Preis und der Lagerbestandszeile. Beginnen Sie mit der Struktur der Daten. Ein sealed record bietet Ihnen einen unveränderlichen Typ mit Wertgleichheit, sodass Tests ganze Zeilen vergleichen können und ein SequenceEqual Aufruf nachweisen, dass zwei Parser übereinstimmen.

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

Das [CsvRecord] Attribut und das partial Schlüsselwort sind für CsvHelper 34 vorgesehen (siehe Abschnitt „Export“). Nun zum Parser. AngleSharp parst HTML so wie ein Browser, gemäß dem HTML-Standard, und unterstützt dieselben CSS-Selektoren, die man in DevTools eingeben würde.

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

Jedes Feld birgt eine kleine Tücke:

  • Der Selektor „card“. article.product_pod Passt auf jede Buchkarte und sonst nichts.
  • Der Titel. Der sichtbare Linktext ist abgeschnitten („A Light in the …“), lesen Sie daher den vollständigen Titel aus dem title Attribut ab.
  • Der Preis. Auf der Seite wird £51.77. Entfernen Sie das Währungszeichen und werten Sie den Wert als decimal mit CultureInfo.InvariantCulture. Ohne die unveränderliche Kultur decimal.Parse werden die Ländereinstellungen des Computers verwendet, und auf einem deutschen oder rumänischen Computer schlägt „51,77“ entweder fehl oder wird zu 5.177.
  • Die Bewertung. Die Sterne werden mit CSS gezeichnet, und die einzigen Daten im HTML-Code sind Klassennamen wie star-rating Three. AngleSharps ClassList stellt die Klassen bereit, und ein kleines Wörterbuch ordnet das Wort einer Zahl von 1 bis 5 zu. Alles Unerwartete wird zu 0, anstatt als falsche Schätzung gewertet zu werden.
  • Lagerbestand. Die Zeile zur Verfügbarkeit wird zu einer bool.
  • Die URL. Links auf dieser Website sind relativ und beziehen sich auf den Ordner der Seite selbst, nicht auf das Stammverzeichnis der Website. Auf der Startseite beginnt ein href mit catalogue/, auf Seite 2 hingegen nicht. new Uri(pageUrl, href) jeden Link anhand der Seite auflöst, von der er stammt – genau wie es ein Browser tut. Würde man die `href`-Attribute stattdessen an das Stammverzeichnis der Website anhängen, würden die Links auf der Startseite funktionieren, auf Seite 2 und den folgenden Seiten jedoch einen 404-Fehler auslösen.

Die if (link is null) continue; Zeile verhindert, dass eine einzelne ungewöhnliche Karte, wie beispielsweise ein Anzeigenplatz, den Durchlauf zum Absturz bringt. Die Tests überprüfen später die Zeilenanzahl.

Führe den Parser zunächst auf der gespeicherten Kopie aus, damit während der Iteration keine Netzwerkzugriffe stattfinden:

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

Sobald die Ausgabe korrekt aussieht, richten Sie denselben Parser auf die Live-Seite. Der PoliteHttp.Create() Helper ist der konfigurierte Client aus dem Abschnitt „HttpClient“ weiter unten. Für einen ersten Versuch funktioniert auch ein einfacher new HttpClient() ebenfalls aus.

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

Wie führt man das Scraping stattdessen mit HtmlAgilityPack und XPath durch?

HtmlAgilityPack ist der seit langem etablierte .NET-HTML-Parser, und die meisten älteren C#-Tutorials zum Scraping verwenden ihn. Er wird nach wie vor gepflegt, ist sehr tolerant gegenüber fehlerhaftem Markup und basiert auf XPath-Abfragen. Falls Ihr Team ihn bereits nutzt oder Sie in XPath denken, finden Sie hier die gleiche Extraktion:

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

Da Book es sich um einen Datensatz handelt, reicht ein Vergleich aus, um zu zeigen, dass beide Parser übereinstimmen:

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

Die XPath-Version verwendet vier Muster: //tag[contains(@class,'x')] um Elemente an beliebiger Stelle anhand ihrer Klasse zu finden, .// um innerhalb des aktuellen Knotens zu suchen, [@class='x'] um eine exakte Übereinstimmung bei Attributen zu finden und / für ein direktes Kind. Für alles, was darüber hinausgeht, sollten Sie eine XPath-Syntaxreferenz zur Hand haben.

Achten Sie auf eine Falle: SelectNodes() gibt null, keine leere Sammlung, wenn keine Übereinstimmung gefunden wird. Code, der das Ergebnis ohne Null-Prüfung durchläuft, löst beim NullReferenceException beim ersten Mal, wenn sich eine Seite ändert. HtmlEntity.DeEntitize ist vorhanden, da HtmlAgilityPack Entitäten wie &amp; in Attributwerten belässt.

Wenn Sie XPath lieber ganz überspringen möchten, funktioniert HtmlAgilityPack auch mit LINQ. doc.DocumentNode.Descendants("article").Where(n => n.HasClass("product_pod")) findet dieselben Karten, und du kannst Select() von dort aus weiterverketten.

Verwende AngleSharp, wenn du Selektoren aus den DevTools des Browsers kopierst und eine standardkonforme Analyse wünschst. Verwende HtmlAgilityPack, wenn du XPath oder LINQ bevorzugst oder Code pflegst, der diese bereits verwendet.

Wie finden Sie die richtigen CSS-Selektoren?

Öffne die Seite in Chrome oder Firefox, klicke mit der rechten Maustaste auf die gewünschten Daten und wähle „Inspect“ aus. Suche nach einer Klasse oder einem Tag, das sich einmal pro Datensatz wiederholt, teste es in der DevTools-Konsole mit `document.querySelectorAll()` und füge denselben Selektor dann in `QuerySelectorAll()` von AngleSharp ein. Bevorzuge kurze, stabile Klassennamen gegenüber langen, generierten Pfaden, die aus den DevTools kopiert wurden.

Auf „Books to Scrape“ gelangt man durch einen Rechtsklick auf einen Buchtitel und die Auswahl von „Inspect“ zu einem <a> innerhalb eines <h3>, innerhalb <article class="product_pod">. Dieser Artikel stellt die Datensatzgrenze dar: einer pro Buch, zwanzig pro Seite. In der Konsole document.querySelectorAll("article.product_pod").length ergibt sich der Wert 20.

Jeder Selektor in dieser Anleitung verwendet eines von sechs Mustern:

Muster

Beispiel

Übereinstimmungen

Tag

article

Jeder <article> Element

Klasse

.product_pod

Jedes Element mit dieser Klasse

Tag und Klasse

p.price_color

<p> Elemente mit dieser Klasse

Nachkomme

h3 a

Ein <a> beliebige Stelle innerhalb eines <h3>

Attribut

a[href]

Links, die ein href

kombinierten

li.next a

Der Link innerhalb des „next“-Listenelements

Der Selektor „Kopieren“ in den DevTools erzeugt lange Pfade wie #default > div > div > ... > li:nth-child(1) > article , die nicht mehr funktionieren, wenn das Layout einen Wrapper erhält div. Ein kurzer Selektor auf einer aussagekräftigen Klasse übersteht Neugestaltungen.

XPath ist die andere Option und kann Dinge leisten, die CSS nicht kann, wie beispielsweise ein Element anhand seines Textes auszuwählen oder zu einem übergeordneten Element zu navigieren. Einen direkten Vergleich findest du unter „Wann XPath CSS übertrifft“.

Tipp: Wenn ein Selektor in DevTools Elemente findet, in C# jedoch null zurückgibt, befinden sich die Daten wahrscheinlich nicht im rohen HTML-Code. DevTools zeigt die Seite nach der Ausführung von JavaScript an, während HttpClient nur das sieht, was der Server gesendet hat. Drücke Strg+U („Quelltext anzeigen“) und suche nach dem Text. Wenn er dort nicht vorhanden ist, springen Sie zum Abschnitt „JavaScript“ weiter unten.

Wie sollten Sie HttpClient für das Web-Scraping konfigurieren?

Erstellen Sie einen HttpClient und verwenden Sie ihn für jede Anfrage wieder, legen Sie „PooledConnectionLifetime“ so fest, dass DNS-Änderungen übernommen werden, senden Sie einen ehrlichen User-Agent und fügen Sie eine Wiederholungspipeline hinzu, die bei 408-, 429- und 5xx-Antworten eine Wartezeit einlegt und „Retry-After“ berücksichtigt. Microsoft.Extensions.Http.Resilience bietet dir dieses Wiederholungsverhalten in etwa fünfzehn Zeilen.

Es gibt zwei klassische Fehler. Der erste besteht darin, pro Anfrage einen neuen HttpClient pro Anfrage: Jede Instanz öffnet ihre eigenen Verbindungen, und bei einigen Tausend Seiten kann dies die Sockets des Rechners erschöpfen. Der zweite ist ein statischer Client, der Verbindungen niemals wiederverwendet, sodass er nach einer DNS-Änderung der Website weiterhin mit einer alten IP-Adresse kommuniziert.

Die HttpClient-Richtlinien von Microsoft lösen beide Probleme: Verwenden Sie einen langlebigen Client und legen Sie PooledConnectionLifetime in dessen Handler so ein, dass Verbindungen alle paar Minuten wiederverwendet werden. Dieselben Richtlinien zeigen, wie man einem statischen Client Ausfallsicherheit verleiht – das ist das unten gezeigte Muster.

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

Hier ist die Funktion der einzelnen Teile:

  • SocketsHttpHandler ist der Netzwerkstack unter HttpClient. PooledConnectionLifetime = 5 minutes recycelt Verbindungen, damit DNS-Änderungen wirksam werden, und AutomaticDecompression unterstützt gzip, Brotli und deflate.
  • Die Wiederholungsstrategie stammt aus der HTTP-Resilience-Bibliothek von Microsoft, die auf Polly aufbaut. HttpRetryStrategyOptions weiß bereits, bei welchen Fehlern ein erneuter Versuch sinnvoll ist: 408 Request Timeout, HTTP 429 Too Many Requests, 5xx-Serverfehler, Netzwerkausnahmen und Timeouts. Es wendet ein exponentielles Backoff an, fügt Jitter hinzu, damit parallele Worker nicht im Gleichschritt erneut versuchen, und wartet auf den Retry-After Header, sobald der Server einen sendet – damit teilt die Website mit, wie lange die Pause dauern soll.
  • Die Timeout-Strategie gewährt jedem Versuch standardmäßig 30 Sekunden. HttpClient.Timeout ist auf „unendlich“ gesetzt, da HttpClientder eigene Standardwert von 100 Sekunden andernfalls die Wiederholungsversuche unterbrechen würde. Die Pipeline verwaltet die Timeouts stattdessen selbst. Der optionale attemptTimeout Parameter ermöglicht es, bei langsameren Aufrufen, wie z. B. gerenderten API-Anfragen, ein längeres Limit zu verwenden.
  • Der User-Agent benennt Ihren Scraper und bietet eine Möglichkeit, Sie zu kontaktieren. Websites können Sie so erreichen, anstatt Sie zu blockieren. Ersetzen Sie die E-Mail-Adresse durch Ihre eigene. Informationen zu den übrigen Anfrage-Headern und deren Bedeutung finden Sie unter „Welche Header sind für das Scraping wichtig?“.

Drei Wiederholungsversuche reichen aus. Mehr davon führen seltener dazu, dass aus einer Ablehnung ein Erfolg wird; protokolliere daher den Fehler und fahre fort.

In ASP.NET Core oder einem Worker-Dienst sollten Sie auf den statischen Helper verzichten und die Verwaltung des Clients der Dependency Injection überlassen. Registrieren Sie einen typisierten Client mit builder.Services.AddHttpClient<BookFetcher>() und verketten Sie .AddStandardResilienceHandler() , um Wiederholungsversuche, Timeouts und einen Circuit Breaker mit sinnvollen Standardwerten zu erhalten. IHttpClientFactory kümmert sich dann für Sie um die Verbindungslaufzeiten. Die Parser-Klassen in dieser Anleitung ändern sich überhaupt nicht. Nur die Stelle, von der die HttpClient herkommt, ändert sich.

Wie kann man in C# mehrere Seiten scrapen?

Parsen Sie die erste Seite, lesen Sie den Link zur nächsten Seite aus, lösen Sie ihn anhand der aktuellen Seiten-URL mit `new Uri(pageUrl, href)` auf und wiederholen Sie den Vorgang, bis der Link verschwindet oder Sie ein Seitenlimit erreichen. Verwenden Sie denselben `HttpClient` wieder, warten Sie etwa eine Sekunde zwischen den Anfragen und lassen Sie die Wiederholungspipeline die 429- und 503-Antworten verarbeiten.

„Books to Scrape“ teilt seinen Katalog auf 50 Seiten auf, und jede Seite endet mit einem Pager, dessen „nächster“ Eintrag <li class="next"><a href="page-2.html">next</a></li>. Wenn man diesem Element folgt, bis es verschwindet, kann man die Seitenanzahl nie erraten.

// 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 führt bei jeder Schleife vier Schritte aus: die Seite abrufen, die Bücher mit demselben BookParser, sucht nach li.next aund wartet eine Sekunde. Auch hier kommt es auf die new Uri(url, next) Zeile spielt hier ebenfalls eine Rolle. Der nächste Link auf Seite 1 ist page-2.html, relativ zu /catalogue/, und die Auflösung anhand der aktuellen Seite liefert jedes Mal die richtige absolute URL. Jede Seite wird an ParseListing mit ihrer eigenen URL übergeben, sodass auch jeder Buchlink auf jeder Seite korrekt aufgelöst wird.

Der maxPages Parameter dient als Sicherheitsventil. Halten Sie ihn während der Entwicklung klein, damit ein Fehler im Selektor oder ein Fehler in der Schleife nur drei statt dreitausend Anfragen kostet. Erhöhen Sie ihn, sobald die Ausgabe korrekt aussieht.

Beginnen Sie auf catalogue/page-1.html, nicht auf der Startseite, damit jede Seiten-URL denselben Aufbau hat.

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

Zeile 21 ist das erste Buch auf Seite 2, und seine URL gibt den Status 200 zurück, was bestätigt, dass die seitenrelative Auflösung auch über die erste Seite hinaus funktioniert. An dieser Stelle scheitert die stammverzeichnisrelative Verknüpfung normalerweise.

Diese „Next-Link“-Schleife ist ein kleiner Crawler und kann dieselbe Seite nicht zweimal besuchen, da jede Seite nur nach vorne verweist. Sobald du Links folgst, die zurückführen können – wie Kategorieseiten, Filter oder „Ähnliche Artikel“ –, führe eine HashSet<string> Liste der besuchten URLs und überspringe alles, was bereits darin enthalten ist. Falls der Unterschied zwischen den beiden Aufgaben unklar ist: Hier erfährst du, wie sich Scraping und Crawling unterscheiden.

Die Verzögerung von einer Sekunde ist selbst auferlegt. Beginne auf echten Websites langsam, achte auf 429-Fehler und betrachte „robots.txt“ sowie die Bedingungen als Obergrenze.

Wie kann man Seiten in C# gleichzeitig scrapen?

Detailseiten enthalten zusätzliche Informationen wie die UPC und den genauen Lagerbestand. Diese einzeln abzurufen ist langsam, und sie alle auf einmal abzufragen ist unhöflich. Parallel.ForEachAsync liegt dazwischen: Es führt für jeden Artikel einen asynchronen Block aus, wobei höchstens MaxDegreeOfParallelism gleichzeitig ausgeführt werden.

FetchDetailsAsync Im obigen Crawler ist diese Anzahl auf drei begrenzt. Wählen Sie lieber eine kleine, feste Zahl pro Website statt Environment.ProcessorCount, den Wert, den viele Beispiele anstreben. Die Anzahl Ihrer CPU-Kerne sagt nichts darüber aus, was eine Website bewältigen kann, und beim Scraping wartet man ohnehin fast ausschließlich auf die Netzwerkverbindung.

Die Ergebnisse werden in ein ConcurrentBag, an das mehrere Aufgaben gleichzeitig sicher Daten hinzufügen können, und sie werden am Ende sortiert, da die parallelen Aufgaben in keiner bestimmten Reihenfolge abgeschlossen werden.

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

Jede Aufgabe nutzt denselben HttpClient, was sicher und beabsichtigt ist: HttpClient ist threadsicher für das Senden von Anfragen, und durch die gemeinsame Nutzung gelten die Wiederholungspipeline und der Verbindungspool für alle Worker gleichzeitig.

Wie speichert man in C# gescrapte Daten als CSV oder JSON?

Für JSON serialisieren Sie Ihre Liste von Datensätzen mit `System.Text.Json`, das im Lieferumfang von .NET enthalten ist. Für CSV verwenden Sie CsvHelper: Markieren Sie den Datensatz mit [CsvRecord], öffnen Sie einen Writer mit CsvWriter.ToFile() und rufen Sie WriteRecords() auf. CsvHelper 34 schreibt Zahlen standardmäßig mit der invarianten Kultur, sodass Preise auf jedem Rechner einen Punkt behalten.

Beide Formate erfordern nur wenige Zeilen:

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

Die Kopfzeile ergibt sich aus den Eigenschaftsnamen des Datensatzes, und die Spaltenreihenfolge folgt den Parametern des Datensatzes. JSON behält die gleichen Namen und Typen bei, sodass Preise als Zahlen erhalten bleiben und InStock bleiben als boolesche Werte erhalten. Verwenden Sie CSV für Tabellenkalkulationen und Importe, JSON für Programme und verschachtelte Daten.

CsvHelper 34 hat die API geändert. CsvHelper 34.0.0, veröffentlicht am 28. Oktober 2026, ist eine Neuprogrammierung. Code, der new CsvWriter(writer, CultureInfo.InvariantCulture)– wie es in den meisten C#-Tutorials zum Scraping gezeigt wird – lässt sich mit dem aktuellen Paket nicht mehr kompilieren. Verwenden Sie entweder die hier gezeigte v34-API oder fixieren Sie die vorherige Version mit dotnet add package CsvHelper --version 33.1.0 für älteren Code. Der Migrationsleitfaden für CsvHelper v34 listet die übrigen Änderungen auf.

Dies ist auch der Grund, warum Book erklärt wird partial mit einem [CsvRecord] Attribut deklariert. CsvHelper 34 generiert die Zuordnung zwischen dem Datensatz und seinen Spalten zur Kompilierungszeit, und der Quellcode-Generator benötigt einen partial Typ, um diesen Code hinzuzufügen.

Bei älteren Versionen musste man jedem Writer eine Kultur übergeben, und wenn man dies vergaß, schrieb ein deutscher Rechner 51,77 in eine kommagetrennte Datei schrieb. Version 34 verwendet standardmäßig die invariante Kultur.

Woher weißt du, ob eine Seite JavaScript benötigt?

Rufe den Roh-HTML-Code mit HttpClient ab und wende deinen Selektor darauf an. Wenn er nichts findet, der Browser die Daten aber anzeigt, wird dieser Inhalt von JavaScript erstellt. Bevor du einen Browser startest, durchsuche den Quellcode der Seite und den Reiter „Netzwerk“ in den DevTools: Viele Websites liefern die Datensätze als JSON aus, das System.Text.Json direkt lesen kann.

Die /js/ Version von „Quotes to Scrape“ sieht im Browser identisch aus wie die reguläre Website: zehn Zitate, jedes in einem div.quote mit dem Text, dem Autor und den Tags. Der Roh-HTML-Code erzählt eine andere Geschichte. Der Server sendet einen leeren Container sowie einen <script> Block, der jedes Zitat als JavaScript-Array enthält, und der Browser baut die div.quote Elemente daraus, nachdem die Seite geladen wurde.

Überprüfe also die Antwort, bevor du zum Browser greifst. Wenn die Datensätze in einem Script-Tag stehen, HttpClient und System.Text.Json aus.

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

Die erste Zeile ist die Überprüfung: null div.quote Elemente im rohen HTML-Code, auch wenn der Browser zehn anzeigt. AngleSharp findet dann das Skript, das var data, ein quellseitig generierter regulärer Ausdruck extrahiert das Array, und „System.Text.Json“ wandelt es in Quote Datensätze um. [GeneratedRegex] kompiliert das Muster zur Erstellungszeit und PropertyNameCaseInsensitive ordnet den Datensatz-Eigenschaften Schlüssel in Kleinbuchstaben zu.

Das gleiche Prinzip deckt zwei weitere häufige Fälle ab. Viele Produkt- und Artikelseiten betten strukturierte Daten in <script type="application/ld+json">, die Sie mit AngleSharp auswählen und direkt deserialisieren können – ganz ohne reguläre Ausdrücke. Und viele JavaScript-lastige Websites laden ihre Datensätze über eigene JSON-Endpunkte. Öffnen Sie die DevTools, wechseln Sie zum Tab „Netzwerk“, filtern Sie nach „Fetch/XHR“ und laden Sie die Seite neu. Wenn eine Anfrage die Daten zurückgibt, die Sie auf dem Bildschirm sehen, rufen Sie diese URL mit HttpClient.GetFromJsonAsync<T>() und überspringen Sie HTML vollständig.

Das Einlesen von JSON ist schneller als das Rendern und führt seltener zu Fehlern, da sich Datenformate seltener ändern als Layouts.

Wie scrapt man JavaScript-gerenderte Seiten in C#?

Verwende Playwright für .NET, die Browser-Automatisierungsbibliothek von Microsoft. Sie lädt Chromium herunter, lädt die Seite im Headless-Modus, wartet auf deinen Selektor und ermöglicht es dir, Elemente oder das gerenderte HTML auszulesen. PuppeteerSharp und Selenium WebDriver funktionieren ebenfalls. Bei vielen Seiten oder gesperrten Websites solltest du das Rendern über eine Scraper-API aus der Ferne durchführen, anstatt Browser selbst auszuführen.

Wenn kein JSON zum Auslesen vorhanden ist oder Sie zuerst klicken oder etwas eingeben müssen, verwenden Sie einen echten Browser. Playwright steuert Chromium, Firefox oder WebKit von C# aus an, führt sie standardmäßig im Headless-Modus aus und verfügt über integrierte Wartefunktionen in seinen Locators. Falls Ihnen das Konzept eines Browsers ohne Fenster neu ist: So funktionieren Headless-Browser.

Um den Browser aus Ihrem Hauptprojekt herauszuhalten, handelt es sich hierbei um eine dateibasierte .NET 10-App, die ein eigenes Paket deklariert:

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

Die beiden #: Zeilen am Anfang ersetzen eine Projektdatei. #:package ruft Playwright aus NuGet ab. #:property PublishAot=false ist erforderlich, da dateibasierte Apps standardmäßig native AOT-Einstellungen verwenden und Playwright auf einer reflektionsbasierten JSON-Serialisierung beruht, die bei AOT entfernt wird. Der erste Lauf mit install lädt einen passenden Chromium-Build herunter. Unter Linux benötigt Chromium möglicherweise auch Systembibliotheken; überprüfen Sie daher die Playwright-für-.NET-Dokumentation für Ihr Betriebssystem.

Die entscheidende Zeile lautet await quotes.First.WaitForAsync(). Sie wartet, bis mindestens ein div.quote vorhanden ist, was bedeutet, dass JavaScript den Aufbau der Liste abgeschlossen hat. Ein fester Task.Delay(3000) ist auf schnellen Seiten zu lang und auf langsamen zu kurz.

Wenn Sie lieber die gesamte Analyse an einer Stelle bündeln möchten, page.ContentAsync() gibt das gerenderte HTML als Zeichenfolge zurück. Übergebe diese an BookParser.Load() und fragen Sie es mit AngleSharp genau so ab, wie Sie es bei statischen Seiten getan haben.

PuppeteerSharp ist eine C#-Portierung von Nodes Puppeteer. Es konzentriert sich auf Chromium, und seine API entspricht fast Zeile für Zeile der von Puppeteer, was es zu einer guten Wahl macht, wenn du bestehende Node-Skripte auf .NET portierst.

Selenium WebDriver ist die seit langem etablierte Option und eignet sich für Teams, die bereits Selenium-Tests oder ein Selenium Grid ausführen. Beim Scraping müssen Sie damit rechnen, expliziteren Wartecode zu schreiben als bei Playwright, da die meisten Elemente eine WebDriverWait , bevor man sie auslesen kann.

Lokale Browser beanspruchen Arbeitsspeicher und Rechenleistung. Wenn Sie viele Sitzungen benötigen oder interaktive Schritte wie Klicks und das Ausfüllen von Formularen auf Websites ausführen müssen, die sich nur schwer automatisieren lassen, verlagert ein gehosteter automatisierter Browser diese Arbeit von Ihrem Rechner weg.

Wie nutzt man eine Web-Scraping-API aus C#?

Sende die Ziel-URL mit HttpClient an die Scraper-API und analysiere das zurückgegebene HTML mit deinem bestehenden Code. Bei der Scraper-API von WebScrapingAPI rufst du einen Endpunkt mit deinem api_key und der URL-kodierten Zieladresse auf; füge dann für JavaScript-Seiten „render_js=1“ und für den Standort „country“ hinzu. Lesen Sie den Schlüssel aus einer Umgebungsvariablen aus, niemals aus dem Quellcode.

Das Abrufen von Ihrem eigenen Rechner ist die richtige Standardeinstellung. Diese Symptome deuten darauf hin, dass dies zum Engpass geworden ist:

  • 403- oder 429-Antworten, die selbst bei einer schonenden Abfragerate und nach Wiederholungsversuchen immer wieder auftreten.
  • Anstelle des Inhalts, den Sie im Browser sehen, werden Authentifizierungs- oder Einwilligungsseiten angezeigt.
  • Inhalte, die sich je nach Land ändern, wie z. B. Preise, Verfügbarkeit oder Suchergebnisse, wenn Sie einen bestimmten Standort benötigen.
  • Mehr JavaScript-Rendering, als lokale Chromium-Instanzen bewältigen können.

Schließen Sie zunächst die grundlegenden Ursachen aus. Diese vier Vorgehensweisen beheben viele Blockaden, und es gibt darüber hinaus weitere Möglichkeiten, Blockaden zu reduzieren:

  • Verlangsamen Sie den Vorgang: weniger gleichzeitige Anfragen und eine Verzögerung zwischen ihnen.
  • Senden Sie einen ehrlichen, konsistenten User-Agent.
  • Speichern Sie bereits abgerufene Seiten im Cache, anstatt sie erneut herunterzuladen.
  • Laden Sie nur die Seiten und Felder, die Sie benötigen.

Wenn Sie einen eigenen Proxy-Pool betreiben, können Sie den HttpClient auch über einen Proxy leiten und alles andere unverändert lassen.

Wenn diese Maßnahmen nicht ausreichen, lassen Sie eine Scraper-API den Abruf übernehmen. Die Scraper-API von WebScrapingAPI akzeptiert eine URL und gibt die Seite zurück, wobei sie sich um Proxys, Wiederholungsversuche und optionales JavaScript-Rendering kümmert. Von C# aus ist es eine einzige GET-Anfrage, und eine kleine Klasse bildet die gesamte Integration:

// 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 ist öffentlich, sodass Sie genau sehen können, was gesendet wird, ohne tatsächlich etwas zu senden:

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

Schauen Sie sich den url= Wert an. Der ? und & wurde zu %3F und %26. Ohne Uri.EscapeDataString()würde die API tag=life als einen ihrer eigenen Parameter und würde die falsche Seite abrufen.

Der Schlüssel taucht im Code nie auf. Legen Sie ihn einmal pro Shell oder Rechner fest:

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

In einer ASP.NET Core-Anwendung dotnet user-secrets halten Sie den Schlüssel während der Entwicklung aus Ihrem Repository heraus, und der Secret-Speicher Ihres Hosts übernimmt dies in der Produktion. Fehlt die Variable, WsaClient bricht sie mit einer eindeutigen Meldung ab, bevor eine Anfrage gesendet wird, wie die zweite Ausgabezeile zeigt. Ist der Schlüssel falsch, antwortet die API mit einem HTTP-401-Fehler {"error":"Invalid API key"}und WsaClient zeigt diesen Body in einem HttpRequestException, sodass Sie den Grund sehen, anstatt ein leeres Ergebnis zu erhalten.

Zwei Parameter decken die meisten Anforderungen ab. renderJs: true „adds“ render_js=1, wodurch die Seite in einem Browser gerendert wird, bevor sie zurückgegeben wird. country: "us" fügt country=us, das die Seite von diesem Speicherort abruft. Das Rendern mit JavaScript und das Geotargeting kosten keine zusätzlichen Credits. Gerenderte Anfragen dauern länger als einfache, daher sollten Sie dem Client mit PoliteHttp.Create(TimeSpan.FromSeconds(120)).

Die Antwort ist HTML, daher wird sie unverändert in denselben BookParser , das Sie zu Beginn geschrieben haben, unverändert:

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

Beispielausgabe:

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

ParseListing erhält weiterhin die Ziel-URL, nicht den API-Endpunkt. Links werden anhand der Seite aufgelöst, von der das HTML stammt, sodass jede Buch-URL korrekt bleibt, unabhängig davon, wie die Seite abgerufen wurde. Die gerenderte „Zitate“-Seite enthält dieselben div.quote Elemente, die Playwright gesehen hat.

Welchen C#-Scraping-Ansatz sollten Sie verwenden?

Verwende HttpClient mit AngleSharp oder HtmlAgilityPack für statische Seiten. Füge die Resilience-Pipeline und eine Parallelitätsbegrenzung hinzu, wenn du viele Seiten crawlen möchtest. Lies JSON aus dem Script-Tag oder vom JSON-Endpunkt der Website aus, wenn JavaScript die Daten lädt. Verwenden Sie Playwright für einige wenige interaktive Seiten und eine Scraper-API, wenn Blockierungen oder das Datenvolumen das lokale Abrufen unzuverlässig machen.

Situation

Verwenden Sie

Warum

Statisches HTML, wenige Seiten

HttpClient + AngleSharp oder HtmlAgilityPack

Am wenigsten bewegliche Teile

Viele Seiten oder ein Crawl

Gemeinsam genutzter HttpClient mit der Resilience-Pipeline, Verzögerung, Parallel.ForEachAsync Cap

Wiederholungsversuche, „Retry-After“, höfliche Ratenbegrenzung

Daten im <script> JSON oder einem JSON-Endpunkt

HttpClient + System.Text.Json

Schneller und stabiler als das Rendern

JavaScript-basierte Seite, geringes Datenvolumen

Playwright für .NET

Echter Browser, integrierte Wartezeiten

Klicks, Anmeldungen oder mehrstufige Abläufe, die Sie automatisieren dürfen

Playwright (oder Selenium, falls Ihr Team dieses bereits einsetzt)

Unterstützung für Interaktionen

403/429-Fehler, die durch Wiederholungsversuche nicht behoben werden können, länderspezifische Inhalte oder Rendering bei hohem Datenaufkommen

Scraper-API über HttpClient

Verwaltete Proxys, Wiederholungsversuche und Rendering; Parser unverändert

Wenn Sie sich nicht sicher sind, wo eine Seite einzuordnen ist, gehen Sie diese Fragen der Reihe nach durch und hören Sie beim ersten „Ja“ auf:

  1. Befinden sich die Daten im Quelltext? Rufen Sie sie mit HttpClient ab und parsen Sie sie mit AngleSharp oder HtmlAgilityPack.
  2. Benötigen Sie viele Seiten? Fügen Sie die Resilience-Pipeline, eine Verzögerung und eine geringe Begrenzung der Parallelität hinzu.
  3. Befinden sich die Daten in einem Script-Tag oder einer JSON-Anfrage auf der Registerkarte „Netzwerk“? Lesen Sie sie mit System.Text.Json aus.
  4. Benötigt die Seite einen Browser, und das nur gelegentlich? Verwenden Sie Playwright für .NET.
  5. Sind Sie blockiert, benötigen Sie ein bestimmtes Land oder führen Sie das Rendering in großem Umfang durch? Senden Sie den Abruf über die Scraper-API und behalten Sie Ihren Parser bei.

Wie testet man einen C#-Web-Scraper?

Speichern Sie eine Kopie jedes Seitentyps als HTML-Datei, fügen Sie ein xUnit-Testprojekt hinzu, das diese Dateien offline parst, und überprüfen Sie die Zeilenanzahl, die Typwerte und die absoluten URLs. Wenn die Website ihr Markup ändert, schlägt ein Test innerhalb von Sekunden fehl, anstatt dass Ihr Scraper in der Produktion stillschweigend leere Ergebnisse zurückgibt.

Scraper scheitern unbemerkt: Eine Website benennt eine Klasse um, und der Job schreibt eine leere Datei, die wie ein Erfolg aussieht. Die Seiten, die du während der Einrichtung gespeichert hast, sind bereits deine Test-Fixtures.

Erstellen Sie ein Testprojekt neben dem Scraper und verweisen Sie darauf:

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

Füge dies zu „BookScraper.Tests.csproj“ hinzu, damit die gespeicherten Seiten neben die Test-Binärdateien kopiert werden:

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

Testen Sie dann das Verhalten, auf das Sie angewiesen sind, und nicht die Implementierung:

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

Die drei Tests sichern die drei Punkte ab, bei denen am ehesten Fehler auftreten: die Zeilenanzahl (der Karten-Selektor passt weiterhin), die URL auf Seite 2 (Links führen weiterhin zu dieser Seite) und die eingegebenen Werte (Preis und Bewertung lassen sich weiterhin auswerten). Seite 2 dient bewusst als Testumgebung, da sich dort Fehler bei root-relativen URLs zeigen.

Wende denselben Ansatz in der Produktion mit der Regel „Fail Loudly“ an: Wenn eine Listenseite null Karten zurückgibt, löse eine Ausnahme aus, anstatt eine leere Datei zu schreiben. Wenn der Test fehlschlägt, speichere eine neue Kopie der Seite, korrigiere den Selektor und führe den Test erneut aus. Erwäge außerdem, eine ScrapedAt Zeitstempel-Spalte in Ihre Datensätze einzufügen, damit jede Zeile anzeigt, wann sie erfasst wurde, und Sie die Durchläufe später vergleichen können.

Wie führt man verantwortungsbewusstes Scraping mit C# durch?

Lies vor dem Crawlen die Nutzungsbedingungen der Website und die robots.txt, halte die Anfragerate niedrig, identifiziere deinen Scraper mit einem User-Agent, der Kontaktdaten enthält, und sammle nur die Felder, die du benötigst. Vermeide personenbezogene Daten, sofern du keine rechtmäßige Grundlage dafür hast, und führe ohne Erlaubnis kein Scraping hinter einer Anmeldung durch. Bevorzuge eine offizielle API, sofern eine vorhanden ist.

„robots.txt“ ist eine Klartextdatei im Stammverzeichnis einer Website, die Crawlern mitteilt, welche Pfade übersprungen werden sollen, und deren Format durch den „robots.txt“-Standard (RFC 9309) definiert ist. Das Auslesen erfordert nur wenige Zeilen mit dem Client, den Sie bereits haben:

// 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“ veröffentlicht keine robots.txt-Datei, und gemäß dem Standard bedeutet das Fehlen einer solchen Datei, dass keine Crawling-Regeln gelten. Beachten Sie auf echten Websites die Disallow Einträge für Ihren User-Agent und alle Crawl-delay.

Beachten Sie, dass „robots.txt“ eine Crawler-Konvention und keine rechtliche Erlaubnis ist. Die Nutzungsbedingungen einer Website, die Art der Daten sowie der Ort, an dem Sie und die Website tätig sind, spielen ebenfalls eine Rolle. Informieren Sie sich daher darüber, ob Web-Scraping legal ist, bevor Sie kommerzielle Inhalte scrapen. Sammeln Sie nur das, was Sie benötigen, speichern Sie die Daten sicher und halten Sie sich von personenbezogenen Daten fern, es sei denn, Sie verfügen über eine klare Rechtsgrundlage. Die Standards, an die wir uns halten, finden Sie in unseren Grundsätzen für den verantwortungsvollen Umgang mit Webdaten.

Häufig gestellte Fragen

Was ist die beste C#-Bibliothek für Web-Scraping?

Es gibt nicht die eine Bibliothek, die für jede Aufgabe geeignet ist. Verwenden Sie HttpClient zum Herunterladen von Seiten, AngleSharp oder HtmlAgilityPack zum Parsen von HTML und Playwright für .NET, wenn der Inhalt mit JavaScript erstellt wird. Bei umfangreichen oder blockierten Aufgaben behalten Sie den Parser bei und ersetzen Sie den Download-Schritt durch eine Scraper-API. Die meisten Scraper in der Produktion kombinieren zwei oder drei dieser Komponenten.

Sollte ich HtmlAgilityPack oder AngleSharp verwenden?

Beide werden aktiv weiterentwickelt und analysieren HTML aus der Praxis gut. AngleSharp folgt dem HTML-Standard und unterstützt CSS-Selektoren nativ, sodass aus den DevTools des Browsers kopierte Selektoren unverändert funktionieren. HtmlAgilityPack ist älter, sehr tolerant gegenüber fehlerhaftem Markup und basiert auf XPath und LINQ. Wählen Sie den Abfragestil, den Ihr Team bevorzugt. Ein späterer Wechsel betrifft nur den Parser.

Kann C# JavaScript-gerenderte Websites scrapen?

Nicht allein mit HttpClient, da dieser nur den vom Server gesendeten HTML-Code empfängt. Playwright für .NET, PuppeteerSharp oder Selenium können die Seite in einem headless Browser ausführen und dir den gerenderten Inhalt bereitstellen. Überprüfe zunächst den Quellcode der Seite und die Netzwerkanfragen auf JSON – dies ist in der Regel schneller und zuverlässiger als das Rendern.

Ist Playwright oder Selenium besser für das Scraping in C# geeignet?

Für Einsteiger in das Scraping ist Playwright für .NET in der Regel einfacher: Es installiert eigene Browser, wartet automatisch auf Elemente und unterstützt Chromium, Firefox und WebKit über eine einzige API. Selenium WebDriver ist nach wie vor sinnvoll, wenn Ihr Team bereits Selenium-Tests oder ein Selenium Grid einsetzt. Beide sind langsamer und ressourcenintensiver als einfache HTTP-Anfragen, verwenden Sie sie daher nur bei Bedarf.

Gibt es ein Scrapy-Äquivalent für C#?

Kein .NET-Framework ist so weit verbreitet wie Scrapy in Python. Die meisten C#-Teams stellen die gleichen Komponenten selbst zusammen: HttpClient mit einer Resilience-Pipeline zum Abrufen, AngleSharp oder HtmlAgilityPack zum Parsen, Parallel.ForEachAsync oder Channels für die Parallelität sowie eine Warteschlange oder Datenbank für URLs. Dieser Stack deckt die Anforderungen der meisten Scraping-Projekte ab.

Wie gehe ich in C# mit dem HTTP-Fehler 429 „Too Many Requests“ um?

Verlangsamen Sie den Vorgang und versuchen Sie es später erneut. Fügen Sie eine Wiederholungspipeline mit Microsoft.Extensions.Http.Resilience hinzu, die 429- und 5xx-Antworten mit exponentiellem Backoff erneut versucht und auf den „Retry-After“-Header wartet, sofern der Server einen sendet. Verringern Sie die Parallelität und fügen Sie eine Verzögerung zwischen den Anfragen ein. Wenn 429-Fehler weiterhin in angemessener Häufigkeit auftreten, fordert die Website Sie auf, den Vorgang zu beenden.

Wie vermeide ich es, beim Scraping mit C# blockiert zu werden?

Verwenden Sie denselben HttpClient wieder, senden Sie einen authentischen User-Agent, halten Sie die Parallelität gering, speichern Sie bereits abgerufene Seiten im Cache und wiederholen Sie Versuche bei 429-Antworten erst nach der vom Server geforderten Verzögerung. Wenn eine Website weiterhin mit 403-Fehlern oder Authentifizierungsseiten antwortet, leiten Sie die Anfragen über eine Scraper-API weiter, die Proxys und das Rendering verwaltet, und lassen Sie Ihren Parser unverändert.

Wie sollte ich einen API-Schlüssel in einer .NET-App speichern?

Halten Sie ihn aus dem Quellcode heraus. Lesen Sie ihn mit `Environment.GetEnvironmentVariable` aus einer Umgebungsvariablen wie `WSA_API_KEY` aus, verwenden Sie `dotnet user-secrets` bei der lokalen ASP.NET Core-Entwicklung und nutzen Sie den Secret Store Ihres Hosts in der Produktion. Fügen Sie Schlüssel niemals in URLs ein, die Sie teilen, committen Sie sie nicht in Git und protokollieren Sie niemals die vollständige Anfrage-URL.

Wie kann ich in C# viele Seiten parallel scrapen?

Verwenden Sie `Parallel.ForEachAsync` mit `MaxDegreeOfParallelism` auf einen kleinen Wert gesetzt, z. B. drei bis fünf pro Website, und nutzen Sie einen gemeinsamen `HttpClient` für alle Aufgaben. Sammeln Sie die Ergebnisse in einer threadsicheren Sammlung wie `ConcurrentBag`. Die Obergrenze sollte sich danach richten, was die Website verkraften kann, und nicht danach, wie viele CPU-Kerne Ihr Rechner hat.

Das Scrapen öffentlich zugänglicher Daten ist oft rechtmäßig, hängt jedoch von den Nutzungsbedingungen der Website, der Art der Daten und den lokalen Gesetzen ab, insbesondere bei personenbezogenen Daten und urheberrechtlich geschützten Inhalten. Die Programmiersprache ändert daran nichts. Beachten Sie die robots.txt-Datei, vermeiden Sie das Sammeln personenbezogener Daten ohne rechtmäßige Grundlage und holen Sie rechtlichen Rat ein, bevor Sie für ein kommerzielles Projekt Daten scrapen.

Behalten Sie Ihren C#-Parser, delegieren Sie die schwierigen Seiten

Ihr Code in AngleSharp oder HtmlAgilityPack muss nicht geändert werden, wenn eine Website Sie blockiert oder JavaScript benötigt. Verweisen Sie WsaClient auf die Scraper-API von WebScrapingAPI und fahren Sie mit dem Parsen in .NET fort. JavaScript-Rendering und Geotargeting kosten keine zusätzlichen Credits, fehlgeschlagene Anfragen werden nicht in Rechnung gestellt, und die kostenlose Testversion umfasst 5.000 Anfragen.

Über den Autor

Sorin-Gabriel Marica, Full-Stack-Entwickler @ WebScrapingAPI

Sorin-Gabriel Marica

Full-Stack-Entwickler

Sorin Marica ist Full-Stack- und DevOps-Entwickler bei WebScrapingAPI, wo er Produktfunktionen entwickelt und die Infrastruktur wartet, die für einen reibungslosen Betrieb der Plattform sorgt.

Web-Scraping mit AWS Lambda: Leitfaden für Python und Java 2026
Anleitungen

Web-Scraping mit AWS Lambda: Leitfaden für Python und Java 2026

TL;DR: Kurz gesagt: Web-Scraping mit AWS Lambda funktioniert am besten, wenn jeder Aufruf kurz, begrenzt und unabhängig wiederholbar ist. Beginnen Sie mit direktem HTTP, AWS SAM und S3 und fügen Sie erst dann SQS, Container, Browser-Rendering, Proxys oder eine verwaltete Abrufebene hinzu, wenn sich im Rahmen der Arbeitslast herausstellt, dass diese erforderlich sind.

Suciu Dan26 min read
Artikel lesen
So verwenden Sie GoSpider: Crawlen, URLs bereinigen und Daten extrahieren
Anleitungen

So verwenden Sie GoSpider: Crawlen, URLs bereinigen und Daten extrahieren

TL;DR: GoSpider ist ein Befehlszeilen-Crawler zum Auffinden von URLs, kein vollständiger Scraper für strukturierte Daten. Diese Anleitung zur Verwendung von GoSpider zeigt einen begrenzten Crawl, die saubere Verarbeitung der Ausgabe, die Übergabe von Colly an CSV sowie einen Diagnosepfad für 403-Antworten oder Seiten, die eine JavaScript-Darstellung erfordern.

Suciu Dan20 min read
Artikel lesen
Wie man Redfin scrappt: Python-Leitfaden für Immobiliendaten
Anleitungen

Wie man Redfin scrappt: Python-Leitfaden für Immobiliendaten

TL;DR: Redfin stellt versteckte API-Endpunkte zur Verfügung, die strukturiertes JSON für Immobilienangebote zurückgeben, wodurch das fragile HTML-Parsing vollständig übersprungen werden kann. Diese Anleitung führt Sie durch den Aufbau eines Python-Scrapers, der Miet- und Verkaufsdaten extrahiert, nach Standort sucht, neue Angebote über XML-Sitemaps überwacht und saubere Ergebnisse in CSV oder JSON exportiert.

Suciu Dan11 min read
Artikel lesen

Los geht’s

Sind Sie bereit, Ihre Datenerfassung zu erweitern?

Schließen Sie sich den über 2.000 Unternehmen an, die WebScrapingAPI nutzen, um Webdaten im Unternehmensmaßstab ohne zusätzlichen Infrastrukturaufwand zu extrahieren.