開發網頁爬蟲時,三大核心挑戰:遵守緩存減輕伺服器壓力、正確處理多種編碼與壓縮、高效解析 HTML 結構。本文以實戰代碼逐一攻克。
HTTP 規範支援 Last-Modified / ETag 條件請求,未變更回傳 304 Not Modified,無需傳輸內容。
static void CrawlWithCache()
{
DateTime lastModified = DateTime.MinValue;
for (int i = 0; i < 10; i++)
{
try
{
var req = (HttpWebRequest)WebRequest.Create("https://www.cnblogs.com");
req.Method = "HEAD"; // 僅取標頭
if (i > 0) req.IfModifiedSince = lastModified;
req.Timeout = 3000;
using var resp = (HttpWebResponse)req.GetResponse();
Console.WriteLine($"第 {i+1} 次狀態碼: {resp.StatusCode}");
if (resp.StatusCode == HttpStatusCode.OK)
{
lastModified = DateTime.Parse(resp.Headers[HttpResponseHeader.Date]);
Console.WriteLine(" → 內容已更新,記錄新 Last-Modified");
}
}
catch (WebException ex) when (ex.Response is HttpWebResponse hr)
{
Console.WriteLine($"第 {i+1} 次狀態碼: {hr.StatusCode}");
}
Thread.Sleep(1000);
}
}
輸出示例:首次 200 OK,後續多次 304 Not Modified,完美符合緩存機制。
static string FetchHtml(string url)
{
var req = (HttpWebRequest)WebRequest.Create(url);
req.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; // 自動解壓
using var resp = (HttpWebResponse)req.GetResponse();
// 1. 判斷編碼
string encodingName = resp.CharacterSet;
if (encodingName == "ISO-8859-1") encodingName = "gb2312"; // 常見誤標修正
// 2. 取得解壓後 Stream
Stream stream = resp.GetResponseStream(); // AutomaticDecompression 已處理
// 3. 讀取並解碼
using var reader = new StreamReader(stream, Encoding.GetEncoding(encodingName));
return reader.ReadToEnd();
}
HttpWebRequest.AutomaticDecompression是 .NET 4.5+ 隱藏神技,自動處理 GZIP/Deflate,無需手動包裝GZipStream。
Install-Package HtmlAgilityPack
var html = FetchHtml("https://www.sohu.com/");
var doc = new HtmlDocument();
doc.LoadHtml(html);
// 1. 取 Title
string title = doc.DocumentNode.SelectSingleNode("//title")?.InnerText?.Trim();
// 2. 取 Keywords
string keywords = doc.DocumentNode
.SelectSingleNode("//meta[@name='keywords' or @name='Keywords']")
?.GetAttributeValue("content", "");
// 3. 取所有連結
var links = doc.DocumentNode.SelectNodes("//a[@href]")
?.Select(a => a.GetAttributeValue("href", ""))
.Where(h => h.StartsWith("http"))
.Distinct()
.ToList();
// 4. 取特定結構資料(如新聞列表)
var newsItems = doc.DocumentNode.SelectNodes("//div[@class='news-list']/ul/li")
?.Select(li => new
{
Title = li.SelectSingleNode(".//h3")?.InnerText?.Trim(),
Url = li.SelectSingleNode(".//a")?.GetAttributeValue("href", ""),
Time = li.SelectSingleNode(".//span[@class='time']")?.InnerText?.Trim()
})
.ToList();
| 語法 | 含義 |
|---|---|
//div | 文件中所有 div 元素 |
//div[@class='news'] | class='news' 的 div |
//ul/li | ul 直系子元素 li |
//div//a | div 所有後代 a 元素 |
//a/@href | 所有 a 元素的 href 屬性值 |
(//table)[2] | 第二個 table 元素 |
public class SimpleCrawler
{
private readonly HashSet _visited = new();
private readonly Queue _queue = new();
public async Task CrawlAsync(string startUrl, int maxPages = 100)
{
_queue.Enqueue(startUrl);
while (_queue.Count > 0 && _visited.Count < maxPages)
{
string url = _queue.Dequeue();
if (!_visited.Add(url)) continue;
try
{
string html = await FetchAsync(url);
var doc = new HtmlDocument();
doc.LoadHtml(html);
// 業務解析邏輯
ProcessPage(doc, url);
// 發現新連結
var links = ExtractLinks(doc, url);
foreach (var link in links) _queue.Enqueue(link);
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] {url}: {ex.Message}");
}
await Task.Delay(500); // 禮貌延遲
}
}
// ... FetchAsync, ProcessPage, ExtractLinks 實作
}
爬蟲開發的核心不是「如何拿到資料」,而是「如何負責任、高效率、穩健地持續拿到資料」。掌握條件請求、編碼自動偵測、XPath 精準定位,是邁向專業爬蟲工程師的必修課。