Analizar archivos JSON grandes en Nodejs


Tengo un archivo que almacena muchos objetos JavaScript en forma JSON y necesito leer el archivo, crear cada uno de los objetos y hacer algo con ellos (insertarlos en una base de datos en mi caso). Los objetos JavaScript se pueden representar en un formato:

Formato A:

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

O Formato B:

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

Tenga en cuenta que el ... indica una gran cantidad de objetos JSON. Soy consciente de que podría leer todo el archivo en la memoria y luego utilizar JSON.parse() like esto:

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

Sin embargo, el archivo podría ser realmente grande, preferiría usar una secuencia para lograr esto. El problema que veo con una secuencia es que el contenido del archivo podría dividirse en trozos de datos en cualquier momento, así que ¿cómo puedo usar JSON.parse() en tales objetos?

Idealmente, cada objeto se leería como un fragmento de datos separado, pero no estoy seguro de cómo hacerlo.

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!");
});*/

Tenga en cuenta que deseo evitar leer todo el archivo en la memoria. La eficiencia del tiempo no importa a mí. Sí, podría intentar leer un número de objetos a la vez e insertarlos todos a la vez, pero eso es un ajuste de rendimiento: necesito una forma que esté garantizada para no causar una sobrecarga de memoria, sin importar cuántos objetos estén contenidos en el archivo.

Puedo elegir usar FormatA o FormatB o tal vez algo más, solo especifique en su respuesta. ¡Gracias!

Author: Amol M Kulkarni, 2012-08-09

9 answers

Para procesar un archivo línea por línea, simplemente necesita desacoplar la lectura del archivo y el código que actúa sobre esa entrada. Usted puede lograr esto mediante el almacenamiento en búfer de su entrada hasta que llegue a una nueva línea. Suponiendo que tenemos un objeto JSON por línea (básicamente, formato 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!
    }
}

Cada vez que el flujo de archivos recibe datos del sistema de archivos, se almacena en un búfer, y luego se llama a pump.

Si no hay una nueva línea en el búfer, pump simplemente devuelve sin hacer nada. Se agregarán más datos (y potencialmente una nueva línea) al búfer la próxima vez que el flujo obtenga datos, y luego tendremos un objeto completo.

Si hay una nueva línea, pump corta el búfer desde el principio hasta la nueva línea y lo entrega a process. Luego comprueba de nuevo si hay otra nueva línea en el búfer (el bucle while). De esta manera, podemos procesar todas las líneas que se leyeron en el fragmento actual.

Finalmente, process se llama una vez por entrada alinear. Si está presente, quita el carácter de retorno de carro (para evitar problemas con los finales de línea – LF vs CRLF), y luego llama a JSON.parse una línea. En este punto, puedes hacer lo que necesites con tu objeto.

Tenga en cuenta que JSON.parsees estricto sobre lo que acepta como entrada; debe citar sus identificadores y valores de cadena con comillas dobles. En otras palabras, {name:'thing1'} lanzará un error; debes usar {"name":"thing1"}.

Porque no habrá más de un trozo de datos memoria a la vez, esto será extremadamente eficiente en memoria. También será extremadamente rápido. Una prueba rápida mostró que procesé 10,000 filas en menos de 15 ms.

 61
Author: josh3736,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-27 17:58:17

Justo cuando estaba pensando que sería divertido escribir un analizador JSON de streaming, también pensé que tal vez debería hacer una búsqueda rápida para ver si ya hay uno disponible.

Resulta que sí.

Desde que lo encontré, obviamente no lo he usado, así que no puedo comentar sobre su calidad, pero me interesará saber si funciona.

Funciona considere lo siguiente CoffeeScript:

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

Esto registrará los objetos a medida que entran si la secuencia es una matriz de objetos. Por lo tanto, lo único que se almacena en búfer es un objeto a la vez.

 29
Author: 3 revs, 3 users 46%user1106925,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-27 17:37:23

A partir de octubre de 2014, puedes hacer algo como lo siguiente (usando 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
     });

