内存泄漏在Node.js刮刀

这是一个使用Node.js编写的JavaScript简单的刮板,用于抓取Wikipedia中的元素周期表数据。 依赖关系是用于DOM操作的jsdom和用于排队的chain-gang 。

它工作得很好,大部分时间(它不能很好地处理错误),代码也不错,我敢说是一个尝试,但是有一个严重的错误 – 它泄漏内存可怕,在任何地方从每个元素的计算机内存的0.3%到0.6%,这样当它领先的时候,它会在接近20%的地方使用,这显然是不可接受的。

我已经尝试使用分析器,但是我没有发现它们有帮助或难以解释数据。 我怀疑它与processElement传递的方式有关,但我很难将队列代码重写成更优雅的东西。

 var fs = require('fs'), path = require('path'), jsdom = require("jsdom"), parseUrl = require('url').parse, chainGang = require('chain-gang'); var chain = chainGang.create({ workers: 1 }); var Settings = { periodicUrl: 'http://en.wikipedia.org/wiki/Template:Periodic_table', periodicSelector: '#bodyContent > table:first', pathPrefix: 'data/', ignoredProperties: ['Pronunciation'] }; function writeToFile(output) { var keys = 0; // Huge nests for finding the name of the element... yeah for(var i in output) { if(typeof output[i] === 'object' && output[i] !== null){ for(var l in output[i]) { if(l.toLowerCase() === 'name') { var name = output[i][l]; } } keys += Object.keys(output[i]).length; } } console.log('Scraped ' + keys + ' properties for ' + name); console.log('Writing to ' + Settings.pathPrefix + name + '.json'); fs.writeFile(Settings.pathPrefix + name + '.json', JSON.stringify(output)); } // Generic create task function to create a task function that // would be passed to the chain gang function createTask (url, callback) { console.log('Task added - ' + url); return function(worker){ console.log('Requesting: ' +url); jsdom.env(url, [ 'jquery.min.js' // Local copy of jQuery ], function(errors, window) { if(errors){ console.log('Error! ' + errors) createTask(url, callback); } else { // Give me thy $ var $ = window.$; // Cleanup - remove unneeded elements $.fn.cleanup = function() { return this.each(function(){ $(this).find('sup.reference, .IPA').remove().end() .find('a, b, i, small, span').replaceWith(function(){ return this.innerHTML; }).end() .find('br').replaceWith(' '); }); } callback($); } worker.finish(); }); } } function processElement ($){ var infoBox = $('.infobox'), image = infoBox.find('tr:contains("Appearance") + tr img:first'), description = $('#toc').prevAll('p').cleanup(), headers = infoBox.find('tr:contains("properties")'), output = { Appearance: image.attr('src'), Description: $('.infobox + p').cleanup().html() }; headers.each(function(){ var that = this, title = this.textContent.trim(), rowspan = 0, rowspanHeading = ''; output[title] = {}; $(this).nextUntil('tr:has(th:only-child)').each(function(){ var t = $(this).cleanup(), headingEle = t.children('th'), data = t.children('td').html().trim(); if(headingEle.length) { var heading = headingEle.html().trim(); } // Skip to next heading if current property is ignored if(~Settings.ignoredProperties.indexOf(heading)) { return true; } if (rowspan) { output[title][rowspanHeading][data.split(':')[0].trim()] = data.split(':')[1].trim(); rowspan--; } else if (headingEle.attr('rowspan')){ rowspan = headingEle.attr('rowspan') - 1; rowspanHeading = heading; output[title][heading] = {}; output[title][heading][data.split(':')[0]] = data.split(':')[1]; } else if (~heading.indexOf(',')){ data = data.split(','); heading.split(',').forEach(function(v, i){ output[title][v.trim()] = data[i].trim(); }); } else { output[title][heading] = data; } }); }); writeToFile(output); } function fetchElements(elements) { elements.forEach(function(value){ // Element URL used here as task id (second argument) chain.add(createTask(value, processElement), value); }); } function processTable($){ var elementArray = $(Settings.periodicSelector).find('td').map(function(){ var t = $(this), atomicN = parseInt(t.text(), 10); if(atomicN && t.children('a').length) { var elementUrl = 'http://' + parseUrl(Settings.periodicUrl).host + t.children('a:first').attr('href'); console.log(atomicN, t.children('a:first').attr('href').split('/').pop(), elementUrl); return elementUrl; } }).get(); fetchElements(elementArray); fs.writeFile(Settings.pathPrefix + 'elements.json', JSON.stringify(elementArray)); } // Get table - init function getPeriodicList(){ var elementsList = Settings.pathPrefix + 'elements.json'; if(path.existsSync(elementsList)){ var fileData = JSON.parse(fs.readFileSync(elementsList, 'utf8')); fetchElements(fileData); } else { chain.add(createTask(Settings.periodicUrl, processTable)); } } getPeriodicList(); 

对于类似于jQuery的html处理,我现在使用cheerio代替jsdom。 到目前为止,我还没有看到任何内存泄漏,而在几十个小时的报废和parsing超过10K页。

jsdom确实存在内存泄漏,这源于节点vm.runInContext()后面的复制和复制逻辑。 一直在努力用c ++来解决这个问题,我们希望在尝试将它推入节点之前certificate这个解决scheme。

目前的解决方法是为每个dom生成一个subprocess,并在完成后closures它。

编辑:

从jsdom 0.2.3开始,只要closures窗口( window.close() ),就可以解决这个问题。

我想我有一个更好的解决方法,通过设置window.document.innerHTML属性重用您的jsdom的实例。 解决了我的内存泄漏问题!

  // jsdom has a memory leak when using multiple instance // cache a single instance and swap out innerHTML var dom = require('jsdom'); var win; var useJQuery = function(html, fnCallback) { if (!win) { var defEnv = { html:html, scripts:['jquery-1.5.min.js'], }; dom.env(defEnv, function (err, window) { if (err) throw new Error('failed to init dom'); win = window; fnCallback(window.jQuery); }); } else { win.document.innerHTML = html; fnCallback(win.jQuery); } }; .... // Use it! useJQuery(html, function($) { $('woohoo').val('test'); }); 

我知道它不是一个答案,但我有一个类似的问题。 我有多个刮板同时运行,内存泄漏。

我已经结束了使用node-jquery而不是JSDOM

https://github.com/coolaj86/node-jquery