2011-04-19 27 views
10

这是一个简单的用JavaScript编写的带有Node.js的刮板,用于抓取维基百科中的元素周期表元素数据。 DOM操作的依赖关系为jsdom,排队依赖关系为chain-gangNode.js scraper中的内存泄漏

它工作正常,大部分时间(它不能很好地处理错误),代码也不错,我敢说为尝试,但有一个严重的错误 - 它泄漏内存可怕,每个元素的计算机内存的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(); 

回答

11

对于类似于jQuery的html处理,我现在使用节点cheerio而不是jsdom。到目前为止,我还没有看到任何内存泄漏,同时在10K页面上解析和解析几个小时。

+0

这是一个很好的提示。我有一些代码试图解析一些大约25兆字节的html,并且jsdom在LONG延迟之后崩溃,并出现内存不足错误。重写代码以便在7秒钟内完成代码,而不会出现任何错误。 – 2012-03-18 21:37:30

+2

在当前阶段,cheerio与jQuery非常不同,在选择器和DOM包装器中缺少很多功能。这肯定更快,但不兼容,如果你需要jQuery的熟悉和表现力,这是一个问题。 – sapht 2012-04-30 20:58:37

0

我知道它没有太多的答案,但我有类似的问题。我有多个刮刀同时运行,内存泄漏。

我已经结束了使用节点的jquery代替JSDOM

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

+0

不,我实际上用jsdom替换了'node-jquery' - 两者都以令人惊讶的方式泄露了内存 – 2011-04-19 15:04:54

+0

node-jquery依赖于JSDOM,这可能是为什么。 – Dve 2011-04-19 15:08:12

+0

另一个选择也许使用https://github.com/tautologistics/node-htmlparser – Dve 2011-04-19 15:09:12

24

jsdom确实有内存泄漏,从在复制茎和复制出逻辑后面节点的vm.runInContext()。一直在努力用C++来解决这个问题,我们希望在尝试将它推入节点之前证明解决方案。

目前的解决方法是为每个dom生成一个子进程,并在完成后关闭它。

编辑:

为jsdom 0.2.3这个问题的,只要你关闭窗口(window.close()),当你用它做是固定的。

+1

window.close()是完全正确的,确保您完成后即可关闭窗口,GC将按预期工作。 :) – 2012-11-01 14:05:38

+1

如果我可以的话:你应该在github上的头版自述文件中包含它。我认为它对用户来说很重要,因为它知道窗口对象需要清理!也许它的“显而易见”,但对我来说不是;)。 – 2013-08-15 19:54:42

4

我想我有一个更好的解决方法,通过设置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'); }); 
+0

谢谢你。这真的救了我的一天 – yas4891 2013-04-23 20:45:31

+0

这并没有真正为我工作,继承人我的代码片段工作:var jsdom = require('jsdom'); \t var win = jsdom.jsdom()。CreateWindow的(); \t变种useJQuery =函数(HTML,fnCallback){ \t \t jsdom.jQueryify(取胜 “http://code.jquery.com/jquery.js”,函数(){ \t \t \t win.document。 innerHtml = html; \t \t \t fnCallback(win); \t \t}); }; – kimar 2013-06-15 06:19:17