为什么我的NodeJS脚本在处理大量文件的fs.readFile和fs.appendFile期间陷入困境。

时间:2014-02-05 17:16:10

标签: javascript node.js xpath

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

以下是我的代码:

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

它似乎对前100个左右的文件做得很好,但之后会逐渐变慢,消耗内存和CPU并最终停止。我有16演出的内存,当我的内存使用量达到约7gig时,似乎达到了一定的限制。

我是JS和Node的新手,任何帮助指出我失踪的内容都会非常感激。

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);
            }
        });
    }
}

1 个答案:

答案 0 :(得分:4)

Node.js异步工作。

问题

所以你的代码的结构方式就是这样:

  1. 函数iterateFiles连续发出120k fs.readFile次调用,导致Node.js排队120k文件系统读取操作。

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

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

  4. 解决方案

    对于这样的任务,我建议使用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
    

    这告诉你的是,在解析所有文件之前,Node不会开始保存。由于Node无法释放与文件关联的数据,因为它知道它不会再次使用 - 在这种情况下,它意味着直到文件被保存 - 然后在某些时候节点将至少需要120,000 * 70kb的内存来保存所有文件中的所有数据。