💻

Nushellでスキーマ非統一なJSON Linesのデータをselectする

2024/05/19に公開

概要

  • NushellJSON Lines (ndjson) 形式のログファイルを解析しようとしたところ、select で指定した列が存在しないというエラーになった。
  • そのデータは行ごとにスキーマが異なるもので、抽出しようとした列が存在しない行があったためエラーになっていた。
  • 行によっては存在しないような列を select する場合、--ignore-errors オプションを使うか列名の直後に ? を付ければ、そのような列の値が null であったかのように扱うことができ select できるようになる

動作確認に使った Nushell は 0.93 で、GCP の Cloud Run ジョブで出力した JSON 形式のログを解析するときに経験した話。アプリの出力したログメッセージは jsonPayload というキーに格納されるので select jsonPayload としてアプリのメッセージだけ抽出しようとしたところエラーになった。

現象 (経験した問題)

JSON Lines (ndjson) 形式のファイルがあり、ただし各行のスキーマが統一されていないとする。例えば次のようなデータがあるとき:

data.json
{"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 の仕様を以下に挙げる:

  1. あるテーブルにおける、あるセルの値が null である状態と、そのセルに値が存在しない状態 (absence of a value) とは区別される。
  2. 値が存在しないようなテーブルのセルを getselect で処理しようとすると失敗する。

そして、値が存在しないようなテーブルのセルを処理する方法には 2 種類ある:

  1. getselect にオプション --ignore-errors (-i) を指定する。
  2. そのようなセルのある列名 (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 -oopen 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 produce null, but instead cause an error:
...(略)...
If you would prefer this to return null, 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