JMESPath チュートリアルでプロジェクションを理解する
渡辺です。
前回に引き続き、AWSCLIのqueryオプションで利用できるJMESPathのチュートリアルを紹介します。 今回のチュートリアルを終わらせると、かなり細かい抽出まで可能になるのでしょう。
プロジェクション(投射)
プロジェクション(投射)は、JMESPathのキーとなる機能のひとつです。 イメージしずらいですが、要素をイイ感じにArrayに変換していくことができます。
リストのプロジェクション
ワイルドカードを使った[*]
はJSONのArrayに投射します。
[ {"first": "James", "last": "d"}, {"first": "Jacob", "last": "e"}, {"missing": "different"}, null ]
JMESPath | Result |
---|---|
[*] | [ {"first": "James", "last": "d"}, {"first": "Jacob", "last": "e"}, {"missing": "different"}] |
[*].first | ["James", "Jacob", "Jayden"] |
最もシンプルな[*]
はArrayをArrayに投射します。
この時、nullは除外されることがひとつの特徴です。
また、[*].first
のように、オブジェクトのキーで子要素を指定すると、Arrayの各オブジェクトの値を抽出できます。
この時、nullとキーが存在しないオブジェクトは除外されます。
対象がオブジェクトの場合、people[*]
のようにキーを前に付与して、対象のArrayを指定します。
{ "people": [ {"first": "James", "last": "d"}, {"first": "Jacob", "last": "e"}, {"first": "Jayden", "last": "f"}, null, {"missing": "different"} ], "foo": {"bar": "baz"} }
JMESPath | Result |
---|---|
people[*].first | [ {"first": "James", "last": "d"}, {"first": "Jacob", "last": "e"}, {"missing": "different"}] |
people[*].first | ["James", "Jacob", "Jayden"] |
Arrayの各オブジェクトから特定の要素を列挙する基本的な使い方となるでしょう。
スライスとリストのプロジェクション
ワイルドカードを使った[*]
はArray全体を投射します。
ワイルドカードの代わりにスライスを利用するとArrayの一部を投射できます。
{ "people": [ {"first": "James", "last": "d"}, {"first": "Jacob", "last": "e"}, {"first": "Jayden", "last": "f"}, {"missing": "different"} ], "foo": {"bar": "baz"} }
JMESPath | Result |
---|---|
people[0:1].first | ["James", "Jacob"] |
people[::-1].first | ["Jayden", "Jacob", "James"] |
スライスされるタイミングに注意しましょう。
{ "people": [ null, {"first": "James", "last": "d"}, {"first": "Jacob", "last": "e"}, {"first": "Jayden", "last": "f"}, {"missing": "different"} ], "foo": {"bar": "baz"} }
JMESPath | Result |
---|---|
people[0:2].first | ["James"] |
この場合、people[0:2]
でスライスされた [null, {"first": "James", "last": "d"}]
に対して、投射が行われることになります。
投射のタイミングでnullが除去されるので、期待される結果を得られていません。
オブジェクトのプロジェクション
*
は、オブジェクトの値のみをArrayに投射します。
キーには興味は無く、各値にのみ興味がある時に有効です。
{ "ops": { "functionA": {"numArgs": 2}, "functionB": {"numArgs": 3}, "functionC": {"variadic": true} } }
JMESPath | Result |
---|---|
ops.* | [1, 2, 3] |
opsオブジェクトの各値がArrayとして抽出されています。
{ "ops": { "functionA": {"numArgs": 2}, "functionB": {"numArgs": 3}, "functionC": {"variadic": true} } }
JMESPath | Result |
---|---|
ops.*.numArgs | [2, 3] |
ops.*.numArgs
のように子要素を指定することができます。
キーが存在しない場合には除外される挙動も同様です。
フラット化のプロジェクション
JMESPathで複雑なJSONにプロジェクションを繰り返していくと、往々にしてネストしたArrayが作られます。
{ "reservations": [ { "instances": [ {"state": "running"}, {"state": "stopped"} ] }, { "instances": [ {"state": "terminated"}, {"state": "runnning"} ] } ] }
JMESPath | Result |
---|---|
reservations[].instances[].state | [["running", "stopped"], ["terminated", "runnning"]] |
ちょっとイケていない結果ですね・・・。
ここでArrayをフラット化するには、[]
を利用します。
[ [ "running", "stopped" ], [ "terminated", "runnning" ] ]
JMESPath | Result |
---|---|
[] | ["running", "stopped", "terminated", "runnning"] |
投射されたArrayに投射を繰り返すイメージが掴めれば、最初のJSONが次のように出力されることが解ると思います。
JMESPath | Result |
---|---|
reservations[].instances[].state | [["running", "stopped"], ["terminated", "runnning"]] |
reservations[].instances[].state[] | ["running", "stopped", "terminated", "runnning"] |
reservations[*].instances[].state | ["running", "stopped", "terminated", "runnning"] |
reservations[].instances[*].state | [["running", "stopped"], ["terminated", "runnning"]] |
実際にクエリを書く場合は、1段階づつ投射すると欲しい結果にたどり着くことができます。
プロジェクション時のフィルタ
[]
の中には様々なフィルタを埋め込むことで、投射するArrayをコントロールできます。
{ "Reservations": [ { "Instances": [ { "State": { "Name": "running" }, "InstanceType": "m3.medium", "InstanceId": "i-001" }, { "State": { "Name": "stopped" }, "InstanceType": "t2.micro", "InstanceId": "i-002" } ] }, { "Instances": [ { "State": { "Name": "terminated" }, "InstanceType": "m3.medium", "InstanceId": "i-003" }, { "State": { "Name": "running" }, "InstanceType": "t2.small", "InstanceId": "i-004" } ] } ] }
インスタンス一覧からrunningのインスタンスIDだけ抽出したい感じです。
JMESPath | Result |
---|---|
Reservations[].Instances[].InstanceId[] | ["i-001", "i-002", "i-003", "i-004"] |
Reservations[*].Instances[?State.Name=='running'].InstanceId[] | ["i-001", "i-004"] |
?State.Name=='running'
でArrayをフィルタリングしています。
最後の[]
はフラット化ですよ。
パイプ
JMESPathのアウトプットはJSONオブジェクト(Array)です。 |(パイプ)を利用すれば、JMESPathの出力を次の式の入力に渡すことができます。
JMESPath | Result |
---|---|
Reservations[*].Instances[?State.Name=='running'].InstanceId[] | [0] | "i-001" |
基本的にはプロジェクションを繰り返していけばいいので、使い所は今の所は見えていません。 なお、可読性は良いです。
マルチセレクト
JMESPathでは複数項目を抽出する場合、[A, B]
といった書式を指定します。
JMESPath | Result |
---|---|
Reservations[*].Instances[?State.Name=='running'].[InstanceId, InstanceType] | [[[ "i-001", "m3.medium" ] ], [["i-004", "t2.small" ]]] |
Reservations[*].Instances[?State.Name=='running'].[InstanceId, InstanceType][] | [[ "i-001", "m3.medium" ] ], [["i-004", "t2.small" ]] |
イイ感じになってきました。
でも、Arrayではなくオブジェクトとして欲しいんじゃ…と思いますね。
[要素, 要素]
は配列を返しますが、 {キー名:要素名, キー名:要素名}
とすることでオブジェクトを作成します。
JMESPath | Result |
---|---|
Reservations[*].Instances[?State.Name=='running'].{Id:InstanceId, Type:InstanceType}[] | [{"Id": "i-001", "Type": "m3.medium" }, { "Id": "i-004", "Type": "t2.small" } ] |
巨大なJSONレスポンスを適度な大きさで使いやすい形に加工できるようになりました。
まとめ
JMESPathは取っつきにくい部分がありますが、プロジェクションの感覚を掴むと一気に使いやすくなります。 大きなJSONオブジェクトを少しずつ求める形に加工していくことができるため、AWSCLIをより使いやすく使えるようになります。
早速、runningのEC2インスタンスについて、インスタンスIDとインスタンスタイプを抽出してみましょう。
aws ec2 describe-instances --query "Reservations[*].Instances[?State.Name=='running'].{Id:InstanceId, Type:InstanceType}[]"