C# Web Scraping
Web scraping in C# uses HttpClient to download the page and HtmlAgilityPack or AngleSharp to parse it with XPath or CSS selectors; for pages that need JavaScript, Puppeteer Sharp or Playwright for .NET drive a real Chromium. A console app in Visual Studio or with the dotnet CLI, one NuGet package and about 20 lines make a working scraper. ProxyField's residential proxies plug into HttpClient through a WebProxy, with 412M+ IPs in 195+ countries from $1.95/GB; start with the Easy Trial: 1 GB for $2.95.
- residential IPs
- 412M+
- from, traffic never expires
- $1.95/GB
- Easy Trial
- 1 GB for $2.95
On this page 10 sections
- Is C# good for web scraping?
- The C# web scraping stack
- Set up the project in Visual Studio or with the dotnet CLI
- Scrape a website with HttpClient and HtmlAgilityPack
- Parsing with AngleSharp and CSS selectors
- JavaScript-rendered pages: Puppeteer Sharp
- Concurrency and retries in C#
- Web scraping in C (the language)
- Using a proxy in C# scrapers
- C# Web Scraping FAQ
Is C# good for web scraping?
Yes. .NET has a fast, async HttpClient, two mature HTML parsers, ports of the main browser automation tools, and strong typing that makes scraped records easy to validate. It suits teams that already build in .NET and scrapers that run as Windows services, Azure Functions or background workers. Python has more scraping frameworks and tutorials; C#'s strengths are performance, async I/O and tooling.
The C# web scraping stack
| Library | What it does | Runs JavaScript | License | NuGet package |
|---|---|---|---|---|
HttpClient | Downloads pages and JSON, async, HTTP/2 | No | Part of .NET | Built in |
| HtmlAgilityPack | Parses real-world HTML; XPath queries | No | MIT | HtmlAgilityPack |
| AngleSharp | Parses HTML to the W3C DOM; CSS selectors, QuerySelectorAll | No | MIT | AngleSharp |
| Puppeteer Sharp | .NET port of Puppeteer: drives Chromium or Firefox | Yes | MIT | PuppeteerSharp |
| Playwright for .NET | Drives Chromium, Firefox, WebKit with auto-waiting | Yes | Apache-2.0 | Microsoft.Playwright |
| Selenium WebDriver | Drives real browsers through WebDriver | Yes | Apache-2.0 | Selenium.WebDriver |
Start with HttpClient and a parser. If the data is not in the page source, find the JSON request the page makes (browser DevTools, Network tab) and call it with HttpClient; use a browser library only when there is no such request.
Set up the project in Visual Studio or with the dotnet CLI
Visual Studio: File > New > Project > Console App, pick a current .NET version, then right-click the project > Manage NuGet Packages and install HtmlAgilityPack (and AngleSharp or PuppeteerSharp as needed).
dotnet CLI (Windows, macOS or Linux):
dotnet new console -n BookScraper
cd BookScraper
dotnet add package HtmlAgilityPack
dotnet runScrape a website with HttpClient and HtmlAgilityPack
This program reads every book's title and price from a practice catalog, follows the "next" link through all pages and pauses a second between requests.
// Program.cs
using HtmlAgilityPack;
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
http.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (compatible; books-demo/1.0)");
var url = new Uri("https://books.toscrape.com/catalogue/page-1.html");
var books = new List<(string Title, string Price)>();
while (url != null)
{
var html = await http.GetStringAsync(url);
var doc = new HtmlDocument();
doc.LoadHtml(html);
var cards = doc.DocumentNode.SelectNodes("//article[contains(@class,'product_pod')]");
foreach (var card in cards ?? Enumerable.Empty<HtmlNode>()) // SelectNodes returns null on no match
{
var title = card.SelectSingleNode(".//h3/a").GetAttributeValue("title", "");
var price = card.SelectSingleNode(".//p[contains(@class,'price_color')]").InnerText.Trim();
books.Add((HtmlEntity.DeEntitize(title), price));
}
var next = doc.DocumentNode.SelectSingleNode("//li[@class='next']/a");
url = next == null ? null : new Uri(url, next.GetAttributeValue("href", "")); // resolve relative link
await Task.Delay(1000);
}
Console.WriteLine($"{books.Count} books");
foreach (var b in books.Take(5)) Console.WriteLine($"{b.Title}: {b.Price}");Key points: SelectNodes returns null, not an empty list, when nothing matches; HtmlEntity.DeEntitize turns & and similar into characters; new Uri(baseUri, relative) resolves relative links. Reuse one HttpClient for the whole run instead of creating one per request.
Parsing with AngleSharp and CSS selectors
AngleSharp parses HTML into a standards-based DOM, so the CSS selectors you test in the browser work unchanged:
// dotnet add package AngleSharp
using AngleSharp.Html.Parser;
var html = await http.GetStringAsync("https://books.toscrape.com/");
var document = await new HtmlParser().ParseDocumentAsync(html);
foreach (var card in document.QuerySelectorAll("article.product_pod"))
{
var title = card.QuerySelector("h3 a")?.GetAttribute("title");
var price = card.QuerySelector(".price_color")?.TextContent.Trim();
Console.WriteLine($"{title}: {price}");
}HtmlAgilityPack or AngleSharp? Both are free (MIT) and fast. HtmlAgilityPack is the long-standing choice with XPath and a forgiving parser; AngleSharp follows the HTML5 standard closely and uses CSS selectors natively. Pick the query language you prefer.
JavaScript-rendered pages: Puppeteer Sharp
When the data appears only after the page's scripts run, Puppeteer Sharp downloads a browser build and drives it from C#, with the same concepts as Puppeteer in Node.js (see web scraping with Puppeteer):
// dotnet add package PuppeteerSharp
using PuppeteerSharp;
await new BrowserFetcher().DownloadAsync(); // downloads a compatible browser once
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true });
await using var page = await browser.NewPageAsync();
await page.GoToAsync("https://quotes.toscrape.com/js/", WaitUntilNavigation.DOMContentLoaded);
await page.WaitForSelectorAsync(".quote");
var quotes = await page.EvaluateExpressionAsync<string[]>(
"[...document.querySelectorAll('.quote .text')].map(e => e.textContent)");
Console.WriteLine(string.Join("\n", quotes));Playwright for .NET (Microsoft.Playwright) is the alternative with automatic waiting and Chromium, Firefox and WebKit; see what Playwright is. A browser costs far more time, memory and traffic per page than HttpClient, so keep it for the pages that need it.
Concurrency and retries in C#
- Concurrency.
Parallel.ForEachAsync(urls, new ParallelOptions { MaxDegreeOfParallelism = 4 }, async (url, ct) => { ... })fetches several pages at once with a cap; keep the cap low per site. - Retries. Retry timeouts,
429and5xxresponses with exponential backoff. Polly, the .NET resilience library, provides a retry strategy with backoff and jitter, andMicrosoft.Extensions.Http.Resilienceadds a standard retry handler toHttpClientin apps that use dependency injection. When to retry and when not to is covered in web scraping best practices.
Web scraping in C (the language)
Searches for "C web scraping" often mean C#, but scraping in plain C works too: libcurl downloads the page and libxml2's HTML parser and XPath read it. It is far more code than C#, and it suits embedded tools or performance-critical crawlers more than everyday scraping.
// gcc scrape.c -o scrape $(curl-config --cflags --libs) $(xml2-config --cflags --libs)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#include <libxml/HTMLparser.h>
#include <libxml/xpath.h>
struct buf { char *data; size_t len; };
static size_t on_data(char *ptr, size_t size, size_t nmemb, void *userdata) {
struct buf *b = userdata;
size_t n = size * nmemb;
char *p = realloc(b->data, b->len + n + 1);
if (!p) return 0;
b->data = p;
memcpy(b->data + b->len, ptr, n);
b->len += n;
b->data[b->len] = '\0';
return n;
}
int main(void) {
struct buf b = {0};
CURL *curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, "https://books.toscrape.com/");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, on_data);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &b);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; c-demo/1.0)");
if (curl_easy_perform(curl) != CURLE_OK) return 1;
curl_easy_cleanup(curl);
htmlDocPtr doc = htmlReadMemory(b.data, (int)b.len, NULL, NULL, HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING);
xmlXPathContextPtr ctx = xmlXPathNewContext(doc);
xmlXPathObjectPtr res = xmlXPathEvalExpression((const xmlChar *)"//article//h3/a/@title", ctx);
for (int i = 0; res->nodesetval && i < res->nodesetval->nodeNr; i++) {
xmlChar *title = xmlNodeGetContent(res->nodesetval->nodeTab[i]);
printf("%s\n", (char *)title);
xmlFree(title);
}
xmlXPathFreeObject(res);
xmlXPathFreeContext(ctx);
xmlFreeDoc(doc);
free(b.data);
return 0;
}Using a proxy in C# scrapers
When a site limits requests per IP, send them through a rotating residential proxy. HttpClient takes a WebProxy with credentials through its handler:
using System.Net;
var handler = new HttpClientHandler
{
Proxy = new WebProxy("http://gate.proxyfield.io:7000")
{
Credentials = new NetworkCredential("<login>-country-us-city-new_york", "<password>"),
},
UseProxy = true,
};
using var http = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
Console.WriteLine(await http.GetStringAsync("https://api.ipify.org")); // the exit IPFor Puppeteer Sharp, pass Args = new[] { "--proxy-server=http://gate.proxyfield.io:7000" } in LaunchOptions and call page.AuthenticateAsync(new Credentials { Username = "<login>", Password = "<password>" }); in libcurl, set CURLOPT_PROXY and CURLOPT_PROXYUSERPWD. <login>-country-us-city-new_york asks for an IP in New York, United States. Region, session and session time go in the same username: -region-<region>, -session-<id> and -ttl-<seconds> (30 to 129,600 seconds, 20 minutes by default), as in <login>-country-us-region-new_york-city-new_york-session-he9f2qga-ttl-600; every part after the login is optional. Keep the request rate low, read the site's terms and collect only public data; see is web scraping legal. ProxyField residential proxies cost $2.95/GB for 1 GB down to $1.95/GB from 1,000 GB, with no subscription; the traffic never expires, and all purchases are final (residential and mobile proxy pricing).
C# Web Scraping FAQ
How do I scrape a website with C#?
ProxyField's residential proxies, from $1.95/GB, fit any way you scrape a website with C#: download the page with HttpClient, parse it with HtmlAgilityPack (XPath) or AngleSharp (CSS selectors), follow the "next" links and save the results. A WebProxy on the handler routes the requests through 412M+ residential IPs. Use Puppeteer Sharp or Playwright for .NET only for pages that need JavaScript.
How do I use Puppeteer Sharp in C#?
Install the PuppeteerSharp NuGet package and download a browser once with new BrowserFetcher().DownloadAsync(). Then launch it with Puppeteer.LaunchAsync(), open a page with GoToAsync(), wait for an element with WaitForSelectorAsync() and read data with EvaluateExpressionAsync().
What is Puppeteer Sharp?
A .NET port of Google's Puppeteer library, open source under the MIT license, that drives Chromium or Firefox from C#. It mirrors Puppeteer's API with async .NET methods, so Node.js examples translate almost line by line.
What is AngleSharp?
An open-source .NET library (MIT) that parses HTML and CSS into a DOM that follows the W3C standards. It supports CSS selectors through QuerySelector and QuerySelectorAll and can also fetch pages itself with its default loader.
How does AngleSharp compare to HTML Agility Pack?
Both parse HTML quickly and are free. HtmlAgilityPack is the older, forgiving parser queried with XPath; AngleSharp follows the HTML5 standard closely and is queried with CSS selectors. Choose by the query language your team prefers.
Is AngleSharp free to use?
Yes. AngleSharp is open source under the MIT license, so you can use it in commercial projects at no cost; the same is true of HtmlAgilityPack and Puppeteer Sharp.
Can I do web scraping in Visual Studio?
Yes. Create a Console App project, add HtmlAgilityPack, AngleSharp or Puppeteer Sharp through Manage NuGet Packages, and write the scraper in Program.cs. Visual Studio's debugger is handy for inspecting parsed nodes when a selector returns nothing.
Can you do web scraping in C, not C#?
Yes: libcurl downloads pages and libxml2 parses the HTML and runs XPath queries. It takes several times more code than C# or Python, so it is used mainly for embedded tools and performance-critical crawlers.
How do I retry a request in C#?
Wrap the call in a retry strategy with exponential backoff, retrying only timeouts, 429 and 5xx responses; Polly provides one, and Microsoft.Extensions.Http.Resilience adds a standard retry handler to HttpClient. Without a library, a loop with Task.Delay and doubling waits does the same.
What is the retry method in Polly?
Polly's retry strategy re-runs a failed operation a set number of times, with a configurable delay that can grow exponentially and include jitter, and only for the exceptions or results you choose to handle. Current Polly versions configure it through a resilience pipeline builder.
Related pages
Easy Trial: 1 GB for $2.95.
Try for the price of 1 GB, no subscription, traffic never expires.