Nodejsで大きなJSONファイルを解析する


98

多くのJavaScriptオブジェクトをJSON形式で格納するファイルがあり、ファイルを読み取り、各オブジェクトを作成し、それらを使用して何かを行う必要があります(私の場合は、それらをdbに挿入します)。JavaScriptオブジェクトは次の形式で表すことができます。

フォーマットA:

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

またはフォーマットB:

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

...は多くのJSONオブジェクトを示していることに注意してください。ファイル全体をメモリに読み込んで、次のJSON.parse()ように使用できることを知っています。

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

ただし、ファイルが非常に大きくなる可能性があるため、ストリームを使用してこれを実現したいと思います。ストリームで発生する問題は、ファイルのコンテンツがいつでもデータチャンクに分割される可能性があることJSON.parse()です。そのようなオブジェクトでどのように使用できますか?

理想的には、各オブジェクトは個別のデータチャンクとして読み取られますが、その方法はわかりません

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

ファイル全体をメモリに読み込まないようにしたいことに注意してください。時間効率は私には関係ありません。はい、複数のオブジェクトを一度に読み取って一度にすべて挿入しようとすることもできますが、これはパフォーマンスの微調整です。ファイルに含まれるオブジェクトの数に関係なく、メモリの過負荷を引き起こさないことが保証されている方法が必要です。 。

私は使用するFormatAFormatB、あるいは何か他のものを選択することができます、あなたの答えで明記してください。ありがとう!


フォーマットBの場合、チャンク全体で新しい行を解析し、各行全体を抽出し、途中で途切れた場合は残りを連結できます。よりエレガントな方法があるかもしれません。私はストリームをあまり扱っていません。
トラビス

回答:


82

ファイルを1行ずつ処理するには、ファイルの読み取りとその入力に作用するコードを分離するだけです。これは、改行に達するまで入力をバッファリングすることで実現できます。1行に1つのJSONオブジェクトがあると仮定します(基本的に、フォーマット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!
    }
}

ファイルストリームは、ファイルシステムからデータを受信するたびに、バッファに格納されてからpump呼び出されます。

バッファに改行がない場合は、pump何もせずに単に戻ります。次にストリームがデータを取得したときに、より多くのデータ(および場合によっては改行)がバッファーに追加され、完全なオブジェクトが作成されます。

改行がある場合pump、バッファを最初から改行に切り取り、それをに渡しますprocess。次に、バッファ(whileループ)に別の改行があるかどうかを再度チェックします。このようにして、現在のチャンクで読み取られたすべての行を処理できます。

最後に、process入力行ごとに1回呼び出されます。存在する場合は、キャリッジリターン文字を取り除き(行末の問題を回避するためにLFとCRLFを比較)、JSON.parse1つの行を呼び出します。この時点で、オブジェクトに対して必要なことをすべて実行できます。

JSON.parse入力として受け入れるものについては厳密であることに注意してください。識別子と文字列値は二重引用符で囲む必要があります。つまり、{name:'thing1'}エラーがスローされます。使用する必要があります{"name":"thing1"}

一度にメモリに格納されるのはデータのチャンクにすぎないため、これはメモリ効率が非常に高くなります。また、非常に高速になります。簡単なテストで、15行未満で10,000行を処理したことがわかりました。


12
この答えは冗長です。JSONStreamを使用すれば、すぐにサポートを利用できます。
arcseldon 2014

2
関数名 'process'は不正です。「プロセス」はシステム変数である必要があります。このバグは私を何時間も混乱させました。
Zhigong Li、2015

17
@arcseldonこれを行うライブラリがあるという事実は、この答えを冗長にすることはないと思います。これがモジュールなしでどのように実行できるかを知ることは、確かにまだ役に立ちます。
Kevin B

3
これが縮小されたjsonファイルで機能するかどうかはわかりません。ファイル全体が1行にまとめられ、そのような区切り文字を使用できない場合はどうなりますか?では、この問題をどのように解決すればよいでしょうか?
SLearner 2015

7
サードパーティのライブラリは、あなたが知っている魔法で作られていません。それらは、まさにこの答えのような、手作業で作成されたソリューションの精巧なバージョンですが、プログラムとしてパッケージ化され、ラベルが付けられています。物事がどのように機能するかを理解することは、結果を期待してデータをライブラリに盲目的に投入するよりもはるかに重要で関連性があります。ただ言って:)
zanona

34

ストリーミングJSONパーサーを作成するのは楽しいだろうと思っていたのと同じように、クイック検索を実行して、すでに使用可能なものがあるかどうかを確認する必要があるかもしれません。

あることがわかりました。

見つけたばかりなので、明らかに使用していないので、品質についてはコメントできませんが、動作するかどうか興味があります。

