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 の説明中に以下のような警告をワザワザ書いてあったりする:
nullis 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