使用 HtmlAgilityPack 的 GeckoFX 中的 C# 大量内存泄漏
C# Massive memory leak in GeckoFX with HtmlAgilityPack
我正在编写一个程序来抓取许多公司的网站(最多 100,000 个)以获取最新的联系信息以及有关他们在 C# 中的运营领域的一些信息。因为大多数网站无法在常规 .NET 网络浏览器中显示,所以我使用 geckofx 导航到这些网站并找到与我相关的内容,我 select 使用 HtmlAgilityPack 节点。
过程总是一样的:如果我有一个公司的 URL,我会立即访问该网站,否则我会使用 bing 来查找网址(Google 似乎不喜欢被自动使用)。在网站上,我寻找印记的 link 和可能指示 activity 某些区域的页面的 link,我导航到这些 link 并寻找标语我事先指定的。一切都是运行同步的,我每次都等待浏览器触发它的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()
似乎停止了整个应用程序,所以我无法做到这一点。
此时我几乎没有想法,非常感谢对我尚未采用的方法的新提示。
您尝试过使用回收的 AppDomain 吗?
AppDomain workerAppDomain = AppDomain.CreateDomain("WorkerAppDomain");
workerAppDomain.SetData("URL", "https://whosebug.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}");
输出:
Scraping https://whosebug.com
ContentLength: 262.013
您在主 AppDomain 和辅助 AppDomain 之间传递的数据必须是可序列化的。
更新: 最干净的解决方案应该是使用单独的进程。这样可以保证泄漏可以可靠地清理。
我正在编写一个程序来抓取许多公司的网站(最多 100,000 个)以获取最新的联系信息以及有关他们在 C# 中的运营领域的一些信息。因为大多数网站无法在常规 .NET 网络浏览器中显示,所以我使用 geckofx 导航到这些网站并找到与我相关的内容,我 select 使用 HtmlAgilityPack 节点。
过程总是一样的:如果我有一个公司的 URL,我会立即访问该网站,否则我会使用 bing 来查找网址(Google 似乎不喜欢被自动使用)。在网站上,我寻找印记的 link 和可能指示 activity 某些区域的页面的 link,我导航到这些 link 并寻找标语我事先指定的。一切都是运行同步的,我每次都等待浏览器触发它的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()
似乎停止了整个应用程序,所以我无法做到这一点。
此时我几乎没有想法,非常感谢对我尚未采用的方法的新提示。
您尝试过使用回收的 AppDomain 吗?
AppDomain workerAppDomain = AppDomain.CreateDomain("WorkerAppDomain");
workerAppDomain.SetData("URL", "https://whosebug.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}");
输出:
Scraping https://whosebug.com
ContentLength: 262.013
您在主 AppDomain 和辅助 AppDomain 之间传递的数据必须是可序列化的。
更新: 最干净的解决方案应该是使用单独的进程。这样可以保证泄漏可以可靠地清理。