Quay lại blog

C# Thu thập dữ liệu trên web: Hướng dẫn thực tế

Michael Lee
Michael Lee

Expert Network Defense Engineer

11-Aug-2026

TL;DR:

  • HttpClient cộng với AngleSharp là sự kết hợp hiện đại của C#. AngleSharp sử dụng các bộ chọn CSS, vì vậy doc.QuerySelectorAll("div.quote") hoạt động giống như cách bộ chọn đó hoạt động trong bảng điều khiển của trình duyệt.
  • API Qu scraping Không Tồn Tại trả về với một bao bì JSON. Mã HTML được nhận bên trong data, vì vậy bạn đọc JSON trước và phân tích HTML sau.
  • Bước đầu tiên thông thường của PuppeteerSharp tải xuống một trình duyệt. Kết nối với một cái từ xa bỏ qua bước này hoàn toàn. Mỗi ví dụ khởi động đều mở với BrowserFetcher().DownloadAsync(); Puppeteer.ConnectAsync không bao giờ gọi nó. Đo lường: dự án xác minh kết thúc với 7.6 MB mà không có Chromium trên đĩa và không có bộ đệm trình duyệt PuppeteerSharp.
  • Đường dẫn CDP từ xa hoạt động mà không có nghi thức. ConnectAsync chấp nhận một điểm cuối wss:// mang theo thông tin xác thực chuỗi truy vấn và trả về title=Quotes to Scrape, quotes on page=10.
  • Bắt đầu miễn phí: Bảng điều khiển Scrapeless phát hành một khóa hoạt động với mọi ví dụ bên dưới.

Những gì bạn cần

Mọi thứ bên dưới đều chạy trên .NET SDK 8.0.129 chống lại quotes.toscrape.com, một trang web được xuất bản để thực hành scraping.

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 nằm trong thư viện lớp cơ sở, vì vậy chỉ có hai phụ thuộc là trình phân tích và, sau đó, trình điều khiển trình duyệt.

Về lựa chọn trình phân tích: HtmlAgilityPack là tên mà hầu hết tài liệu scraping C# muốn sử dụng và nó hoạt động. AngleSharp đáng được ưu tiên cho mã mới vì nó triển khai W3C DOM và sử dụng trực tiếp các bộ chọn CSS, có nghĩa là các bộ chọn bạn sao chép từ công cụ phát triển trình duyệt hoạt động không thay đổi thay vì cần dịch sang XPath.

Lấy và Phân Tích

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

Ba chi tiết đáng lưu ý.

Đặt một user agent. HttpClient không gửi bất kỳ cái nào theo mặc định, và một yêu cầu mà không có user agent là một trong những thứ rẻ nhất mà một trang web có thể xử lý khác đi.

Encoding.UTF8.GetByteCount(html) không phải là html.Length. Chuỗi .NET là UTF-16, vì vậy Length đếm các đơn vị mã UTF-16, không phải byte trên mạng. Trên trang này hai cái khác nhau vì các trích dẫn sử dụng dấu ngoặc kép uốn cong. Báo cáo cái nào bạn có nghĩa, nhưng đừng nhầm lẫn chúng khi so sánh một lần lấy trực tiếp với một phản hồi API.

Giới hạn các truy vấn con đến hàng. quotes[0].QuerySelector(...) tìm kiếm bên trong phần tử đó; gọi doc.QuerySelector(...) bên trong vòng lặp trả về giá trị của trích dẫn đầu tiên trong mỗi lần lặp, sản xuất một tập dữ liệu trông hoàn chỉnh và hoàn toàn sai.

Nơi C# Thông Thường Dừng Lại

Kịch bản hoạt động vì mục tiêu được render ở phía máy chủ và không từ chối người gọi. Hai điều kết thúc điều đó: nội dung chỉ tồn tại sau khi JavaScript thực thi, và các trang từ chối phục vụ một khách hàng HTTP trống. Không cái nào là hạn chế của .NET — không có khách hàng HTTP nào trong ngôn ngữ nào xử lý cả hai.

