web-dev-qa-db-fra.com

Analyser un fichier JSON volumineux dans Nodejs

J'ai un fichier qui stocke de nombreux objets JavaScript au format JSON et je dois le lire, créer chacun des objets et en faire quelque chose (les insérer dans une base de données dans mon cas). Les objets JavaScript peuvent être représentés sous un format:

Format A:

[{name: 'thing1'},
....
{name: 'thing999999999'}]

ou Format B:

{name: 'thing1'}         // <== My choice.
...
{name: 'thing999999999'}

Notez que ... indique un grand nombre d'objets JSON. Je sais que je pourrais lire le fichier entier en mémoire, puis utiliser JSON.parse() comme ceci:

fs.readFile(filePath, 'utf-8', function (err, fileContents) {
  if (err) throw err;
  console.log(JSON.parse(fileContents));
});

Cependant, le fichier peut être très volumineux, je préférerais utiliser un flux pour y parvenir. Le problème que je vois avec un flux, c'est que le contenu du fichier peut être divisé en blocs de données à tout moment. Alors, comment utiliser JSON.parse() sur de tels objets? 

Idéalement, chaque objet devrait être lu comme un bloc de données séparé, mais je ne suis pas sûr sur comment faire cela.

var importStream = fs.createReadStream(filePath, {flags: 'r', encoding: 'utf-8'});
importStream.on('data', function(chunk) {

    var pleaseBeAJSObject = JSON.parse(chunk);           
    // insert pleaseBeAJSObject in a database
});
importStream.on('end', function(item) {
   console.log("Woot, imported objects into the database!");
});*/

Notez que je souhaite empêcher la lecture de la totalité du fichier en mémoire. L'efficacité du temps n'a pas d'importance pour moi. Oui, je pourrais essayer de lire plusieurs objets à la fois et de les insérer tous en même temps, mais c'est une performance Tweak - j'ai besoin d'une méthode qui garantisse de ne pas causer de surcharge de mémoire, quel que soit le nombre d'objets contenus dans le fichier . 

Je peux choisir d'utiliser FormatA ou FormatB ou peut-être autre chose, précisez-le dans votre réponse. Merci!

78
dgh

Pour traiter un fichier ligne par ligne, il vous suffit de découpler la lecture du fichier et du code qui agit sur cette entrée. Vous pouvez accomplir cela en tamponnant votre entrée jusqu'à ce que vous frappiez une nouvelle ligne. En supposant que nous ayons un objet JSON par ligne (en gros, format B):

var stream = fs.createReadStream(filePath, {flags: 'r', encoding: 'utf-8'});
var buf = '';

stream.on('data', function(d) {
    buf += d.toString(); // when data is read, stash it in a string buffer
    pump(); // then process the buffer
});

function pump() {
    var pos;

    while ((pos = buf.indexOf('\n')) >= 0) { // keep going while there's a newline somewhere in the buffer
        if (pos == 0) { // if there's more than one newline in a row, the buffer will now start with a newline
            buf = buf.slice(1); // discard it
            continue; // so that the next iteration will start with data
        }
        processLine(buf.slice(0,pos)); // hand off the line
        buf = buf.slice(pos+1); // and slice the processed data off the buffer
    }
}

function processLine(line) { // here's where we do something with a line

    if (line[line.length-1] == '\r') line=line.substr(0,line.length-1); // discard CR (0x0D)

    if (line.length > 0) { // ignore empty lines
        var obj = JSON.parse(line); // parse the JSON
        console.log(obj); // do something with the data here!
    }
}

Chaque fois que le flux de fichiers reçoit des données du système de fichiers, celles-ci sont stockées dans un tampon, puis pump est appelé.

S'il n'y a pas de nouvelle ligne dans la mémoire tampon, pump revient simplement sans rien faire. Plus de données (et potentiellement une nouvelle ligne) seront ajoutées à la mémoire tampon la prochaine fois que le flux obtiendra des données, puis nous aurons un objet complet.

S'il y a une nouvelle ligne, pump coupe le tampon du début à la nouvelle ligne et le transfère à process. Il vérifie ensuite s'il existe une autre nouvelle ligne dans le tampon (la boucle while). De cette manière, nous pouvons traiter toutes les lignes lues dans le bloc actuel.

