web-dev-qa-db-ja.com

Node.jsで大きなファイルを書く

書き込み可能なストリーム を使用してnode.jsで大きなファイルを書いています。

var fs     = require('fs');
var stream = fs.createWriteStream('someFile.txt', { flags : 'w' });

var lines;
while (lines = getLines()) {
    for (var i = 0; i < lines.length; i++) {
        stream.write( lines[i] );
    }
}

drain eventを使用せずにこのスキームが安全かどうか疑問に思っていますか?そうでない場合(私はそう思う)、任意の大きなデータをファイルに書き込むためのパターンは何ですか?

24
nab

それが私が最終的にやった方法です。背後にある考え方は、 ReadStream interfaceを実装する読み取り可能なストリームを作成し、pipe()メソッドを使用してデータを書き込み可能なストリームにパイプすることです。

var fs = require('fs');
var writeStream = fs.createWriteStream('someFile.txt', { flags : 'w' });
var readStream = new MyReadStream();

readStream.pipe(writeStream);
writeStream.on('close', function () {
    console.log('All done!');
});

MyReadStreamクラスの例は、mongoose QueryStream から取得できます。

19
nab

ドレインの背後にある考え方は、ここでテストするために使用するということです。

var fs = require('fs');
var stream = fs.createWriteStream('someFile.txt', {flags: 'w'});

var lines;
while (lines = getLines()) {
    for (var i = 0; i < lines.length; i++) {
        stream.write(lines[i]); //<-- the place to test
    }
}

あなたはそうではありません。したがって、「再入可能」にするために再設計する必要があります。

var fs = require('fs');
var stream = fs.createWriteStream('someFile.txt', {flags: 'w'});

var lines;
while (lines = getLines()) {
    for (var i = 0; i < lines.length; i++) {
        var written = stream.write(lines[i]); //<-- the place to test
        if (!written){
           //do something here to wait till you can safely write again
           //this means prepare a buffer and wait till you can come back to finish
           //  lines[i] -> remainder
        }
    }
}

ただし、これは待機中にgetLinesもバッファリングし続ける必要があるということですか?

var fs = require('fs');
var stream = fs.createWriteStream('someFile.txt', {flags: 'w'});

var lines,
    buffer = {
     remainingLines = []
    };
while (lines = getLines()) {
    for (var i = 0; i < lines.length; i++) {
        var written = stream.write(lines[i]); //<-- the place to test
        if (!written){
           //do something here to wait till you can safely write again
           //this means prepare a buffer and wait till you can come back to finish
           //  lines[i] -> remainder
           buffer.remainingLines = lines.slice(i);
           break;
           //notice there's no way to re-run this once we leave here.
        }
    }
}

stream.on('drain',function(){
  if (buffer.remainingLines.length){
    for (var i = 0; i < buffer.remainingLines.length; i++) {
      var written = stream.write(buffer.remainingLines[i]); //<-- the place to test
      if (!written){
       //do something here to wait till you can safely write again
       //this means prepare a buffer and wait till you can come back to finish
       //  lines[i] -> remainder
       buffer.remainingLines = lines.slice(i);
      }
    }
  }
});
11
jcolebrand

これを処理する最もクリーンな方法は、ラインジェネレーターを 読み取り可能なストリーム にすることです。これをlineReaderと呼びましょう。次に、以下が自動的にバッファを処理し、適切にドレインします:

lineReader.pipe(fs.createWriteStream('someFile.txt'));

読み取り可能なストリームを作成したくない場合は、writeの出力をバッファー満杯でリッスンし、次のように応答できます。

var i = 0, n = lines.length;
function write () {
  if (i === n) return;  // A callback could go here to know when it's done.
  while (stream.write(lines[i++]) && i < n);
  stream.once('drain', write);
}
write();  // Initial call.

この状況のより長い例は here にあります。

3
Tyler

[編集]更新されたNode.js writable.write(...) AP​​Iドキュメント たとえば:

[The]戻り値は厳密に助言です。 falseを返したとしても、書き続けることができます。ただし、書き込みはメモリにバッファされるため、過度に行わないことをお勧めします。代わりに、ドレインイベントを待ってから、さらにデータを書き込みます。

[Original]stream.write(...) documentation (emphasis mine)から:

文字列がカーネルバッファーにフラッシュされている場合は、trueを返します。 falseを返し、カーネルバッファーがいっぱいであり、データが将来送信されることを示します

これは、指定された文字列が基になるOSバッファーにすぐに書き込まれた場合は「書き込み」関数がtrueを返し、まだ書き込まれていないが書き込み関数によってが書き込まれる場合はfalseを返すと解釈します(たとえば、WriteStreamによってバッファリングされたと思われます)、再度 "write"を呼び出す必要がないようにします。

2
maerics

大きなファイルを処理するには、ストリームのパフォーマンスが低いことがわかりました-これは、適切な入力バッファサイズを設定できないためです(少なくとも、それを行う良い方法がわかりません)。これが私がすることです:

var fs = require('fs');

var i = fs.openSync('input.txt', 'r');
var o = fs.openSync('output.txt', 'w');

var buf = new Buffer(1024 * 1024), len, prev = '';

while(len = fs.readSync(i, buf, 0, buf.length)) {

    var a = (prev + buf.toString('ascii', 0, len)).split('\n');
    prev = len === buf.length ? '\n' + a.splice(a.length - 1)[0] : '';

    var out = '';
    a.forEach(function(line) {

        if(!line)
            return;

        // do something with your line here

        out += line + '\n';
    });

    var bout = new Buffer(out, 'ascii');
    fs.writeSync(o, bout, 0, bout.length);
}

fs.closeSync(o);
fs.closeSync(i);
2
youurayy

この質問に対するいくつかの提案された答えは、ストリームに関するポイントを完全に見落としていました。

このモジュールは役に立ちます https://www.npmjs.org/package/JSONStream

ただし、説明したような状況を想定して、自分でコードを記述しましょう。 MongoDBからストリームとして読み取ります。デフォルトではObjectMode = trueです。

これは、ファイルに直接ストリーミングしようとすると問題が発生します-「Invalid non-string/buffer chunk」エラーなど。

このタイプの問題の解決策は非常に簡単です。

読み取り可能オブジェクトと書き込み可能オブジェクトの間に別の変換を配置して、読み取り可能なオブジェクトを適切に書き込み可能なストリングに適合させます。

サンプルコードソリューション:

var fs = require('fs'),
    writeStream = fs.createWriteStream('./out' + process.pid, {flags: 'w', encoding: 'utf-8' }),
    stream = require('stream'),
    stringifier = new stream.Transform();
stringifier._writableState.objectMode = true;
stringifier._transform = function (data, encoding, done) {
    this.Push(JSON.stringify(data));
    this.Push('\n');
    done();
}
rowFeedDao.getRowFeedsStream(merchantId, jobId)
.pipe(stringifier)
.pipe(writeStream).on('error', function (err) {
   // handle error condition
}
1
arcseldon

入力ストリームがない場合は、パイプを簡単に使用できません。上記のどれも私にとってはうまくいきませんでした。ドレインイベントは発生しません。次のように解決しました(タイラーズの回答に基づく):

var lines[]; // some very large array
var i = 0;

function write() {
    if (i < lines.length)  {
        wstream.write(lines[i]), function(err){
            if (err) {
                console.log(err);
            } else {
                i++;
                write();
            }
        });
    } else {
        wstream.end();
        console.log("done");
    }
};
write();
1
anneb