Từ đây sự thao tác là những công cụ khác nhau hơn là những công cụ tốt hơn, và con đường bình thường vẫn rẻ nhất nơi nó hoạt động. Đối với hình dạng tổng quát của những gì một trình duyệt thêm vào, giải thích tự động hóa trình duyệt bao quát thể loại này.

Tăng Cường Một: API Qu scraping Toàn Cầu

Điều này giữ cho mã của bạn là một người gọi HttpClient thông thường và di chuyển độ khó về phía máy chủ.

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

Bao bì là phần cần ghi nhớ. Thân phản hồi là JSON và mã HTML sống tại data. Đưa thân thô vào ParseDocumentAsync tạo ra một tài liệu không có phần tử phù hợp và không ném ra gì — các bộ chọn lặng lẽ trả về kết quả bằng không. Đọc JSON, lấy data, sau đó phân tích, và giữ phương thức đó trong một phương thức.

Lưu ý req.Headers.Add("x-api-token", key) thay vì một tiêu đề Authorization. Đây là một tiêu đề tùy chỉnh, vì vậy nó đi trên HttpRequestMessage.Headers trực tiếp; cố gắng diễn đạt nó như một tiêu đề xác thực kiểu được đi đường vòng không phù hợp ở đây.

Tăng Cường Hai: Một Trình Duyệt Từ Xa Mà Không Tải Xuống

Đây là nơi C# thực hiện tốt hơn rõ rệt so với các hướng dẫn thông thường. Mỗi PuppeteerSharp ví dụ khởi động bắt đầu như thế này:

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 không cần bất kỳ cái nào trong số đó. Bạn đang kết nối với một trình duyệt đã tồn tại ở nơi khác, vì vậy BrowserFetcher không bao giờ liên quan và không gì được lưu trên đĩa:

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

Hai điều này mang lại cho bạn ngoài sự tiện lợi.

Dự án vẫn nhỏ. Sau khi chạy mọi ví dụ trong bài viết này, dự án xác minh đo được 7.6 MB trên đĩa, không chứa Chromium, và không để lại bộ nhớ đệm trình duyệt PuppeteerSharp. Một quy trình LaunchAsync thêm một phiên bản trình duyệt vào mỗi máy và mỗi hình ảnh container chạy mã.
EvaluateExpressionAsync<T> giải mã cho bạn. Yêu cầu <int> trả về một int, vì vậy không có JsonElement để giải nén và không có phân tích thủ công giữa trình duyệt và biến của bạn.

Đáng để biết: ConnectAsync đã chấp nhận một wss:// endpoint mang thông tin xác thực của nó trong chuỗi truy vấn mà không cần cấu hình bổ sung. Điều này không phổ biến toàn cầu ở các client CDP của các ngôn ngữ — một số loại bỏ chuỗi truy vấn hoặc bỏ qua thương lượng tên máy chủ TLS — vì vậy nếu bạn đang chuyển từ một ngăn xếp khác, phần này có khả năng sẽ dễ dàng hơn so với phần bạn đã rời bỏ.

Lựa Chọn Giữa Ba Cách Tiếp Cận

cách tiếp cận sử dụng khi nào chi phí
HttpClient + AngleSharp HTML được_render từ máy chủ, mục tiêu rộng rãi thấp nhất; một gói
Universal Scraping API bị chặn hoặc thách thức, không cần JS một cuộc gọi HTTP, bao bì để giải nén
PuppeteerSharp + một trình duyệt từ xa nội dung yêu cầu thực thi JavaScript một phiên cho mỗi công việc, nhưng không có trình duyệt trên đĩa

Làm theo danh sách theo chiều từ dưới lên thay vì từ trên xuống. Một trang dường như cần một trình duyệt rất thường nhúng dữ liệu của nó trong một thẻ <script>, và một QuerySelector cộng với JsonDocument.Parse tốt hơn một phiên trình duyệt trên mọi phương diện.

Kết Luận

C# là một ngôn ngữ thu thập dữ liệu thoải mái và thư viện chuẩn bao phủ nhiều hơn những gì các hướng dẫn trong hệ sinh thái gợi ý. HttpClient và AngleSharp xử lý các trang được_render từ máy chủ với các bộ chọn CSS mà bạn có thể dán từ devtools, và hai cạm bẫy cần nhớ là đếm các đơn vị UTF-16 khi bạn có ý định nói đến byte, và truy vấn tài liệu khi bạn có ý định nói đến hàng.

