採用流式字符掃描,避免載入完整 DOM 樹的記憶體開銷:
public class HtmlParser
{
public string Source { get; set; }
private int _pos = 0;
public char ParseNext()
{
if (_pos >= Source.Length) return '\0';
char ch = Source[_pos++];
if (ch == '<') // 遇到標籤起始
{
// 回退一步,讓 GetTag 完整讀取標籤
_pos--;
return '\0'; // 約定:返回 0 表示遇到標籤
}
return ch;
}
public HtmlTag GetTag()
{
// 解析 結構,返回 HtmlTag 對象
// 包含 TagName、Attributes 字典等
}
public bool Eof() => _pos >= Source.Length;
}
使用方式:
var parser = new HtmlParser { Source = html };
while (!parser.Eof())
{
char ch = parser.ParseNext();
if (ch == '\0') // 遇到標籤
{
HtmlTag tag = parser.GetTag();
if (tag.Name.Equals("A", StringComparison.OrdinalIgnoreCase))
{
string href = tag["HREF"]?.Value; // 提取超連結
EnqueueUrl(href);
}
}
else
{
// 處理純文本內容
}
}
private string DownloadPage(Uri uri)
{
var request = (HttpWebRequest)WebRequest.Create(uri);
using (var response = (HttpWebResponse)request.GetResponse())
using (var stream = response.GetResponseStream())
{
// 1. 判斷內容類型
if (!response.ContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase))
{
SaveBinaryFile(response, uri); // 圖片、PDF 等直接存檔
return null;
}
// 2. 文本流讀取(自動處理編碼)
using (var reader = new StreamReader(stream, Encoding.GetEncoding(response.CharacterSet)))
{
return reader.ReadToEnd();
}
}
}
private void SaveBinaryFile(HttpWebResponse response, Uri uri)
{
string localPath = MapUrlToLocalPath(uri); // 保持目錄結構
Directory.CreateDirectory(Path.GetDirectoryName(localPath));
using (var outStream = File.Create(localPath))
using (var inStream = response.GetResponseStream())
{
byte[] buffer = new byte[8192];
int read;
while ((read = inStream.Read(buffer, 0, buffer.Length)) > 0)
outStream.Write(buffer, 0, read);
}
}
public class DocumentWorker
{
private readonly SpiderController _controller;
public DocumentWorker(SpiderController controller) { _controller = controller; }
public void Run()
{
while (!_controller.IsStopping)
{
Uri uri = _controller.GetNextUrl(); // 阻塞直到有 URL
if (uri == null) continue;
_controller.WorkerBegin();
try
{
string html = DownloadPage(uri);
if (html != null) ProcessPage(html, uri);
}
finally { _controller.WorkerEnd(); }
}
}
}
核心難點:URL 隊列動態增減,線程間狀態同步。採用 Monitor 實現等待/通知:
public class CrawlMonitor
{
private int _activeWorkers = 0;
private bool _started = false;
private readonly object _lock = new object();
public void WorkerBegin()
{
lock (_lock)
{
_activeWorkers++;
_started = true;
Monitor.PulseAll(_lock);
}
}
public void WorkerEnd()
{
lock (_lock)
{
_activeWorkers--;
Monitor.PulseAll(_lock);
}
}
// 等待進入正式工作階段(避免啟動初期誤判為完成)
public void WaitBegin()
{
lock (_lock)
{
while (!_started) Monitor.Wait(_lock);
}
}
// 等待所有工作真正完成
public void WaitDone()
{
lock (_lock)
{
while (_activeWorkers > 0) Monitor.Wait(_lock);
}
}
}
主程式流程:
var monitor = new CrawlMonitor();
var controller = new SpiderController(monitor, maxThreads: 8);
// 初始種子 URL
controller.EnqueueSeed("https://example.com");
// 啟動工作線程
for (int i=0; i<8; i++) new Thread(new DocumentWorker(controller).Run).Start();
monitor.WaitBegin(); // 等待第一個線程開始
monitor.WaitDone(); // 等待全部結束
Console.WriteLine("爬取完成!");
HttpClient 替代 HttpWebRequest(連接池、非同步支援)。HtmlAgilityPack 或 AngleSharp 替代手寫解析器。robots.txt 遵守、請求頻率限制、重試策略。BlockingCollection<T> 作為線程安全 URL 隊列。