使用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

