返回博客

C# 网络爬虫:实用指南

Michael Lee
Michael Lee

Expert Network Defense Engineer

11-Aug-2026

TL;DR:

  • HttpClient 加上 AngleSharp 是现代 C# 的组合。 AngleSharp 使用 CSS 选择器,因此 doc.QuerySelectorAll("div.quote") 的行为与同一选择器在浏览器控制台中的行为一致。
  • Scrapeless Universal Scraping API 以 JSON 信封的形式响应。 标记内容在 data 中到达,因此您首先读取 JSON,然后解析 HTML。
  • PuppeteerSharp 通常的第一步是下载一个浏览器。连接到远程浏览器则完全跳过这一步。 每个快速入门都从 BrowserFetcher().DownloadAsync() 开始;Puppeteer.ConnectAsync 从不调用它。测量:验证项目完成时,磁盘上没有 Chromium,也没有 PuppeteerSharp 浏览器缓存,大小为 7.6 MB
  • 远程 CDP 路径毫不费力地工作。 ConnectAsync 接受一个携带查询字符串凭据的 wss:// 端点,并返回 title=Quotes to Scrapequotes on page=10
  • 免费开始: Scrapeless 仪表盘 发放的密钥可以与下面的每个示例一起使用。

你需要什么

以下所有内容均在 .NET SDK 8.0.129 上运行,目标网站为 quotes.toscrape.com,这是一个用于爬取练习的网站。

bash Copy
dotnet new console -o scraper
cd scraper
dotnet add package AngleSharp        # 1.7.1
dotnet add package PuppeteerSharp    # 25.5.0

HttpClientSystem.Text.Json 在基础类库中,因此唯一的依赖是解析器和后面的浏览器驱动。

关于解析器的选择:HtmlAgilityPack 是大多数 C# 爬虫材料选择的名称,它有效。 AngleSharp 在新代码中值得优先选择,因为它实现了 W3C DOM,并直接使用 CSS 选择器,这意味着您从浏览器开发工具中复制的选择器可以不变地工作,而不需要转换为 XPath。

获取和解析

csharp Copy
using AngleSharp.Html.Parser;
using System.Text;

var http = new HttpClient();
http.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (compatible; csharp-guide/1.0)");

var res  = await http.GetAsync("https://quotes.toscrape.com/");
var html = await res.Content.ReadAsStringAsync();
Console.WriteLine($"http={(int)res.StatusCode} bytes={Encoding.UTF8.GetByteCount(html)}");

var parser = new HtmlParser();
var doc    = await parser.ParseDocumentAsync(html);
var quotes = doc.QuerySelectorAll("div.quote");

Console.WriteLine($"quotes={quotes.Length}");
Console.WriteLine($"first_author={quotes[0].QuerySelector("small.author")!.TextContent}");
Console.WriteLine($"first_text={quotes[0].QuerySelector("span.text")!.TextContent[..40]}");
text Copy
http=200 bytes=11064
quotes=10
first_author=Albert Einstein
first_text=“The world as we have created it is a pr

值得注意的三个细节。

设置一个用户代理。 HttpClient 默认不发送用户代理,且没有用户代理的请求是网站区分对待的最便宜的方式之一。

Encoding.UTF8.GetByteCount(html) 不是 html.Length。 .NET 字符串是 UTF-16,因此 Length 计算的是 UTF-16 代码单元,而不是网络上的字节。在此页面上,两者有所不同,因为引号使用了卷曲引号。报告您指的内容,但在将直接获取与 API 响应进行比较时,不要将其混淆。

将子查询的范围限定于行。 quotes[0].QuerySelector(...) 在该元素内搜索;在循环中调用 doc.QuerySelector(...) 每次迭代返回第一个引号的值,产生看似完整但 uniformly 错误的数据集。

普通 C# 的止步之处

该脚本之所以有效,是因为目标在服务器端渲染,并且不屏蔽调用者。有两件事终止这一点:JavaScript 执行后才存在的内容,以及拒绝为裸 HTTP 客户端提供服务的网站。这两者都不是 .NET 的限制——任何语言中的 HTTP 客户端都无法处理。

从这里开始,升级使用的是不同的工具,而不是更好的工具,简单路径在有效的地方保持最便宜。关于浏览器添加的内容的大致形状,请参见 浏览器自动化解释

升级一:Universal Scraping API

这使您的代码成为普通的 HttpClient 调用者,并将难度转移到服务器端。

csharp Copy
using System.Text;
using System.Text.Json;
using AngleSharp.Html.Parser;

var key = Environment.GetEnvironmentVariable("SCRAPELESS_API_KEY")!;

var payload = JsonSerializer.Serialize(new
{
    actor = "unlocker.webunlocker",
    input = new { url = "https://quotes.toscrape.com/", js_render = false }
});

using var req = new HttpRequestMessage(
    HttpMethod.Post, "https://api.scrapeless.com/api/v2/unlocker/request")
{
    Content = new StringContent(payload, Encoding.UTF8, "application/json")
};
req.Headers.Add("x-api-token", key);

var apiRes = await http.SendAsync(req);
using var json = JsonDocument.Parse(await apiRes.Content.ReadAsStringAsync());

var keys = string.Join(",", json.RootElement.EnumerateObject().Select(p => p.Name));
Console.WriteLine($"http={(int)apiRes.StatusCode} envelope_keys={keys}");

var apiHtml = json.RootElement.GetProperty("data").GetString()!;
var apiDoc  = await new HtmlParser().ParseDocumentAsync(apiHtml);
Console.WriteLine($"quotes={apiDoc.QuerySelectorAll("div.quote").Length}");
text Copy
http=200 envelope_keys=code,data
quotes=10

