为什么我的NodeJS脚本在fs.readFile和fs.appendFile处理大量文件时陷入了僵局。

我有一个需要打开大约120k HTML页面的文件夹(每个文件大约70kb),使用xPathparsing一些数据并将该数据附加到.csv文件。

以下是我的代码:

它应该从parseFolder读取文件列表,遍历每个文件名,用fs.readFile打开它,然后使用jsdom和xpathparsing数据,并使用fs.appendFile将其保存到csv文件中。

前100个文件似乎做得好,但之后会逐渐减慢,消耗内存和CPU,最终停摆。 我有16个内存的演出,当我的内存使用量达到7Gig时,似乎达到了一些限制。

我是新来的JS和节点,任何帮助指出我失踪将非常感激。

var fs = require('fs'); var jsdom = require('jsdom').jsdom; var xpath = require('xpath'); var S = require('string'); var os = require('os'); ParserRules = { saveFile: 'output.csv', parseFolder: '/a/folder/with/120k/HTML/files', fields: { "field1": "//div[@class='field1']/text()", } }; start(); function start() { console.log('Starting...'); fs.readdir(ParserRules.parseFolder, iterateFiles); } function iterateFiles(err, filesToParse) { for (var i = 0; i < filesToParse.length; i++) { file = ParserRules.parseFolder + '/' + filesToParse[i]; console.log('Beginning read of ' + file); fs.readFile(file, {encoding: 'utf8'}, parseFile); } } function parseFile(err, data) { if (err == null) { var jsdomDocument = jsdom(data); var document = jsdomDocument.parentWindow.document; getContent(document); } } function getContent(document) { fields = ParserRules.fields; var csvRow = []; for (var field in fields) { try { console.log('Looking for ' + field); var nodes = xpath.select(fields[field], document); for (var i = 0; i < nodes.length; i++) { csvRow.push(getValue(nodes[i])); } } catch (err) { console.log(err); } } saveToCsv(csvRow, ParserRules.saveFile); } function getValue(node) { if(node.nodeValue != null) { toReturn = node.nodeValue; } else { newNode = $(node); toReturn = newNode.html(); } return toReturn; } function saveToCsv(object, filePath) { console.log('Saving...'); if(object.length > 0) { console.log('Row Exists, Saving...'); toString = S(object).toCSV().s + os.EOL; fs.appendFile(filePath, toString, {encoding: 'utf8', flag: 'a'}, function(err){ if (err) { console.log('Write Error: ' + err); } else { console.log('Saved ' + object); } }); } } 

Node.jsasynchronous工作。

问题

所以你的代码结构的方式,这发生了:

  1. 函数iterateFiles在一行中发出120k个fs.readFile调用,这导致Node.js排队120k文件系统读取操作。

  2. 当读取操作完成时,Node.js将调用fs.readFile的120kcallbackfs.readFile并且每个fs.readFile都会发出fs.appendFile操作,这将导致Node.js对文件系统的写操作进行排队。

  3. 最终Node.js将调用传递给fs.appendFile的120kcallbackfs.appendFile 。 在完成这些写入操作之前,Node.js 必须挂在要写入的数据上。

解决scheme

对于这样的任务,我build议使用fs调用的同步版本: fs.readFileSyncfs.appendFileSync

在为Web服务器编写代码或者以某种方式事件驱动时,您不希望使用这些调用的同步版本,因为它们会导致应用程序阻塞。 但是,如果您正在编写正在对数据进行批处理的代码(例如,像shell脚本那样运行的代码),则使用这些调用的同步版本会更简单。

插图

以下代码是您的代码的简化模型,并说明问题。 它被设置为从/tmp读取,因为这是任何文件的源文件。 如果文件是空的,我也将其设置为避免比parseFile做更多的工作。

 var fs = require('fs'); var ParserRules = { saveFile: 'output.csv', parseFolder: '/tmp' }; start(); function start() { console.log('Starting...'); fs.readdir(ParserRules.parseFolder, iterateFiles); } function iterateFiles(err, filesToParse) { for (var i = 0; i < filesToParse.length; i++) { var file = ParserRules.parseFolder + '/' + filesToParse[i]; console.log('Beginning read of file number ' + i); fs.readFile(file, {encoding: 'utf8'}, parseFile); } } var parse_count = 0; function parseFile(err, data) { if (err) return; if (data.length) { console.log("Parse: " + parse_count++); getContent(data); } } function getContent(data) { saveToCsv(data, ParserRules.saveFile); } var save_count = 0; function saveToCsv(data, filePath) { fs.appendFile(filePath, data, {encoding: 'utf8', flag: 'a'}, function(err){ if (err) { console.log('Write Error: ' + err); } else { console.log('Saved: ' + save_count++); } }); } 

如果你运行这个代码,你会看到所有的Parse:消息都是连续的。 然后, 只有在输出所有Parse:消息之后 ,才会得到Saved:消息。 所以你会看到像这样的东西:

 Beginning read of file number N Beginning read of file number N+1 Parse: 0 Parse: 1 ... more parse messages ... Parse: 18 Parse: 19 Saved: 0 Saved: 1 ... more saved messages... Saved: 18 Saved: 19 

这告诉你的是,只有在parsing完所有文件之后,节点才会开始保存。 由于Node不能释放与一个文件相关的数据,直到它知道它不会被再次使用—在这种情况下,这意味着直到文件被保存—那么在某个点上,Node将至less需要12万* 70kb的内存来保存来自所有文件的所有数据。