jqを使ってJSONデータを変換する方法
作者は、書いて募金するプログラムの一環としてフリーやオープンソースの基金を寄付先として選びました。
序文(じょぶん)
大規模なJSONファイルで作業する際に、必要な情報を見つけて操作するのは難しい場合があります。手作業で関連するスニペットをコピーして合計を計算することもできますが、時間がかかる上に人為的なミスのリスクもあります。他の選択肢としては、情報の検索や操作に汎用ツールを使用する方法もあります。現代のLinuxシステムでは、sed、awk、grepの3つの確立されたテキスト処理ユーティリティがすでにインストールされています。これらのコマンドは、ゆるく構造化されたデータで作業する際に役立ちますが、JSONのような機械可読データ形式に対しては他のオプションも存在します。
jqは、コマンドラインでJSONの処理を行うツールであり、機械可読なデータ形式を扱う際に良い解決策です。特にシェルスクリプトでの使用が非常に便利です。jqを使用することで、データの操作を容易に行うことができます。例えば、JSON APIへのcurlコールを実行し、jqを使用してサーバーの応答から特定の情報を抽出することができます。また、データエンジニアとしてjqをデータ取り込みプロセスに組み込むこともできます。Kubernetesクラスタを管理している場合は、kubectlのJSON出力をjqの入力ソースとして使用して、特定のデプロイメントの利用可能なレプリカ数を抽出することができます。
この記事では、海洋生物に関するサンプルのJSONファイルを変換するためにjqを使用します。フィルタを利用してデータ変換を行い、変換されたデータの一部を新しいデータ構造に結合します。チュートリアルの最後には、操作したデータに関する質問にjqスクリプトを使用できるようになります。
前提条件
このチュートリアルを完了するためには、以下のものが必要です。
- jq, a JSON parsing and transformation tool. It is available from the repositories for all major Linux distributions. If you are using Ubuntu, run sudo apt install jq to install it.
- An understanding of JSON syntax, which you can refresh in An Introduction to JSON.
1. ステップ1ー最初のjqコマンドの実行
このステップでは、サンプル入力ファイルを設定し、jqコマンドを実行してサンプルファイルのデータの出力を生成することで、セットアップをテストします。jqはファイルまたはパイプから入力を受け取ることができますが、ここでは前者を使用します。
最初に、サンプルファイルを生成します。お好きなエディタ(このチュートリアルではnanoを使用)を使って、新しいファイル seaCreatures.json を作成して開いてください。
- nano seaCreatures.json
以下の内容をファイルにコピーしてください。
[
{ "name": "Sammy", "type": "shark", "clams": 5 },
{ "name": "Bubbles", "type": "orca", "clams": 3 },
{ "name": "Splish", "type": "dolphin", "clams": 2 },
{ "name": "Splash", "type": "dolphin", "clams": 2 }
]
チュートリアルの残りの部分でもこのデータを使用します。チュートリアルの最後までに、このデータについて以下の質問に答えるためのjqコマンドを一行で書くことになります。
- What are the names of the sea creatures in list form?
- How many clams do the creatures own in total?
- How many of those clams are owned by dolphins?
ファイルを保存して閉じてください。
入力ファイルに加えて、行いたい正確な変換を説明するフィルタが必要です。.(ピリオド)フィルタ、または恒等演算子とも呼ばれるフィルタは、JSONの入力をそのまま出力します。
セットアップが正しく機能しているかをテストするために、同一演算子を使用することができます。もしパースエラーが表示された場合は、seaCreatures.jsonが有効なJSONを含んでいるか確認してください。
以下のコマンドを使用して、JSONファイルにアイデンティティ演算子を適用します。
- jq ‘.’ seaCreatures.json
ファイルを使用してjqを使う場合、常に入力ファイルに続けてフィルターを渡します。フィルターには、シェルに特別な意味を持つスペースや他の文字が含まれる可能性があるため、フィルターをシングルクォートで囲むのが良い習慣です。それにより、フィルターがコマンドパラメーターであることをシェルに伝えます。安心してください、jqを実行しても元のファイルは変更されません。
以下の出力結果を受け取ります。
[ { “name”: “Sammy”, “type”: “shark”, “clams”: 5 }, { “name”: “Bubbles”, “type”: “orca”, “clams”: 3 }, { “name”: “Splish”, “type”: “dolphin”, “clams”: 2 }, { “name”: “Splash”, “type”: “dolphin”, “clams”: 2 } ]
デフォルトでは、jqは出力を見やすく整形します。自動的にインデントを適用し、値の後に改行を追加し、可能な場合には出力に色を付けます。色付けにより、他のツールで生成されたJSONデータを調査する際に、多くの開発者が可読性を向上させることができます。たとえば、JSON APIに対してcurlリクエストを送信する際に、JSONの応答をjq ‘.’にパイプして整形する場合があります。
現在、jqを使ってデータを操作できるようになりました。入力ファイルが設定されているので、いくつかの異なるフィルタを使用して、creatures、totalClams、totalDolphinClamsの値を計算します。次のステップでは、creaturesの値から情報を取得します。
ステップ2 – 生物の価値を取得する
このステップでは、クリーチャーの値を使用して、全ての海の生物のリストを生成します。このステップの最後には、以下の名前のリストが生成されます。
[ “Sammy”, “Bubbles”, “Splish”, “Splash” ],
このリストを生成するためには、生き物の名前を抽出し、それらを配列に結合する必要があります。
すべての生物の名前を取得し、それ以外のものを破棄するために、フィルタを洗練させる必要があります。配列で作業しているため、jqに対して配列そのものではなく、その配列の値に対して操作したいことを伝える必要があります。.[]と書かれる配列値イテレータは、この目的を果たします。
修正したフィルタを使用してjqを実行してください。
- jq ‘.[]’ seaCreatures.json
すべての配列の値は、現在個別に出力されます。
{ “name”: “Sammy”, “type”: “shark”, “clams”: 5 } { “name”: “Bubbles”, “type”: “orca”, “clams”: 3 } { “name”: “Splish”, “type”: “dolphin”, “clams”: 2 } { “name”: “Splash”, “type”: “dolphin”, “clams”: 2 }
全ての配列アイテムを完全に出力する代わりに、name属性の値を出力し、残りを破棄したいでしょう。パイプ演算子「|」を使用することで、各出力にフィルターを適用することができます。コマンドライン上でfind | xargsを使用して、各検索結果にコマンドを適用したことがあれば、このパターンは馴染みがあるでしょう。
seaCreatures.json ファイルに対して、パイプとフィルタを組み合わせて、.name を書き込むことで、JSONオブジェクトの name プロパティにアクセスできます。
- jq ‘.[] | .name‘ seaCreatures.json
他の属性が出力から消えていることにお気づきになるでしょう。
“Sammy” “Bubbles” “Splish” “Splash”
デフォルトでは、jqは正しいJSONを出力するため、文字列は二重引用符(“”)で表示されます。もしも二重引用符なしの文字列が必要な場合は、-rフラグを追加して生の出力を有効にしてください。
- jq -r ‘.[] | .name’ seaCreatures.json
引用符が消えてしまいました。
Sammy Bubbles Splish Splash
あなたは今、JSON入力から特定の情報を抽出する方法を知っています。次のステップでは、このテクニックを使って他の特定の情報を見つけ、最終ステップで生物の値を生成します。
ステップ3 – mapとaddを使用してtotalClamsの値を計算します。
このステップでは、生物が持っている貝の総数の情報が得られます。いくつかのデータを集計することで、答えを計算することができます。jqに慣れれば、手動計算よりも早く、人為的なミスのリスクも減ります。このステップの最終的な予測値は12です。
ステップ2では、項目のリストから特定の情報を抽出しました。この技術を再利用して、アサリの属性値を抽出することができます。新しい属性のためにフィルタを調整し、次のコマンドを実行してください。
- jq ‘.[] | .clams‘ seaCreatures.json
貝の属性の個別の値が出力されます。
5 3 2 2
個々の値の合計を求めるには、「add」フィルタが必要です。この「add」フィルタは配列に対して使われます。ただし、現在は配列の値を出力しているため、まず配列に包んでおく必要があります。
以下のように、既存のフィルターを[]で囲んでください。
- jq ‘[.[] | .clams]‘ seaCreatures.json
値はリストに表示されます。 (The values will be displayed in a list.)
[ 5, 3, 2, 2 ]
アドフィルターを適用する前に、マップ関数を使用してコマンドの可読性を向上させることができます。これにより、保守も容易になります。配列を繰り返し処理し、各アイテムにフィルターを適用し、結果を配列でラップするためには、1つのマップ呼び出しで実現することができます。アイテムの配列が与えられた場合、マップは各アイテムに引数としてフィルターを適用します。例えば、[{“name”: “Sammy”}, {“name”: “Bubbles”}]に対してフィルターmap(.name)を適用すると、結果のJSONオブジェクトは[“Sammy”, “Bubbles”]となります。
フィルターの書き換えを行い、配列を生成するためにマップ関数を使用し、それを実行してください。
- jq ‘map(.clams)’ seaCreatures.json
以前と同じ結果が出力されます。
[ 5, 3, 2, 2 ]
今、配列を持っているので、それを追加フィルターにパイプできます。
- jq ‘map(.clams) | add‘ seaCreatures.json
配列の合計額が受け取れます。
12
このフィルターを使って、貝の総数を計算しました。この後、totalClamsの値を生成するために使用します。3つの質問のうち、2つのフィルターを既に作成しました。あと1つのフィルターを作成した後、最終的な出力を生成することができます。
ステップ4:addフィルタを使用して、totalDolphinClamsの値を計算する。
生物が持っている二枚貝の数がわかったので、イルカがそれらの二枚貝のうち何枚持っているかを特定することができます。特定の条件を満たす配列要素の値のみを加算することで、答えを生成できます。このステップの終わりに期待される値は4であり、それがイルカが持っている二枚貝の総数です。最終ステップでは、この結果の値がtotalDolphinClams属性で使用されます。
Step3で行ったように、すべてのアサリの値を追加する代わりに、「イルカ」のタイプを持つクリーチャーが持っているアサリのみを数えます。特定の条件を選択するため、select関数を使用します:select(条件)。条件がtrueと評価される場合は、入力がそのまま渡されます。それ以外の入力は破棄されます。例えば、JSONの入力が「イルカ」で、フィルターがselect(. == “イルカ”)である場合、出力は「イルカ」となります。入力が「Sammy」の場合、同じフィルターでは何も出力されません。
配列のすべての値に適用するには、mapを使用することができます。その際、条件を満たさない配列の値は破棄されます。
あなたの場合、タイプの値が「イルカ」と等しい配列の値のみを保持したいです。結果として得られるフィルターは次のようなものです。
- jq ‘map(select(.type == “dolphin”))’ seaCreatures.json
あなたのフィルターは、サメのサミーとシャチのバブルズには合致しませんが、2匹のイルカには合致します。
[ { “name”: “Splish”, “type”: “dolphin”, “clams”: 2 }, { “name”: “Splash”, “type”: “dolphin”, “clams”: 2 } ]
この出力には、生物ごとの貝の数が含まれていますが、関係のない情報も含まれています。貝の値のみを保持するには、マップのパラメータの末尾にフィールドの名前を追加することができます。
- jq ‘map(select(.type == “dolphin”).clams)’ seaCreatures.json
マップ関数は配列を入力として受け取り、マップのフィルター(引数として渡される)を各配列要素に適用します。その結果、クリーチャーごとにセレクトが4回呼び出されます。セレクト関数は、条件に一致する2つのイルカのために出力を生成し、それ以外の要素は省略します。
あなたの出力は、2つの一致する生物の貝の値のみを含む配列となります。
[ 2, 2 ]
配列の値をaddにパイプしてください。
- jq ‘map(select(.type == “dolphin”).clams) | add‘ seaCreatures.json
「ドルフィン」のタイプの生物の貝の価値の合計が出力されます。
4
マップとセレクトを組み合わせて、配列へのアクセスを成功させ、条件に一致する配列の要素を選択し、それらを変換してその変換結果を合計することに成功しました。この戦略を使用して、最終出力のtotalDolphinClamsを計算することができます。次のステップでその計算を行います。
ステップ5− データを新しいデータ構造に変換する
前の手順で、サンプルデータを抽出し、操作するフィルターを書きました。これらのフィルターを組み合わせて、データに関する質問に答える出力を生成することができます。
- What are the names of the sea creatures in list form?
- How many clams do the creatures own in total?
- How many of those clams are owned by dolphins?
リスト形式で海の生き物の名前を見つけるために、マップ関数を使用しました:map(.name)。生き物たちが所有する貝の数を合計するために、すべての貝の値を追加フィルターにパイプしました:map(.clams) | add。それらの貝のうちイルカが所有する貝の数を見つけるために、select関数を使用し、条件は「.type == “dolphin”」です:map(select(.type == “dolphin”).clams) | add。
これらのフィルターを1つのjqコマンドに統合して、すべての作業を行います。新しいJSONオブジェクトを作成し、3つのフィルターを組み合わせて、望んだ情報を表示するための新しいデータ構造を作成します。
以下の通り、ご提供いただいたJSONファイルが初期データとなりますので、ご確認ください。
[
{ "name": "Sammy", "type": "shark", "clams": 5 },
{ "name": "Bubbles", "type": "orca", "clams": 3 },
{ "name": "Splish", "type": "dolphin", "clams": 2 },
{ "name": "Splash", "type": "dolphin", "clams": 2 }
]
変換されたJSONの出力は次のように生成されます。
{ “creatures”: [ “Sammy”, “Bubbles”, “Splish”, “Splash” ], “totalClams”: 12, “totalDolphinClams”: 4 }
以下には、入力値が空の場合の完全なjqコマンドの構文のデモンストレーションがあります。
- jq ‘{ creatures: [], totalClams: 0, totalDolphinClams: 0 }’ seaCreatures.json
このフィルターを使用すると、3つの属性を含むJSONオブジェクトを作成できます。
{ “creatures”: [], “totalClams”: 0, “totalDolphinClams”: 0 }
これは最終的な出力に似てきていますが、入力値が正しくありません。なぜなら、それらはあなたのseaCreatures.jsonファイルから取得されていないからです。
「前のステップで作成したフィルターで、ハードコードされた属性値を置き換えてください」
- jq ‘{ creatures: map(.name), totalClams: map(.clams) | add, totalDolphinClams: map(select(.type == “dolphin”).clams) | add }’ seaCreatures.json
上記のフィルターは、jqに対してJSONオブジェクトを作成するよう指示します。
- A creatures attribute containing a list of every creature’s name value.
- A totalClams attribute containing a sum of every creature’s clams value.
- A totalDolphinClams attribute containing a sum of every creature’s clams value for which type equals “dolphin”.
コマンドを実行し、このフィルターの出力は次のようになります。
{ “creatures”: [ “Sammy”, “Bubbles”, “Splish”, “Splash” ], “totalClams”: 12, “totalDolphinClams”: 4 }
今、3つの質問に関連するデータを提供する単一のJSONオブジェクトを持っています。データセットが変わった場合でも、書いたjqフィルタを使用していつでも変換を再適用することができます。
結論
JSON入力を扱う際には、sedのようなテキスト操作ツールでは難しいデータの変換をjqで行うことができます。このチュートリアルでは、select関数でデータをフィルタリングし、mapで配列要素を変換し、addフィルターで数値の配列を合計し、変換を新しいデータ構造にマージする方法を学びました。
jqの高度な機能について学びたい場合は、jqリファレンスドキュメントを詳しく見てください。もしJSON以外のコマンド出力でよく作業する場合は、sed、awk、grepのガイドを探索することで、どんな形式でも動作するテキスト処理技術に関する情報を得ることができます。