💻

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 は標準で JSON Lines (ndjson) 形式のファイルに対応しており、拡張子が ndjson または jsonl であれば記事中で open --raw FILENAME | from json -o としているところは open FILENAME で済むようになる。なので例として使うファイル名を data.jsonl などとしてもよかったのだけれど、記事を書くきっかけとなった GCP (Cloud Run ジョブ) のログは拡張子が json なので、それに合わせた内容としてみた。

ちなみに Nushell は open 以外にも標準ライブラリ formatsfrom jsonl, from ndjson, to jsonl, to ndjson という JSON Lines (ndjson) の入出力をサポートするコマンドが入っており、それらを使えば例えば本記事の最後に書いた ... | each { to json --raw }... | to jsonl とすっきり書ける。なお標準ライブラリのコマンドは明示的に使用を宣言してからでないと使えないので、先の例は use std formats "to jsonl" を実行してからでないとエラーになるので注意。

最後に、この問題に突き当たったときに感じたことを。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