理系学生日記

おまえはいつまで学生気分なのか

非JSONの文字列が含まれるファイルに対してjqを使いたい

jq は CLI で使用できる JSON processor でみなさんも日々お使いのことと存じます。

stedolan.github.io

問題

jq は標準入力に対して様々なフィルタや加工を行えるわけですが、基本的にはその入力フォーマットが JSON になっていることがベースになります。 このため、以下のように JSON の形ではない行が混ざると、問題になります。

{"level":"info","msg":"hello world","time":"2019-09-01T07:01:28.99333+09:00"}
hoge
{"level":"info","msg":"hello world","time":"2019-09-01T06:58:28.99333+09:00"}
fuga
{"level":"info","msg":"hello world","time":"2019-09-01T07:03:28.99333+09:00"}

例えば上記のファイルを jq に通すと、エラーになります。

$ cat log.json | jq -c '.'
{"level":"info","msg":"hello world","time":"2019-09-01T07:01:28.99333+09:00"}
parse error: Invalid numeric literal at line 3, column 0

最近は標準出力に対して様々なものが混ざってくるので、jq で処理できる行だけを処理し、処理できない行でエラーを起こしたくない。こういうときにどうすれば良いのかという話です。

解法

上記のようなケースにおいては、ぼくは以下のように jq -R 'fromjson?' 一度通すようにしています。

$ cat log.json | jq -cR 'fromjson?'
{"level":"info","msg":"hello world","time":"2019-09-01T07:01:28.99333+09:00"}
{"level":"info","msg":"hello world","time":"2019-09-01T06:58:28.99333+09:00"}
{"level":"info","msg":"hello world","time":"2019-09-01T07:03:28.99333+09:00"}

あとは後段に、jq を含む任意のコマンドをつなげれば良いですね。

解説

まず、-R (--raw-input) を使うことで、input を直接 JSON として parse しないようにし、文字列がそのまま jq のフィルタに渡るようにします。こうすることで、jq の入力処理でエラーになりません。 しかし、フィルタ部に渡るのは文字列ですから、これを JSON 化しなければなりません。これは fromjson で可能です。しかし、fromjson だけだと、JSON でない文字列に対してエラーが発生してしまいます。

$ cat log.json | jq -cR 'fromjson'
{"level":"info","msg":"hello world","time":"2019-09-01T07:01:28.99333+09:00"}
jq: error (at <stdin>:2): Invalid numeric literal at EOF at line 1, column 4 (while parsing 'hoge')
{"level":"info","msg":"hello world","time":"2019-09-01T06:58:28.99333+09:00"}
jq: error (at <stdin>:4): Invalid literal at EOF at line 1, column 4 (while parsing 'fuga')
{"level":"info","msg":"hello world","time":"2019-09-01T07:03:28.99333+09:00"}

ここで fromjson の最後に ? を加えます。これは、jq におけるエラー抑制の省略形ですね。

$ cat log.json | jq -cR 'fromjson?'
{"level":"info","msg":"hello world","time":"2019-09-01T07:01:28.99333+09:00"}
{"level":"info","msg":"hello world","time":"2019-09-01T06:58:28.99333+09:00"}
{"level":"info","msg":"hello world","time":"2019-09-01T07:03:28.99333+09:00"}

省略しなければ以下のような形でしょうか。

$ cat log.json | jq -cR 'try fromjson?'
{"level":"info","msg":"hello world","time":"2019-09-01T07:01:28.99333+09:00"}
{"level":"info","msg":"hello world","time":"2019-09-01T06:58:28.99333+09:00"}
{"level":"info","msg":"hello world","time":"2019-09-01T07:03:28.99333+09:00"}