Para demostrar con un ejemplo de trabajo:

npm install JSONStream event-stream

Data.json:

{
  "greeting": "hello world"
}

Hola.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
 21
Author: arcseldon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-06 23:35:16

Me doy cuenta de que desea evitar leer todo el archivo JSON en la memoria si es posible, sin embargo, si tiene la memoria disponible, puede que no sea una mala idea en cuanto al rendimiento. Usando node.require () de js en un archivo json carga los datos en la memoria muy rápido.

Ejecuté dos pruebas para ver cómo se veía el rendimiento al imprimir un atributo de cada característica de un archivo geojson de 81 MB.

En la primera prueba, leí todo el archivo geojson en la memoria usando var data = require('./geo.json'). Que tomó 3330 milisegundos y luego imprimir un atributo de cada entidad tomó 804 milisegundos para un total general de 4134 milisegundos. Sin embargo, apareció ese nodo.js estaba usando 411MB de memoria.

En la segunda prueba, usé la respuesta de @arcseldon con JSONStream + event-stream. Modifiqué la consulta JSONPath para seleccionar solo lo que necesitaba. Esta vez la memoria nunca fue superior a 82MB, sin embargo, todo el asunto ahora tomó 70 segundos para completar!

 10
Author: Evan Siroky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-13 07:06:54

Tenía un requisito similar, necesito leer un archivo json grande en el nodo js y procesar datos en trozos y llamar a una api y guardarlos en mongodb. inputFile.json es como:

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

Ahora utilicé JsonStream y EventStream para lograr esto sincrónicamente.

 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();
    });
}
 7
Author: karthick N,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-21 12:08:51

Resolví este problema usando el módulo split npm. Pipe su corriente en split, y se "Romper un flujo y volver a montarlo de modo que cada línea es un trozo".

Código de ejemplo:

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);           
    // ...
});
 3
Author: Brian Leathem,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-09 16:03:55

Si tiene control sobre el archivo de entrada, y es una matriz de objetos, puede resolver esto más fácilmente. Organice la salida del archivo con cada registro en una línea, así:

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

Esto sigue siendo JSON válido.

Entonces, use el nodo.módulo js readline para procesarlos una línea a la vez.

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
Author: Steve Hanov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-06-02 15:46:52

Escribí un módulo que puede hacer esto, llamado BFJ. Específicamente, el método bfj.match se puede usar para dividir un flujo grande en trozos discretos 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
  });

Aquí, bfj.match devuelve un flujo legible en modo objeto que recibirá los elementos de datos analizados, y se le pasan 3 argumentos:

  1. Un flujo legible que contiene el JSON de entrada.

  2. Predicado que indica qué elementos del JSON analizado se enviarán al resultado flujo.

  3. Un objeto options que indica que la entrada es JSON delimitada por una nueva línea (esto es para procesar el formato B desde la pregunta, no es necesario para el formato A).

Al ser llamado, bfj.match analizará JSON desde la profundidad del flujo de entrada, llamando al predicado con cada valor para determinar si debe o no enviar ese elemento al flujo de resultados. Al predicado se le pasan tres argumentos:

  1. La clave de propiedad o índice de matriz (esto será undefined para los elementos de nivel superior).

  2. El valor en sí.

  3. La profundidad del elemento en la estructura JSON (cero para los elementos de nivel superior).

Por supuesto, un predicado más complejo también se puede usar según sea necesario de acuerdo con los requisitos. También puede pasar una cadena o una expresión regular en lugar de una función de predicado, si desea realizar coincidencias simples con claves de propiedad.

 2
Author: Phil Booth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-06 08:01:52

Creo que necesitas usar una base de datos. MongoDB es una buena opción en este caso porque es compatible con JSON.

ACTUALIZACIÓN: Puede usar la herramienta mongoimport para importar datos JSON a MongoDB.

mongoimport --collection collection --file collection.json
 1
Author: Vadim Baryshev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-08-08 23:39:06