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
が使えるかどうかがほぼ全てといっても過言ではないのでまずはそちらから覚えていったほうが良さそうだ。
相変わらずこの手のコアなライブラリは日本語の情報が少ないためこの記事が少しでも役に立てたら幸いである。