Finalement, process est appelé une fois par ligne d’entrée. S'il est présent, il supprime le caractère de retour chariot (pour éviter les problèmes de fin de ligne - LF vs CRLF), puis appelle JSON.parse sur la ligne. À ce stade, vous pouvez faire ce que vous voulez avec votre objet.

Notez que JSON.parse est strict sur ce qu’il accepte en entrée; vous devez indiquer vos identifiants et valeurs de chaîne avec des guillemets doubles . En d'autres termes, {name:'thing1'} lève une erreur; vous devez utiliser {"name":"thing1"}.

Parce qu’un maximum de données ne sera jamais en mémoire à la fois, cela sera extrêmement efficace en termes de mémoire. Ce sera aussi extrêmement rapide. Un test rapide a montré que j'avais traité 10 000 lignes en moins de 15 ms.

67
josh3736

Alors que je pensais qu'il serait amusant d'écrire un analyseur JSON en streaming, j'ai également pensé que je devrais peut-être faire une recherche rapide pour voir s'il en existe déjà un.

Il s'avère que c'est.

Depuis que je viens de le trouver, je ne l'ai évidemment pas utilisé. Je ne peux donc pas en dire plus sur sa qualité, mais je serais intéressé de savoir si cela fonctionne.

Cela fonctionne considérer le CoffeeScript suivant:

stream.pipe(JSONStream.parse('*'))
.on 'data', (d) ->
    console.log typeof d
    console.log "isString: #{_.isString d}"

Cela enregistrera les objets au fur et à mesure qu'ils arriveront si le flux est un tableau d'objets. Par conséquent, la seule chose mise en mémoire tampon est un objet à la fois.

29
user1106925

À compter d’octobre 2014, vous pouvez utiliser les méthodes suivantes (avec JSONStream) - https://www.npmjs.org/package/JSONStream

 var fs = require('fs'),
         JSONStream = require('JSONStream'),

    var getStream() = function () {
        var jsonData = 'myData.json',
            stream = fs.createReadStream(jsonData, {encoding: 'utf8'}),
            parser = JSONStream.parse('*');
            return stream.pipe(parser);
     }

     getStream().pipe(MyTransformToDoWhateverProcessingAsNeeded).on('error', function (err){
        // handle any errors
     });

Pour démontrer avec un exemple de travail:

npm install JSONStream event-stream

data.json:

{
  "greeting": "hello world"
}

hello.js:

var fs = require('fs'),
  JSONStream = require('JSONStream'),
  es = require('event-stream');

var getStream = function () {
    var jsonData = 'data.json',
        stream = fs.createReadStream(jsonData, {encoding: 'utf8'}),
        parser = JSONStream.parse('*');
        return stream.pipe(parser);
};

 getStream()
  .pipe(es.mapSync(function (data) {
    console.log(data);
  }));


$ node hello.js
// hello world
23
arcseldon

Je me rends compte que vous voulez éviter de lire le fichier JSON entier en mémoire si possible, mais si vous avez de la mémoire disponible, les performances ne seront peut-être pas une mauvaise idée. L'utilisation de node.js require () sur un fichier json charge les données en mémoire très rapidement. 

J'ai exécuté deux tests pour voir à quoi ressemblait la performance d'impression d'un attribut à partir de chaque fonctionnalité à partir d'un fichier Geojson de 81 Mo. 

Lors du premier test, j'ai lu l'intégralité du fichier geojson en mémoire avec var data = require('./geo.json'). Cela a pris 3330 millisecondes, puis l'impression d'un attribut de chaque fonctionnalité a pris 804 millisecondes pour un total général de 4134 millisecondes. Cependant, il est apparu que node.js utilisait 411 Mo de mémoire.

Dans le deuxième test, j'ai utilisé la réponse de @ arcseldon avec JSONStream + event-stream. J'ai modifié la requête JSONPath pour ne sélectionner que ce dont j'avais besoin. Cette fois, la mémoire n’a jamais dépassé 82 Mo. Cependant, l’ensemble a pris 70 secondes! 

11
Evan Siroky

J'avais des exigences similaires, j'ai besoin de lire un fichier JSON volumineux dans le nœud JS et de traiter les données en morceaux, puis d'appeler une API et de l'enregistrer dans mongodb .

{
 "customers":[
       { /*customer data*/},
       { /*customer data*/},
       { /*customer data*/}....
      ]
}

Maintenant, j'ai utilisé JsonStream et EventStream pour y parvenir de manière synchrone.

var JSONStream = require("JSONStream");
var es = require("event-stream");

fileStream = fs.createReadStream(filePath, { encoding: "utf8" });
fileStream.pipe(JSONStream.parse("customers.*")).pipe(
  es.through(function(data) {
    console.log("printing one customer object read from file ::");
    console.log(data);
    this.pause();
    processOneCustomer(data, this);
    return data;
  }),
  function end() {
    console.log("stream reading ended");
    this.emit("end");
  }
);

function processOneCustomer(data, es) {
  DataModel.save(function(err, dataModel) {
    es.resume();
  });
}
10
karthick N

J'ai résolu ce problème en utilisant le module split npm . Découpez le flux dans votre flux et vous obtiendrez "Découpez un flux et réassemblez-le afin que chaque ligne soit un bloc} _".

Exemple de code:

var fs = require('fs')
  , split = require('split')
  ;

var stream = fs.createReadStream(filePath, {flags: 'r', encoding: 'utf-8'});
var lineStream = stream.pipe(split());
linestream.on('data', function(chunk) {
    var json = JSON.parse(chunk);           
    // ...
});
4
Brian Leathem

J'ai écrit un module permettant de faire cela, appelé BFJ . Plus précisément, la méthode bfj.match peut être utilisée pour diviser un flux important en morceaux distincts de JSON:

const bfj = require('bfj');
const fs = require('fs');

const stream = fs.createReadStream(filePath);

bfj.match(stream, (key, value, depth) => depth === 0, { ndjson: true })
  .on('data', object => {
    // do whatever you need to do with object
  })
  .on('dataError', error => {
    // a syntax error was found in the JSON
  })
  .on('error', error => {
    // some kind of operational error occurred
  })
  .on('end', error => {
    // finished processing the stream
  });

Ici, bfj.match renvoie un flux lisible en mode objet qui recevra les éléments de données analysés. Trois arguments sont passés:

  1. Un flux lisible contenant le JSON d'entrée.

  2. Un prédicat qui indique quels éléments du JSON analysé seront placés dans le flux de résultats.

  3. Un objet options indiquant que l'entrée est un fichier JSON délimité par une nouvelle ligne (il s'agit de traiter le format B de la question, il n'est pas requis pour le format A).

Lors de l'appel, bfj.match analysera JSON du flux d'entrée en profondeur en premier, appelant le prédicat avec chaque valeur pour déterminer s'il convient ou non de pousser cet élément dans le flux de résultat. Le prédicat reçoit trois arguments:

  1. La clé de propriété ou l'index de tableau (il s'agira de undefined pour les éléments de niveau supérieur).

  2. La valeur elle-même.

  3. La profondeur de l'élément dans la structure JSON (zéro pour les éléments de niveau supérieur).

Bien entendu, un prédicat plus complexe peut également être utilisé selon les besoins. Vous pouvez également transmettre une chaîne ou une expression régulière à la place d'une fonction de prédicat si vous souhaitez effectuer des correspondances simples avec des clés de propriété.

3
Phil Booth

Si vous avez le contrôle sur le fichier d'entrée et qu'il s'agit d'un tableau d'objets, vous pouvez résoudre ce problème plus facilement. Organisez la sortie du fichier avec chaque enregistrement sur une ligne, comme ceci:

[
   {"key": value},
   {"key": value},
   ...

Ceci est toujours valide JSON.

Ensuite, utilisez le module readline node.js pour les traiter ligne par ligne.

var fs = require("fs");

var lineReader = require('readline').createInterface({
    input: fs.createReadStream("input.txt")
});

lineReader.on('line', function (line) {
    line = line.trim();

    if (line.charAt(line.length-1) === ',') {
        line = line.substr(0, line.length-1);
    }

    if (line.charAt(0) === '{') {
        processRecord(JSON.parse(line));
    }
});

function processRecord(record) {
    // Process the records one at a time here! 
}
2
Steve Hanov

Je pense que vous devez utiliser une base de données. MongoDB est un bon choix dans ce cas car il est compatible JSON.

UPDATE: Vous pouvez utiliser mongoimport tool pour importer des données JSON dans MongoDB.

mongoimport --collection collection --file collection.json
0
Vadim Baryshev