You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用HtmlAgilityPack的C# .NET控制台爬虫循环运行疑似内存泄漏,问题在哪?

问题

我运行这段基于HtmlAgilityPack的C#控制台应用代码时,内存占用随时间持续增长,疑似内存泄漏。尝试了强制垃圾回收、复用对象、置空对象及清理数据等操作后,仍无法定位问题。这段代码循环运行10分钟后就彻底卡顿,而运行环境是128GB内存的系统。

请问我的代码存在什么缺陷?还是HtmlAgilityPack本身的问题?

代码如下:

using HtmlAgilityPack;

internal class Program
{
    private static int setNum = 16020;
    static HtmlDocument doc = null;
    static HtmlWeb web = new HtmlWeb();

    static void Main(string[] args)
    {
        ScrapeTypeYearName(setNum);

        Console.WriteLine("Press Enter to exit...");
        Console.ReadLine(); // Wait for the user to press Enter
    }

    public static void ScrapeTypeYearName(int setNum)
    {
        string setUrl = "https://www.tcdb.com/Checklist.cfm/sid/" + setNum;

        doc = web.Load(setUrl);

        // Process breadcrumb information
        string category = "";
        string year = "";
        string company = "";
        string setName = "";
        var breadcrumb = doc.DocumentNode.SelectSingleNode("//div[@class='d-none d-md-block']/nav[@aria-label='breadcrumb']");
        if (breadcrumb != null)
        {
            var breadcrumbItems = breadcrumb.SelectNodes("./ol/li/a");
            if (breadcrumbItems != null && breadcrumbItems.Count >= 4)
            {
                category = breadcrumbItems[1].InnerText.Trim();
                year = breadcrumbItems[3].InnerText.Trim();
                Console.WriteLine("Category: " + category);
                Console.WriteLine("Year: " + year);

                var companyNode = breadcrumb.SelectSingleNode("following::h1[@class='site']");
                var setNode = breadcrumb.SelectSingleNode("following::h3[@class='site']");
                if (companyNode != null)
                {
                    company = companyNode.InnerText.Trim();
                    company = company.Replace(year, "").Trim();
                    Console.WriteLine("Company: " + company);
                }

                if (setNode != null)
                {
                    setName = setNode.InnerText.Trim();
                    Console.WriteLine("Set: " + setName);
                }
            }
        } 

        // Do stuff here with the 

        // Clear the main document to free up memory
        doc?.DocumentNode.RemoveAll();

        setNum += 1;
        if (setNum < 99999)
        {
            Console.WriteLine($"");
            Console.WriteLine($"On set #: {setNum}");
            // Force garbage collection before the next recursive call
            GC.Collect();
            GC.WaitForPendingFinalizers();
            ScrapeTypeYearName(setNum);
        }
    }
}
分析与解决

你的代码核心问题不是HtmlAgilityPack的问题,而是递归调用导致的内存堆积和栈资源无法释放,再加上对象复用方式不当,具体问题和修复方案如下:

1. 递归调用引发的内存暴增

你用ScrapeTypeYearName自身递归的方式实现循环爬取,每一次递归都会在调用栈中保留当前方法的上下文(包括参数、局部变量等)。这些栈帧不会被释放,直到整个递归链条结束——但你的递归要跑到setNum >=99999才会停止,期间会生成数万层栈帧,直接导致内存占用飙升,最终引发卡顿甚至栈溢出。

修复:把递归改成迭代循环,用while循环替代递归,这样每次循环结束后,当前方法的上下文能被正常回收,不会堆积栈资源。

2. 静态HtmlDocument的复用问题

你将doc声明为静态变量,每次赋值doc = web.Load(setUrl)时,旧的HtmlDocument实例虽然被覆盖,但静态变量的生命周期和程序一致,可能会延迟GC对旧实例的回收;另外HtmlAgilityPack内部的节点缓存等资源也可能因静态引用无法及时释放。

修复:把doc改为方法内的局部变量,并使用using语句(新版本HtmlAgilityPack的HtmlDocument实现了IDisposable),确保使用完后自动释放资源。

3. 手动GC操作无效且有害

你手动调用GC.Collect()和GC.WaitForPendingFinalizers()不仅无法解决递归栈帧的问题,反而会干扰GC的正常回收策略,增加系统开销,拖慢程序运行。

修复:删除手动触发GC的代码,让GC根据自身策略自动处理内存回收。

4. 额外优化建议

  • 增加请求延迟:避免频繁请求目标网站被封禁,同时缓解程序的内存和CPU压力;
  • 简化字符串处理:合并冗余的字符串操作,减少临时字符串对象的生成。

修改后的代码

using HtmlAgilityPack;
using System.Threading;

internal class Program
{
    private static int setNum = 16020;
    static readonly HtmlWeb web = new HtmlWeb();

    static void Main(string[] args)
    {
        ScrapeTypeYearName();

        Console.WriteLine("Press Enter to exit...");
        Console.ReadLine();
    }

    public static void ScrapeTypeYearName()
    {
        while (setNum < 99999)
        {
            string setUrl = $"https://www.tcdb.com/Checklist.cfm/sid/{setNum}";
            
            // 使用局部变量+using语句,自动释放资源
            using (var doc = web.Load(setUrl))
            {
                string category = "";
                string year = "";
                string company = "";
                string setName = "";
                var breadcrumb = doc.DocumentNode.SelectSingleNode("//div[@class='d-none d-md-block']/nav[@aria-label='breadcrumb']");
                
                if (breadcrumb != null)
                {
                    var breadcrumbItems = breadcrumb.SelectNodes("./ol/li/a");
                    if (breadcrumbItems != null && breadcrumbItems.Count >= 4)
                    {
                        category = breadcrumbItems[1].InnerText.Trim();
                        year = breadcrumbItems[3].InnerText.Trim();
                        Console.WriteLine($"Category: {category}");
                        Console.WriteLine($"Year: {year}");

                        var companyNode = breadcrumb.SelectSingleNode("following::h1[@class='site']");
                        var setNode = breadcrumb.SelectSingleNode("following::h3[@class='site']");
                        
                        if (companyNode != null)
                        {
                            company = companyNode.InnerText.Trim().Replace(year, "").Trim();
                            Console.WriteLine($"Company: {company}");
                        }

                        if (setNode != null)
                        {
                            setName = setNode.InnerText.Trim();
                            Console.WriteLine($"Set: {setName}");
                        }
                    }
                }
            } // using结束自动调用doc.Dispose(),释放资源

            setNum++;
            Console.WriteLine($"\nOn set #: {setNum}");
            
            // 增加500ms延迟,避免被网站封禁,同时缓解资源压力
            Thread.Sleep(500);
        }
    }
}

内容的提问来源于stack exchange,提问作者Ethan Allen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 18:22:04