it-swarm.com.de

Große JSON-Datei in Nodejs parsen

Ich habe eine Datei, die viele JavaScript-Objekte in einem JSON-Formular speichert, und ich muss die Datei lesen, jedes Objekt erstellen und etwas damit tun (in meinem Fall in eine Datenbank einfügen). Die JavaScript-Objekte können in einem Format dargestellt werden:

Format A:

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

oder Format B:

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

Beachten Sie, dass ... viele JSON-Objekte anzeigt. Mir ist bekannt, dass ich die gesamte Datei in den Speicher lesen und dann JSON.parse() wie folgt verwenden könnte:

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

Die Datei könnte jedoch sehr groß sein, ich würde dafür lieber einen Stream verwenden. Das Problem, das ich mit einem Stream sehe, ist, dass der Inhalt der Datei an beliebigen Stellen in Datenblöcke unterteilt werden kann. Wie kann ich also JSON.parse() für solche Objekte verwenden? 

Im Idealfall würde jedes Objekt als separater Datenblock gelesen, aber ich bin nicht sicher, ob es sich bei wie geht das}.

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

Beachten Sie, ich möchte verhindern, dass die gesamte Datei in den Speicher eingelesen wird. Zeiteffizienz spielt für mich keine Rolle. Ja, ich könnte versuchen, mehrere Objekte gleichzeitig zu lesen und sie alle gleichzeitig einzufügen. Dies ist jedoch ein Performance-Tweak. Ich brauche einen Weg, der garantiert keine Speicherüberlastung verursacht, egal wie viele Objekte in der Datei enthalten sind . 

Ich kann FormatA oder FormatB oder vielleicht etwas anderes verwenden. Bitte geben Sie in Ihrer Antwort an. Vielen Dank!

78
dgh

Um eine Datei Zeile für Zeile zu verarbeiten, müssen Sie lediglich das Lesen der Datei und den Code, der auf diese Eingabe wirkt, entkoppeln. Sie können dies erreichen, indem Sie Ihre Eingaben puffern, bis Sie eine neue Zeile erreichen. Angenommen, wir haben ein JSON-Objekt pro Zeile (grundsätzlich 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!
    }
}

Jedes Mal, wenn der Dateistream Daten vom Dateisystem empfängt, werden diese in einem Puffer zwischengespeichert. Anschließend wird pump aufgerufen.

Wenn keine neue Zeile im Puffer vorhanden ist, wird pump einfach zurückgegeben, ohne etwas zu tun. Weitere Daten (und möglicherweise eine neue Zeile) werden dem Puffer hinzugefügt, wenn der Stream das nächste Mal Daten erhält, und dann erhalten wir ein vollständiges Objekt.

Wenn es eine neue Zeile gibt, schneidet pump den Puffer vom Anfang zur neuen Zeile ab und übergibt ihn an process. Anschließend wird erneut geprüft, ob sich im Puffer ein weiterer Zeilenumbruch befindet (die while-Schleife). Auf diese Weise können wir alle Zeilen bearbeiten, die im aktuellen Block gelesen wurden.

Schließlich wird process einmal pro Eingabezeile aufgerufen. Wenn vorhanden, wird das Wagenrücklaufzeichen entfernt (um Probleme mit Zeilenenden - LF vs. CRLF) zu vermeiden, und anschließend wird JSON.parse in der Zeile aufgerufen. An dieser Stelle können Sie mit Ihrem Objekt alles tun, was Sie benötigen.

Beachten Sie, dass JSON.parse streng ist, was als Eingabe akzeptiert wird. Sie müssen Ihre Bezeichner und Zeichenfolgenwerte mit Anführungszeichen angeben. Mit anderen Worten, {name:'thing1'} wird einen Fehler auslösen. Sie müssen {"name":"thing1"} verwenden.

Da sich immer nur ein Teil der Daten gleichzeitig im Speicher befinden wird, ist dies äußerst speichereffizient. Es wird auch extrem schnell sein. Ein schneller Test zeigte, dass ich 10.000 Reihen in weniger als 15 ms bearbeitet habe.

67
josh3736

Gerade als ich dachte, dass es Spaß machen würde, einen Streaming-JSON-Parser zu schreiben, dachte ich auch, dass ich vielleicht eine kurze Suche machen sollte, um zu sehen, ob bereits einer verfügbar ist.

Es stellt sich heraus, dass es gibt.

Da ich es gerade gefunden habe, habe ich es offenbar nicht verwendet, daher kann ich die Qualität nicht kommentieren, aber ich würde gerne wissen, ob es funktioniert.

Es funktioniert unter Berücksichtigung des folgenden CoffeeScript:

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

