Nushellでスキーマ非統一なJSON Linesのデータをselectする
概要
-
Nushell で JSON Lines (ndjson) 形式のログファイルを解析しようとしたところ、
select
で指定した列が存在しないというエラーになった。 - そのデータは行ごとにスキーマが異なるもので、抽出しようとした列が存在しない行があったためエラーになっていた。
- 行によっては存在しないような列を
select
する場合、--ignore-errors
オプションを使うか列名の直後に?
を付ければ、そのような列の値がnull
であったかのように扱うことができselect
できるようになる
動作確認に使った Nushell は 0.93 で、GCP の Cloud Run ジョブで出力した JSON 形式のログを解析するときに経験した話。アプリの出力したログメッセージは jsonPayload
というキーに格納されるので select jsonPayload
としてアプリのメッセージだけ抽出しようとしたところエラーになった。
現象 (経験した問題)
JSON Lines (ndjson) 形式のファイルがあり、ただし各行のスキーマが統一されていないとする。例えば次のようなデータがあるとき:
{"foo": 1, "bar": 2, "qux": null}
{"foo": 3, "qux": 4}
これは次のようにすれば行ごとに JSON としてパースして Nushell のテーブルとして扱えるようになる:
open --raw data.json | from json -o
これを実行すると次のようなテーブルデータがコンソールに表示される:
# | foo | bar | qux |
---|---|---|---|
0 | 1 | 2 | |
1 | 3 | ❎ | 4 |
なお上の表は実際に Nushell がターミナルに出力する表示を再現したもので、空欄は値が null
であることを、❎
は該当する値が存在しないこと (absence of value) を表している。
で、このデータに対して select
を使って列を絞り込む。ここで bar
を指定すると次のように「そのような列は無い」というエラーになる:
> open --raw data.json | from json -o | select bar
Error: nu::shell::column_not_found
× Cannot find column
╭─[entry #19:1:1]
1 │ open --raw data.json | from json -o | select bar
· ──┬─ ─┬─
· │ ╰── cannot find column 'bar'
· ╰── value originates here
╰────
処理対象のテーブルに注目していた自分は「え、bar
列あるじゃん、なんで?」となって困惑した。ちなみに foo
列と qux
列は select
できる。
原因と対策
このエラー原因に関係する Nushell の仕様を以下に挙げる:
- あるテーブルにおける、あるセルの値が
null
である状態と、そのセルに値が存在しない状態 (absence of a value) とは区別される。 - 値が存在しないようなテーブルのセルを
get
やselect
で処理しようとすると失敗する。
そして、値が存在しないようなテーブルのセルを処理する方法には 2 種類ある:
-
get
やselect
にオプション--ignore-errors
(-i
) を指定する。 - そのようなセルのある列名 (cell path) をそのまま指定する代わりに
?
を付けた列名 (optional cell path) を使う
いずれの場合も、値が存在しないセルについては「値が null
である」とみなして処理してくれる:
open --raw data.json | from json -o | select bar?
# または
open --raw data.json | from json -o | select -i bar
# | bar |
---|---|
0 | 2 |
1 |
これで、期待通り列の絞り込みができる 🎉
あとがき
実は Nushell は 0.93 で試験的に JSON Lines (ndjson) 形式のファイルに対応する関数 4 つが「標準ライブラリ」として同梱された。from jsonl
, from ndjson
, to jsonl
, to ndjson
というコマンドで、それらは use std formats "to jsonl"
などと関数名を指定して有効化するか use std formats *
と一括で有効化すれば使えるようになる。これらを使うと、まず from xxx
が定義されていると拡張子が xxx
であるファイルを開くときに使われるため open --raw FILENAME.jsonl | from json -o
は open foo.jsonl
で済むようになる。また to jsonl
を使うと ... | each { to json --raw }
は ... | to jsonl
で済むようになる。こんな話もあるので、例として使うファイル名を data.jsonl
にしても良かったのだけれど、記事を書くきっかけとなった GCP (Cloud Run ジョブ) のログは拡張子が json
なので、それに合わせた内容としてみた。
最後に、この問題に突き当たったときに感じたことを。Nushell ではテーブルにおけるデータの位置を指し示す文字列は cell path と呼ばれ、「存在しない位置を指定する cell path へのアクセスは失敗する」とされている:
By default, cell path access will fail if it can't access the requested row or column. To suppress these errors, you can add ? to a cell path member to mark it as optional:
...(略)...
When using optional cell path members, missing data is replaced with null.
—https://www.nushell.sh/book/types_of_data.html#cell-paths
PostgreSQL などのデータベースで JSON 型の列を扱う場合、値が存在しないことを表す SQL の NULL
と「存在しないという意味の値 (この値は存在する)」である JSON の null
を区別しなければならない面倒くささがある。Nushell でも同様のことがあり、null
の説明中に以下のような警告をワザワザ書いてあったりする:
null
is not the same as the absence of a value! It is possible for a table to be produced that has holes in some of its rows. Attempting to access this value will not producenull
, but instead cause an error:
...(略)...
If you would prefer this to returnnull
, mark the cell path member as optional like.1.a?
.The absence of a value is (as of Nushell 0.71) printed as the ❎ emoji in interactive output.
—https://www.nushell.sh/book/types_of_data.html#null
「データが存在しないことを表す」というトピックは、たいてい混沌としている。R 言語や SQL といった統計・データ分析に強い処理系では最初から欠損値・欠測値をキチンと取り扱えるようサポートしていることもあるのだけれど、そうでない処理系とデータの相互運用がたいてい必要になる。すると前者のタイプの処理系に由来する「キチンと定義された欠損」と、後者のタイプの処理系に由来する「欠損を表す特別な値」が混在するデータを処理することとなる。これが面倒ごとの根本にあるのかなぁ、などと思っている。
Discussion