【问题标题】:C# Massive memory leak in GeckoFX with HtmlAgilityPack使用 HtmlAgilityPack 在 GeckoFX 中出现 C# 大量内存泄漏
【发布时间】:2019-05-01 00:12:14
【问题描述】:

我正在编写一个程序来抓取许多公司的网站(最多 100,000 个),以获取最新的联系信息以及有关他们在 C# 中的运营领域的一些信息。由于大多数网站无法在常规 .NET 网络浏览器中显示,因此我使用 geckofx 导航到这些网站并查找与我相关的内容,因此我使用 HtmlAgilityPack 选择节点。

过程总是一样的:如果我有一个公司的 URL,我会立即访问该网站,否则我会使用 bing 查找网址(Google 似乎不喜欢被自动使用)。在网站上,我寻找指向印记的链接和指向可能表明某些活动领域的页面的链接,我导航到这些链接并查找我事先指定的流行语。一切都在同步运行,我等待浏览器每次触发其DocumentCompleted 事件。

一个例子:

//I navigate to bing looking for my company's name and postal code
Variables.browser.Navigate("https://www.bing.com/search?q=" + c.Name.Replace(" ", "+") + "+" + c.Zip.Replace(" ", "+"));

//I wait for the browser to finish loading. The Navigating event sets BrowserIsReady to false and the DocumentCompleted event sets it to true
do
{
    f.Application.DoEvents();
} while (!Variables.BrowserIsReady);

HtmlDocument browserDoc = new HtmlDocument();
browserDoc.LoadHtml(Variables.browser.Document.Body.OuterHtml);

//I select the relevant node in the document
HtmlNode sidebarNode = browserDoc.DocumentNode.SelectSingleNode("//div[contains(concat(\" \", normalize-space(@class), \" \"), \" b_entityTP \")]");

if (sidebarNode != null)
{
    Variables.logger.Log("Found readable sidebar. Loading data...");
    string lookedUpName, lookedUpStreet, lookedUpCity, lookedUpZip, lookedUpPhone, lookedUpWebsite;

    HtmlNode infoNode = sidebarNode.SelectSingleNode("//div[contains(concat(\" \", normalize-space(@class), \" \"), \" b_subModule \")]");

    HtmlNode nameNode = infoNode.SelectSingleNode("//div[contains(concat(\" \", normalize-space(@class), \" \"), \" b_feedbackComponent \")]");
    if (nameNode != null)
    {
        string[] dataFacts = nameNode.GetAttributeValue("data-facts", "").Replace("{\"", "").Replace("\"}", "").Split(new string[] { "\",\"" }, StringSplitOptions.None);
        foreach (string dataFact in dataFacts)
        {
            //... abbreviated
        }
    }
    //And at the end of every call to a node object I set it back to null
    nameNode = null;
}

我的 geckofx 不允许将缓存写入内存或从网站加载图像,这是我使用设置的

GeckoPreferences.Default["browser.cache.memory.enabled"] = false;
GeckoPreferences.Default["permissions.default.image"] = 2;

在创建我的 GeckoWebBrowser 实例之前。

每次抓取网站后我都会调用

//CookieMan is used as a global variable so I don't have to recreate it every time.
private static nsICookieManager CookieMan;
//...
CookieMan = Xpcom.GetService<nsICookieManager>("@mozilla.org/cookiemanager;1");
CookieMan = Xpcom.QueryInterface<nsICookieManager>(CookieMan);
CookieMan.RemoveAll();
Gecko.Cache.ImageCache.ClearCache(true);
Gecko.Cache.ImageCache.ClearCache(false);
Xpcom.GetService<nsIMemory>("@mozilla.org/xpcom/memory-service;1").HeapMinimize(true);

删除 cookie、图像缓存(我什至不确定是否已创建)并最大限度地减少 Xulrunners 的内存使用。

尽管如此,在以每条记录大约 2-3 秒的运行时间和 200-300mb 的舒适内存使用情况良好地开始之后,每条记录都迅速增加到 16-17 秒,并且仅我的爬虫就使用了超过 2gb 的内存之后1 小时。

我尝试使用 GC.Collect(); 强制垃圾回收(我知道,你不应该这样做),甚至通过停止、处理和重新创建它来回收整个浏览器对象,以尝试摆脱内存中未使用的垃圾,但无济于事。我也试图关闭 Xulrunner 并重新启动它,但 Xpcom.Shutdown() 似乎停止了整个应用程序,所以我无法这样做。

在这一点上我几乎没有想法,非常感谢我尚未采取的方法的新提示。

【问题讨论】:

  • 这有帮助吗?也许内存查看器会帮助您诊断问题的根源。 docs.microsoft.com/en-us/visualstudio/debugger/…
  • 您是否考虑过在多次迭代后回收流程?您可以拥有一个流程控制器来控制整个操作,以及一个可回收的流程工作者。
  • @LarryTang 看来我的 Visual Studio 没有提到的窗口。诊断工具的内存使用情况向我显示了我的文本框中为撤消历史记录保留的大量内存,我已将其删除,但内存使用率仍然高得令人不安,并且应用程序在大约 400 之后仍然变得非常慢- 500 条记录。

标签: c# performance html-agility-pack geckofx


【解决方案1】:

您是否尝试过使用回收的 AppDomain?

AppDomain workerAppDomain = AppDomain.CreateDomain("WorkerAppDomain");
workerAppDomain.SetData("URL", "https://stackoverflow.com");
workerAppDomain.DoCallBack(() =>
{
    var url = (string)AppDomain.CurrentDomain.GetData("URL");
    Console.WriteLine($"Scraping {url}");
    var webClient = new WebClient();
    var content = webClient.DownloadString(url);
    AppDomain.CurrentDomain.SetData("OUTPUT", content.Length);
});
int contentLength = (int)workerAppDomain.GetData("OUTPUT");
AppDomain.Unload(workerAppDomain);
Console.WriteLine($"ContentLength: {contentLength:#,0}");

输出:

抓取https://stackoverflow.com
内容长度:262.013

您在主 AppDomain 和工作 AppDomain 之间传递的数据必须是可序列化的。


更新:不过,最干净的解决方案应该是使用单独的进程。这将保证泄漏可以被可靠地清理。

【讨论】:

  • 感谢您的意见!有没有办法实现这一点而不必重新设计我的整个应用程序?我正在使用一个单独的静态变量类来存储我的应用程序的几乎每个部分都必须使用的值,以及一个调试窗口,用于记录我的进度和我的主窗口上的进度条,这些似乎都无法访问来自 AppDomain。
  • @C.Leimbach AppDomains 旨在被隔离,因此损坏的状态不会传播到另一个。因此,每个 AppDomain 都有自己的一组静态变量。两个 AppDomain 之间的通信几乎与两个进程之间的通信一样困难。在深入研究如何围绕可卸载的 AppDomain 重新构建应用程序之前,我建议首先验证这种方法确实解决了内存泄漏问题。如果没有,那么您应该考虑让您的应用程序进行多处理。
  • 一个多处理的应用程序是我用它构建的,现在似乎可以工作。非常感谢您为我指明了正确的方向。我不知道 stackoverflow 在这方面是如何工作的。即使我最终没有使用 AppDomains 来解决问题,我是否应该接受您的正确答案?
  • @C.Leimbach 我更新了我的答案以包含对多处理作为解决方案的引用。 ?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-06-26
  • 1970-01-01
  • 2013-09-02
  • 2015-03-07
  • 2013-03-11
  • 2023-03-24
  • 2013-11-09
相关资源
最近更新 更多