首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 構建高效網頁爬蟲:HTML 解析、多線程下載與完成度判斷全解析

     編輯:關於C#

爬蟲核心四大難題

  1. HTML 解析:從雜亂標籤中提取超連結與結構化數據。
  2. 頁面處理:區分文本/二進制、持久化存儲、編碼處理。
  3. 多線程協作:生產者-消費者模式、線程池管理、鎖競爭優化。
  4. 完成度判斷:動態 URL 隊列中,如何準確知道「沒有新 URL 且所有線程皆空閒」。

HTML 解析器設計

採用流式字符掃描,避免載入完整 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);
    }
}

多線程架構:DocumentWorker + SpiderController


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(); }
        }
    }
}

完成度判斷:Done 監視器模式

核心難點: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 隊列。
  • 考慮分散式架構:Redis 隊列 + 多節點 Worker + 去重 Bloom Filter。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved