JSONStreamの必要性csvなどの1行ごとに区切って保存できるデータ形式であれば巨大ファイルであってもNode.jsのReadlineにより1行ごと順番に処理が可能である。
しかしJSON形式の巨大なファイルを取り扱おうとした場合、行では意味を成さないためNode.jsの標準ライブラリでは取り扱いが難しい(巨大なファイルを一括で読み込まねばならない)
そこで使ってみたいと考えたのがJSONStream
これを使えば巨大なJSONファイルも意味を成す塊に分けて順番にロードできる。
例えば次のJSONデータを取り扱う
{"total_rows":129,"offset":0,"rows":[
{ "id":"change1_0.6995461115147918"
, "key":"change1_0.6995461115147918"
, "value":{"rev":"1-e240bae28c7bb3667f02760f6398d508"}
, "doc":{
"_id": "change1_0.6995461115147918"
, "_rev": "1-e240bae28c7bb3667f02760f6398d508","hello":1}
},
{ "id":"change2_0.6995461115147918"
, "key":"change2_0.6995461115147918"
, "value":{"rev":"1-13677d36b98c0c075145bb8975105153"}
, "doc":{
"_id":"change2_0.6995461115147918"
, "_rev":"1-13677d36b98c0c075145bb8975105153"
, "hello":2
}
},
]}JSONの場合は入れ子構造によりどんどん複雑になっていく。
特定の1個を取り出すなら簡単かもしれないが、この例のなかからrowsの配列に入っているオブジェクトのdocだけに絞って一つづつ取り出すのは難しい。
そこでJSONStreamはJSONPathという概念を使って取り出していく。
例えばSQLのようにクエリを発行してそれに一致するデータを纏めて取り出す方法。
JSONPathの構文については次が公式のようだ。
https://goessner.net/articles/JsonPath/
JSONPathの例例えば上の例でいくとrows.*.docというJSONPathを発行すると次のデータが帰ってくる。
{
_id: 'change1_0.6995461115147918',
_rev: '1-e240bae28c7bb3667f02760f6398d508',
hello: 1
}
{
_id: 'change2_0.6995461115147918',
_rev: '1-13677d36b98c0c075145bb8975105153',
hello: 2
}これはJavaScriptで見れば次に一致する。
const data = ファイル読み込み
for( const d of data.rows){
console.log(d.doc)
}
//rowsを1件づつ読み込み、さらにその中のdocだけを抽出している普通にオブジェクトのメンバ変数にアクセスするときの.だと思えばそこにワイルドカードなどが使えるようになっている印象だ。思ったよりわかりやすい。
と、ここまで書いてはみたが残念ながら私はJSONPathに関して明るくない。今回はこれを説明するための記事ではないのである程度は他のサイトに譲りたい。興味があれば調べてみて欲しい。
JSONStreamの使い方では実際に使っていく。まずはデータから。
[
{
"No": 1,
"value": "AAA"
},
{
"No": 2,
"value": "BBB"
},
{
"No": 3,
"value": "CCC"
},
{
"No": 4,
"value": "DDD"
}
]あえて一番上の階層が配列になっている形式にした。以前作成した5秒足データに形状を似せている。
※公式にはevent-streamのライブラリを合わせて使っているが、使用しなくてもある程度できるようなのでここでは使用しないこととした。
import JSONStream from 'JSONStream' //本体
import fs from 'fs' //ファイルを読み込むため用意するのはこれだけだ。
次にファイルをfsで読み込んで.pipeでJSONStreamにつないでやる。
const stream = fs.createReadStream('./test.json')
.pipe(JSONStream.parse('$*'))pipeは入力から直接出力につなぐ時に使うもの。例えば読んだファイルをそのまま別のファイルに出力したり標準出力process.stdout(普通のPCなら≒console.log)などに繋ぐといった利用法が挙げられる。
JSONStreamはこの出力として振る舞っている。
またcreateReadStreamはStreamオブジェクトと呼ばれるものを返す。これをstreamという箱に格納した。
JSONStream.parseここでparseが現れた。このメソッドがJSONPathを渡してデータを抽出するというコードだ。
ここでは$*がJSONPathとして渡っている。$は一番上の要素(ルート)を指し*はワイルドカードなので、親の配列に格納されているデータを1個づつ全てにヒットする、といった感じだろうか。
ちなみに$.*では何もヒットしない。.の次に入る文字列はキーを指す?
ここがメインの処理。次のコードでデータを塊単位で受信する(ファイルを一度に読み込まない)
stream.on('data', (data) => {
console.log(data.value)
if (data.value.No === 3) {
stream.pause()
}
}).onこれは先日話したevent-drivenの概念。
データの読み込みが1個発生するごとにdataというイベントが発生しデータ1個分が引数に格納されてくる。先程streamを格納しておいたのはこのイベントを受取るため。
単純にconsole.logで表示している。
また、データのNoが3になった場合読み込みを中断するpauseメソッドを試している。これはJSONStreamではなくStreamオブジェクトの機能。
{ No: 1, value: 'AAA' }
{ No: 2, value: 'BBB' }
{ No: 3, value: 'CCC' }1行ごとに1回の処理に分けて実行。つまり3回処理が実行されている。
まずは上で解説したpauseが機能するため4件目以降のデータは正しく止められている。
注意点が一つ。
上ではdata.valueとなっているが、これはサンプルデータの中にあるvalueではない。
console.log(data)に変更して実行してみると
{ value: { No: 1, value: 'AAA' }, key: 0 }
{ value: { No: 2, value: 'BBB' }, key: 1 }
{ value: { No: 3, value: 'CCC' }, key: 2 }どうやら親切なことにヒットしたデータをvalueに格納し、キー番号を付与してくれるようだ。
不思議なことに冒頭の例「rows.*.doc」ではvalueとkeyに別れずデータがそのまま中に入ってくる。
このあたりは配列に対して付与されるのか、$(一番親の要素)を使った時に付与されるのか詳しく調べていないのであしからず。
全部合わせてもたったこれだけのコードだが、覚えることは多い。
import JSONStream from 'JSONStream'
import fs from 'fs'
const stream = fs.createReadStream('./test.json')
.pipe(JSONStream.parse('$*'))
stream.on('data', (data) => {
console.log(data)
if (data.value.No === 3) {
stream.pause()
}
})書き込みにも使えるようだが詳しく調べていない。書き込み用のwritable streamを生成するコードはJSONStream.stringifyを使うようだがなんと公式もここは詳細な実例を挙げておらず理解できなかった。
どちらにせよNode.jsは「データの途中に挿入する形での書き込み」はワンランク上のプログラミング実装が必要であるため使うとしても一番うしろに追記するような使い方ぐらいだろう。
今回はJSONStreamの特別なコードは殆ど使っていないが他にも複数のメソッドがある。興味があれば公式も参照してほしい(それほど多くない印象)
とはいえこのライブラリはJSONPathが使えるかどうかがほぼ全てといっても過言ではないのでまずはそちらから覚えていったほうが良さそうだ。
相変わらずこの手のコアなライブラリは日本語の情報が少ないためこの記事が少しでも役に立てたら幸いである。