Dies protokolliert Objekte, sobald sie eingehen, wenn der Stream ein Array von Objekten ist. Daher wird immer nur ein Objekt gepuffert.

29
user1106925

Ab Oktober 2014 können Sie Folgendes tun (mit 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
     });

Um es mit einem Arbeitsbeispiel zu demonstrieren:

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

Mir ist klar, dass Sie möglichst nicht die gesamte JSON-Datei in den Speicher lesen möchten. Wenn Sie jedoch über ausreichend Speicher verfügen, ist dies möglicherweise keine schlechte Idee. Die Verwendung von node.js 'requir () für eine Json-Datei lädt die Daten sehr schnell in den Arbeitsspeicher. 

Ich habe zwei Tests durchgeführt, um zu sehen, wie die Leistung aussah, als ein Attribut aus jeder Funktion aus einer 81 MB-Geojson-Datei gedruckt wurde. 

Im ersten Test habe ich die gesamte Geojson-Datei mit var data = require('./geo.json') in den Speicher eingelesen. Das dauerte 3330 Millisekunden, und das Ausdrucken eines Attributs aus jedem Feature benötigte 804 Millisekunden für insgesamt 4134 Millisekunden. Es stellte sich jedoch heraus, dass node.js 411 MB Arbeitsspeicher verwendete.

Im zweiten Test habe ich die Antwort von @ arcseldon mit JSONStream + Ereignisstrom verwendet. Ich habe die JSONPath-Abfrage geändert, um nur das auszuwählen, was ich brauchte. Dieses Mal war der Speicher nie höher als 82 MB, jedoch dauerte es jetzt 70 Sekunden, bis der Vorgang abgeschlossen war! 

11
Evan Siroky

Ich hatte eine ähnliche Anforderung, ich muss eine große Json-Datei in Knoten js lesen und Daten in Klumpen verarbeiten und ein API aufrufen und in Mongodb speichern.

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

Jetzt habe ich JsonStream und EventStream verwendet, um dies synchron zu erreichen.

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

Ich habe dieses Problem mit dem split npm Modul gelöst. Pipe deinen Stream in "split" und es wird "einen Stream aufteilen und wieder zusammenfügen, so dass jede Zeile ein Block ist".

Beispielcode:

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

Ich habe ein Modul geschrieben, das dies kann, genannt BFJ . Insbesondere kann die Methode bfj.match verwendet werden, um einen großen Stream in einzelne JSON-Blöcke aufzuteilen:

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

Hier gibt bfj.match einen lesbaren Objektmodusstrom zurück, der die analysierten Datenelemente empfängt, und erhält 3 Argumente:

  1. Ein lesbarer Stream, der die Eingabe-JSON enthält.

  2. Ein Prädikat, das angibt, welche Elemente aus der analysierten JSON in den Ergebnisstrom verschoben werden.

  3. Ein Optionsobjekt, das angibt, dass es sich bei der Eingabe um einen durch Zeilenumbruch getrennten JSON-Code handelt (um Format B aus der Frage zu verarbeiten, es ist für Format A nicht erforderlich).

Nach dem Aufruf analysiert bfj.match JSON aus dem Eingangsstrom in der Tiefe und ruft das Prädikat mit jedem Wert auf, um zu bestimmen, ob dieses Element in den Ergebnisstrom verschoben werden soll oder nicht. Dem Prädikat werden drei Argumente übergeben:

  1. Der Eigenschaftsschlüssel- oder Arrayindex (dies ist undefined für Elemente der obersten Ebene).

  2. Der Wert selbst.

  3. Die Tiefe des Elements in der JSON-Struktur (Null für Elemente auf oberster Ebene).

Natürlich kann je nach Bedarf auch ein komplexeres Prädikat verwendet werden. Sie können auch eine Zeichenfolge oder einen regulären Ausdruck anstelle einer Prädikatfunktion übergeben, wenn Sie einfache Übereinstimmungen mit den Eigenschaftsschlüsseln durchführen möchten.

3
Phil Booth

Wenn Sie die Eingabedatei mit einem Array von Objekten steuern können, können Sie dies einfacher lösen. Vereinbaren Sie die Ausgabe der Datei mit jedem Datensatz in einer Zeile wie folgt:

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

Dies ist immer noch gültig JSON.

Verwenden Sie dann das Readline-Modul node.js, um sie Zeile für Zeile zu verarbeiten.

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

Ich denke, Sie müssen eine Datenbank verwenden. MongoDB ist in diesem Fall eine gute Wahl, da es JSON-kompatibel ist.

UPDATE: Mit dem Werkzeug mongoimport können Sie JSON-Daten in MongoDB importieren.

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