Câu chuyện về trình duyệt tốt hơn so với tài liệu gợi ý. Bước tải xuống trong mọi hướng dẫn nhanh là một thuộc tính của việc khởi động trình duyệt cục bộ, không phải của PuppeteerSharp, và ConnectAsync loại bỏ nó cùng với dấu vết trên đĩa — một dự án 7,6 MB vẫn điều khiển một Chrome thực.

Sẵn Sàng Để Thu Thập Dữ Liệu Với C#?

Tạo một khóa trên bảng điều khiển Scrapeless và chạy ví dụ HttpClient trên một trang bạn đã thu thập. Nếu nó trả về những gì bạn mong đợi, bạn đã hoàn tất — một gói, không cần trình duyệt. Universal Scraping API bao phủ các trang nơi nó không, và tỷ lệ hiện tại có trên trang giá cả.

Câu Hỏi Thường Gặp

Q: AngleSharp hay HtmlAgilityPack?

Cả hai đều phân tích HTML thực tế rất tốt. AngleSharp triển khai W3C DOM và sử dụng các bộ chọn CSS, vì vậy các bộ chọn devtools không thay đổi; HtmlAgilityPack là XPath-first và có lịch sử dài hơn. Đối với mã mới, AngleSharp thường có nghĩa là công việc chuyển đổi ít hơn.

Q: Tôi có phải tải xuống Chromium để sử dụng PuppeteerSharp không?

Chỉ khi bạn khởi động một trình duyệt cục bộ. BrowserFetcher().DownloadAsync() thuộc về đường dẫn LaunchAsync; ConnectAsync gắn vào một trình duyệt đã tồn tại và không bao giờ chạm vào nó. Dự án xác minh ở đây vẫn giữ 7.6 MB mà không có trình duyệt trên đĩa.

Q: Tại sao phân tích của tôi không trả về gì khi cuộc gọi API thành công?

Bạn đang phân tích bao bì. Phản hồi là JSON với mã bên trong data, vì vậy AngleSharp nhận được một chuỗi JSON, không khớp gì cả và không ném ra gì cả. Đọc data từ JSON trước.

Q: Tại sao số byte lại khác với độ dài chuỗi?

Chuỗi .NET là UTF-16, vì vậy string.Length đếm các đơn vị mã trong khi Encoding.UTF8.GetByteCount đếm byte như đã chuyển. Bất kỳ trang nào với các ký tự không phải ASCII — bao gồm cả dấu nháy kiểu — sẽ cho thấy sự khác biệt.

Q: Tôi có nên tái sử dụng HttpClient không?

Có. Tạo một cái và chia sẻ nó, hoặc sử dụng IHttpClientFactory trong một ứng dụng được lưu trữ. Xây dựng một HttpClient mới cho mỗi yêu cầu làm cạn kiệt các socket dưới tải — Hướng dẫn HttpClient của chính Microsoft nêu rõ lý do — và điều này áp dụng cho các công cụ thu thập dữ liệu chính xác như nó áp dụng cho bất kỳ tải công việc HTTP .NET nào khác.

Tại Scrapless, chúng tôi chỉ truy cập dữ liệu có sẵn công khai trong khi tuân thủ nghiêm ngặt các luật, quy định và chính sách bảo mật trang web hiện hành. Nội dung trong blog này chỉ nhằm mục đích trình diễn và không liên quan đến bất kỳ hoạt động bất hợp pháp hoặc vi phạm nào. Chúng tôi không đảm bảo và từ chối mọi trách nhiệm đối với việc sử dụng thông tin từ blog này hoặc các liên kết của bên thứ ba. Trước khi tham gia vào bất kỳ hoạt động cạo nào, hãy tham khảo ý kiến ​​cố vấn pháp lý của bạn và xem xét các điều khoản dịch vụ của trang web mục tiêu hoặc có được các quyền cần thiết.

Bài viết phổ biến nhất

Danh mục