2012-08-08 52 views
63

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

Formato A:

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

o Formato B:

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

Tenga en cuenta que la ... indica una gran cantidad de objetos JSON. Soy consciente de que podía leer el archivo en la memoria y luego usar JSON.parse() así:

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

Sin embargo, el archivo puede ser muy grande, yo prefiero usar una corriente de lograr esto. El problema que veo con una transmisión es que el contenido del archivo se puede dividir en fragmentos de datos en cualquier punto, entonces, ¿cómo puedo usar JSON.parse() en dichos objetos?

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

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

Nota, deseo evitar la lectura de todo el archivo en la memoria. La eficiencia del tiempo no me importa. Sí, podría intentar leer una serie de objetos a la vez e insertarlos todos a la vez, pero eso es un ajuste de rendimiento: necesito una forma que garantice que no se produzca una sobrecarga de memoria, sin importar cuántos objetos contenga el archivo .

Puedo elegir usar FormatA o FormatB o tal vez alguna otra cosa, solo por favor especifique en su respuesta. ¡Gracias!

+0

Para el formato B, puede analizar el fragmento de nuevas líneas y extraer cada línea completa, concatenando el resto si se corta en el medio. Sin embargo, puede haber una manera más elegante. No he trabajado mucho con las transmisiones. – travis

Respuesta

57

Para procesar un archivo línea por línea, simplemente tiene que desacoplar la lectura del archivo y el código que actúa sobre esa entrada. Puede lograr esto almacenando en búfer su entrada hasta que llegue a una nueva línea. Suponiendo que tenemos un objeto JSON por línea (básicamente, el 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, es escondido en un buffer, y luego pump se llama.

Si no hay nueva línea en el búfer, pump simplemente regresa sin hacer nada. Se agregarán más datos (y posiblemente una nueva línea) al búfer la próxima vez que la transmisión 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 al process. Luego verifica nuevamente si hay otra línea nueva 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 línea de entrada. Si está presente, quita el carácter de retorno de carro (para evitar problemas con las terminaciones de línea – LF vs CRLF), y luego llama al JSON.parse uno por línea. En este punto, puede hacer lo que necesite con su objeto.

Tenga en cuenta que JSON.parse es 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; debe usar {"name":"thing1"}.

Porque no más de una porción de datos alguna vez estará en la memoria a la vez, esto será extremadamente eficiente en la memoria. También será extremadamente rápido. Una prueba rápida mostró que procesé 10.000 filas en menos de 15 ms.

+0

Muy buena respuesta, he encontrado esto útil - gracias. – mrdnk

+11

Esta respuesta ahora es redundante. Use JSONStream, y tiene soporte listo para usar. – arcseldon

+1

El nombre de la función 'proceso' es malo. 'proceso' debería ser una variable del sistema. Este error me confundió por horas. –

1

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

ACTUALIZACIÓN: Puede utilizar mongoimport herramienta para importar datos JSON en MongoDB.

mongoimport --collection collection --file collection.json 
+0

Esto no responde la pregunta. Tenga en cuenta que la segunda línea de la pregunta dice que quiere hacer esto para * obtener datos en una base de datos *. – josh3736

+1

josh3736, tienes razón. Actualizo mi respuesta. –

27

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

Resulta que sí.

Desde que acabo de encontrar, he obviamente no lo usó, así que no puedo comentar sobre su calidad, pero me Me interesaría saber si funciona.

sí considera el siguiente trabajo CoffeeScript:

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

Esto registrará los objetos a medida que llegan, si la corriente es una matriz de objetos. Por lo tanto, lo único que se amortigua es un objeto a la vez.

19

partir de octubre de 2014, sólo puede hacer algo como lo siguiente (utilizando 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" 
} 

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 
+0

Esto es muy cierto y útil, pero creo que debes hacer 'parse ('*')' o no obtendrás ningún dato. –

+0

@JohnZwinck Gracias, actualicé la respuesta y agregué un ejemplo de trabajo para demostrarlo completamente. – arcseldon

+0

en el primer bloque de código, el primer conjunto de paréntesis 'var getStream() = function() {' debe eliminarse. – givemesnacks

3

que resuelven este problema utilizando el split npm module. Canalice la secuencia en split, y será "Divida una secuencia y vuelva a armarla para que cada línea sea un fragmento".

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);   
    // ... 
}); 
6

tuve requisito similar, tengo que leer un archivo JSON grande en js nodos y los datos de proceso en trozos y llamar a un API y guardar 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(); 
    }); 
} 
10

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

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

En la primera prueba, leo todo el archivo geojson en la memoria usando var data = require('./geo.json'). Eso tomó 3330 milisegundos y luego imprimir un atributo de cada característica tomó 804 milisegundos para un gran total de 4134 milisegundos. Sin embargo, parecía que node.js estaba usando 411MB de memoria.

En la segunda prueba, utilicé la respuesta de @arcseldon con JSONStream + event-stream. Modifiqué la consulta JSONPath para seleccionar solo lo que necesitaba. Esta vez, la memoria nunca fue más alta que 82 MB, sin embargo, ¡todo el proceso tardó 70 segundos en completarse!

1

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

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

Esto sigue siendo válido JSON.

Luego, use el módulo readline node.js 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! 
}