信封是需要内化的部分。响应主体是 JSON,标记内容位于 data。将原始主体传递给 ParseDocumentAsync 会产生一个没有匹配元素且不抛出任何异常的文档——选择器安静地返回零结果。读取 JSON,获取 data,然后解析,并保持这个 unwrap 在一个方法中。

请注意使用 req.Headers.Add("x-api-token", key) 而不是 Authorization 头。它是一个自定义头,因此它直接附加在 HttpRequestMessage.Headers 上;试图将其表达为类型化的身份验证头是一条不适用的绕行路。

升级二:一个未下载的远程浏览器

在这里,C# 的表现明显优于通常的说明。每个 PuppeteerSharp 快速入门都是这样开始的:

csharp Copy
// The standard first step — downloads a Chromium build before anything runs.
await new BrowserFetcher().DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true });

Puppeteer.ConnectAsync 不需要其中的任何内容。您正在连接到已经存在于其他地方的浏览器,因此 BrowserFetcher 从未涉及,也没有任何东西落在磁盘上:

csharp Copy
using PuppeteerSharp;

var key = Environment.GetEnvironmentVariable("SCRAPELESS_API_KEY")!;

await using var browser = await Puppeteer.ConnectAsync(new ConnectOptions
{
    BrowserWSEndpoint = $"wss://browser.scrapeless.com/api/v2/browser?token={key}"
});

var page = await browser.NewPageAsync();
await page.GoToAsync("https://quotes.toscrape.com/");

Console.WriteLine($"title={await page.GetTitleAsync()}");
var n = await page.EvaluateExpressionAsync<int>("document.querySelectorAll('.quote').length");
Console.WriteLine($"quotes on page={n}");
text Copy
title=Quotes to Scrape
quotes on page=10

除了便利之外,这样做还为您带来了两件事。

项目保持小巧。 在运行本文中的每个示例后,验证项目在磁盘上的大小测量为 7.6 MB,不包含 Chromium,并且不留下 PuppeteerSharp 浏览器缓存。一个 LaunchAsync 工作流将浏览器构建添加到运行代码的每台机器和每个容器镜像中。
EvaluateExpressionAsync<T> 为您反序列化。 请求 <int> 会返回一个 int,因此没有 JsonElement 需要拆解,并且浏览器与您的变量之间没有手动解析。

值得了解的是:ConnectAsync 接受了一个 wss:// 端点,其中凭证在查询字符串中携带,而无需任何额外配置。这在各语言的 CDP 客户端之间并不普遍——有些会丢弃查询字符串或者省略 TLS 服务器名称协商——因此如果您是从另一个堆栈迁移,本部分很可能比您离开的部分更容易。

选择三者之间的差异

方法 使用时机 成本
HttpClient + AngleSharp 服务器渲染的 HTML,目标宽松 最低;一个包
通用抓取 API 被阻止或挑战,无需 JS 一次 HTTP 调用,需拆解的信封
PuppeteerSharp + 远程浏览器 内容需要 JavaScript 执行 每个任务一个会话,但磁盘上没有浏览器

从下往上查看列表。一页看似需要浏览器的页面通常在 <script> 标签中嵌入数据,而一个 QuerySelector 加上 JsonDocument.Parse 在各个维度上都胜过浏览器会话。

结论

C# 是一个舒适的抓取语言,标准库覆盖的内容比生态系统的教程暗示的要多。HttpClient 和 AngleSharp 处理服务器渲染的页面,您可以从开发工具中粘贴 CSS 选择器,而值得记住的两个陷阱是计算UTF-16单元时,您本是想计算字节,并在查询文档时,您本是想查询行。

浏览器故事比文档所暗示的更好。每个快速入门中的下载步骤是启动本地浏览器的一部分,而不是 PuppeteerSharp 的一部分,ConnectAsync 移除了它及其磁盘占用——一个 7.6 MB 的项目仍然驱动一个真实的 Chrome。

准备好用 C# 进行抓取吗?

Scrapeless 仪表板 创建一个密钥,并对您已收集的页面运行 HttpClient 示例。如果它返回您期望的结果,您就完成了——一个包,无需浏览器。 通用抓取 API 涵盖了其他情况下的页面,当前费用见 定价页面

常见问题解答

问:AngleSharp 还是 HtmlAgilityPack?

两者对现实世界的 HTML 解析得很好。AngleSharp 实现了 W3C DOM,并接受 CSS 选择器,因此开发工具选择器无缝迁移;HtmlAgilityPack 以 XPath 优先,历史更为悠久。对于新代码,AngleSharp 通常意味着更少的翻译工作。

问:我是否必须下载 Chromium 才能使用 PuppeteerSharp?

仅当您启动本地浏览器时。 BrowserFetcher().DownloadAsync() 属于 LaunchAsync 路径;ConnectAsync 附加到已存在的浏览器,并且从不接触它。此处的验证项目保持在 7.6 MB,没有磁盘上的浏览器。

问:当 API 调用成功时,为什么我的解析返回为空?

您正在解析信封。响应是包含在 data 里的 JSON;因此 AngleSharp 接收到一个 JSON 字符串,不匹配任何内容,也不抛出任何内容。请先从 JSON 中读取 data

问:为什么字节数与字符串长度不同?

.NET 字符串是 UTF-16,因此 string.Length 计算代码单位,而 Encoding.UTF8.GetByteCount 计算传输的字节。任何包含非 ASCII 字符(包括排版引号)的页面都会显示差异。

问:我应该重用 HttpClient 吗?

是的。创建一个并分享它,或者在托管应用中使用 IHttpClientFactory。每次请求构造一个新的 HttpClient 在负载下耗尽套接字——微软自己的 HttpClient 指南 清楚说明了原因——这对抓取器的适用性与其他 .NET HTTP 工作负载完全相同。

在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。

最受欢迎的文章

目录