web-dev-qa-db-fra.com

Écrire de gros fichiers avec Node.js

J'écris un fichier volumineux avec node.js en utilisant un flux enregistrable :

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

Je me demande si ce schéma est sûr sans utiliser drain event? Si ce n’est pas le cas (ce qui, selon moi, est le cas), quel est le modèle utilisé pour écrire des données volumineuses arbitraires dans un fichier?

21
nab

C'est comme ça que je l'ai finalement fait. L'idée est de créer un flux lisible implémentant ReadStream interface, puis d'utiliser la méthode pipe() pour diriger les données vers un flux inscriptible.

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

L'exemple de la classe MyReadStream peut être tiré de mongoose QueryStream .

17
nab

L'idée derrière drain est que vous l'utiliseriez pour tester ici:

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

que vous n'êtes pas. Il faudrait donc rechercher un nouveau résultat pour le rendre "réentrant".

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

Cependant, cela signifie-t-il que vous devez continuer à mettre en mémoire tampon getLines pendant que vous attendez?

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

[Edit] La version mise à jour de Node.js writable.write(...) API say:

[La] valeur de retour est strictement consultative. Vous POUVEZ continuer à écrire, même si cela retourne faux. Cependant, les écritures seront mises en mémoire tampon, il est donc préférable de ne pas le faire excessivement. Attendez plutôt l'événement de drain avant d'écrire plus de données.

[Original] Extrait de la stream.write(...) documentation (c'est moi qui souligne):

Renvoie true si la chaîne a été vidée dans la mémoire tampon du noyau. Renvoie false pour indiquer que la mémoire tampon du noyau est pleine et que les données seront envoyées ultérieurement.

J'interprète cela comme signifiant que la fonction "write" renvoie true si la chaîne donnée a été immédiatement écrite dans le tampon sous-jacent du système d'exploitation ou false si elle n'a pas encore été écrite mais sera écrit par la fonction write} (par exemple was vraisemblablement mis en mémoire tampon par WriteStream) afin que vous n'ayez pas à appeler "write" à nouveau.

2
maerics

J'ai trouvé que les flux étaient un moyen peu performant de gérer des fichiers volumineux - c'est parce que vous ne pouvez pas définir une taille de tampon d'entrée adéquate (au moins, je ne connais pas de bonne façon de le faire). C'est ce que je fais:

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

Plusieurs réponses suggérées à cette question ont complètement manqué le point sur les flux.

Ce module peut aider https://www.npmjs.org/package/JSONStream

Cependant, supposons la situation décrite et écrivons le code nous-mêmes. Vous lisez depuis MongoDB en tant que flux, avec ObjectMode = true par défaut.

Cela entraînera des problèmes si vous essayez de diffuser directement dans un fichier, par exemple une erreur du type "Invalid non-string/buffer chunk".

La solution à ce type de problème est très simple.

Il suffit de mettre une autre transformation entre lisible et inscriptible pour adapter l'objet lisible à une chaîne inscriptible de manière appropriée. 

Exemple de solution de code:

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

La façon la plus propre de gérer cela est de faire de votre générateur de ligne un flux lisible - appelons-le lineReader. Ensuite, ce qui suit gérera automatiquement les tampons et les drainera bien pour vous:

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

Si vous ne souhaitez pas créer de flux lisible, vous pouvez écouter la sortie de write pour connaître la saturation du tampon et répondre comme suit:

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.

Vous trouverez un exemple plus long de cette situation ici .

1
Tyler

Si vous ne disposez pas d'un flux d'entrée, vous ne pouvez pas facilement utiliser pipe . Aucune de ce qui précède n'a fonctionné pour moi, l'événement drain ne se déclenche pas. Résolu comme suit (basé sur la réponse de Tylers):

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();
0
anneb