次のJavascriptと_.isString

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

これは、ストリームがオブジェクトの配列である場合、オブジェクトが入ってくるときにログに記録します。したがって、バッファリングされるのは、一度に1つのオブジェクトだけです。


29

2014年10月の時点で、次のようなことを行うことができます(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
});

実用的な例を使って説明するには:

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

2
これはほとんど真実で便利ですが、実行する必要parse('*')があるか、データを取得できないと思います。
John Zwinck、2014年

@JohnZwinckありがとうございます。回答を更新し、それを完全に示すための実用的な例を追加しました。
arcseldon 2014年

最初のコードブロックでは、括弧の最初のセットをvar getStream() = function () {削除する必要があります。
Givemesnacks 2015

1
これは、500mb jsonファイルでのメモリ不足エラーで失敗しました。
キースジョンハッチソン

18

JSONファイル全体を可能な限りメモリに読み込まないようにする必要があることは承知していますが、メモリが利用可能であれば、パフォーマンスの点で悪い考えではないかもしれません。jsonファイルでnode.jsのrequire()を使用すると、データがメモリに非常に高速にロードされます。

2つのテストを実行して、81MBのgeojsonファイルから各機能の属性を出力したときのパフォーマンスを確認しました。

最初のテストでは、を使用してgeojsonファイル全体をメモリに読み込みましたvar data = require('./geo.json')。これには3330ミリ秒かかり、各機能から属性を出力するには804ミリ秒かかり、合計で4134ミリ秒でした。ただし、node.jsが411MBのメモリを使用しているようです。

2番目のテストでは、JSONStream + event-streamで@arcseldonの回答を使用しました。JSONPathクエリを変更して、必要なものだけを選択しました。今回はメモリが82MBを超えることはありませんでしたが、全体が完了するまでに70秒かかりました。


18

同様の要件がありました。ノードjsで大きなjsonファイルを読み取り、チャンクでデータを処理し、apiを呼び出してmongodbに保存する必要があります。inputFile.jsonは次のようになります。

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

今私はJsonStreamとEventStreamを使用してこれを同期的に実現しました。

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

回答を追加していただき、ありがとうございます。私のケースでも同期処理が必要でした。しかし、テスト後、パイプの終了後にコールバックとして「end()」を呼び出すことはできませんでした。できることはイベントを追加することだけだと思います。ストリームが「fileStream.on( 'close'、...)」で「終了」/「クローズ」した後に何が起こるかです。
nonNumericalFloat

6

私はこれを行うことができるBFJと呼ばれるモジュールを書きました。具体的には、このメソッドbfj.matchを使用して、大きなストリームを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
  });

ここでbfj.matchは、解析されたデータアイテムを受け取り、3つの引数が渡される、読み取り可能なオブジェクトモードストリームを返します。

  1. 入力JSONを含む読み取り可能なストリーム。

  2. 解析されたJSONのどのアイテムが結果ストリームにプッシュされるかを示す述語。

  3. 入力が改行区切りのJSONであることを示すオプションオブジェクト(これは質問からフォーマットBを処理するためのもので、フォーマットAには必要ありません)。

呼び出されると、bfj.match入力ストリームからJSONを深さ優先で解析し、各値で述語を呼び出して、その項目を結果ストリームにプッシュするかどうかを決定します。述語には3つの引数が渡されます。

  1. プロパティキーまたは配列インデックス(これはundefinedトップレベルのアイテム用です)。

  2. 値自体。

  3. JSON構造内のアイテムの深さ(トップレベルのアイテムの場合はゼロ)。

もちろん、必要に応じて、必要に応じて、より複雑な述語を使用することもできます。プロパティキーに対して単純な一致を実行する場合は、述語関数の代わりに文字列または正規表現を渡すこともできます。


4

この問題は、split npmモジュールを使用して解決しました。ストリームをスプリットにパイプすると、「ストリームを分割して再構成し、各行がチャンクになるようにします」。

サンプルコード:

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

入力ファイルを制御でき、それがオブジェクトの配列である場合、これをより簡単に解決できます。次のように、各レコードを1行に出力するように配置します。

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

これはまだ有効なJSONです。

次に、node.js readlineモジュールを使用して、一度に1行ずつ処理します。

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

-1

データベースを使う必要があると思います。この場合、JSON互換であるため、MongoDBが適しています。

UPDATEmongoimportツールを使用して、JSONデータをMongoDBにインポートできます。

mongoimport --collection collection --file collection.json

1
これは質問の答えにはなりません。質問の2行目は、データをデータベースに取り込むためにこれを実行したいと言っていることに注意してください。
josh3736 2012

mongoimportは、最大16MBのファイルサイズのみをインポートします。
Haziq Ahmed
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.