📖

ExcelのPowerQueryでJSONを処理する

2022/08/30に公開

背景

https://zenn.dev/ryuden/articles/1161c6bee032c4

という記事に

https://twitter.com/haxe/status/1563995987571785728

とコメントしたのがきっかけで

https://twitter.com/ryudenx/status/1564064253627486208

ExcelのPowerQueryでは実現が難しいとの指摘がありました。
ということで、PowerQueryでは大変な手間がかかる例を実現したいと思います。

Power Queryでは大変な手間がかかる例

こちらの画像のような表がゴールとなります。

実装

PowerQueryの起動

Excelのメニューの「空のクエリ」から起動できます。

データの取得 - その他のデータソースから - 空のクエリ

本来はGUIで操作できますが、今回はデータソースなどを準備していないため、「詳細エディター」に直接入力します。

詳細エディター

PowerQueryの構文

PowerQueryはM式言語と呼ばれる独自のクエリ言語で記述します。

https://docs.microsoft.com/ja-jp/powerquery-m/

この言語には、リスト・レコード・テーブルといったデータ型や、各種関数が用意されています。今回は

を用いることでREST APIを呼び出し、結果として得られるJSONを解析します。

Googleのルート検索API

Googleでは各種APIが提供されていて、ルート検索APIとしてDirection APIが用意されています。

https://developers.google.com/maps/documentation/directions

この中で、リクエストパラメーターレスポンス仕様も説明されています。

送信すべきリクエスト

Direction APIとして送信すべきURLは

https://maps.googleapis.com/maps/api/directions/json

でパラメーターは

  • origin=相原駅
  • destination=新宿駅
  • transit=transit
  • key=APIキー

のようになるかと思います。
また参照すべき結果は

  • routes
  • 配列の先頭の
  • legs
  • 配列の先頭の
  • duration
  • value

の個所に所要時間(単位:秒)となります。

PowerQueryの関数化

まずはDirection APIのクエリパラメータを作成します。

(発駅, 着駅) =>
let
    parameter = [origin=発駅, destination=着駅, transit="transit", key="APIキー"]
in
    parameter

リクエストを投げます。

(発駅, 着駅) =>
let
    parameter = [origin=発駅, destination=着駅, transit="transit", key="APIキー"],
    response = Web.Contents("https://maps.googleapis.com/maps/api/directions/json", [Query=parameter])
in
    response

JSONを解析して、参照すべき結果を取り出します。

(発駅, 着駅) =>
let
    parameter = [origin=発駅, destination=着駅, transit="transit", key="APIキー"],
    response = Web.Contents("https://maps.googleapis.com/maps/api/directions/json", [Query=parameter]),
    seconds = Json.Document(response)[routes]{0}[legs]{0}[duration][value]
in
    seconds

データ型をdurationにします。

(発駅, 着駅) =>
let
    parameter = [origin=発駅, destination=着駅, transit="transit", key="APIキー"],
    response = Web.Contents("https://maps.googleapis.com/maps/api/directions/json", [Query=parameter]),
    seconds = Json.Document(response)[routes]{0}[legs]{0}[duration][value]
in
    #duration(0, 0, 0, seconds)

クエリ式全体

発駅リストと着駅リストは必要であればテキストファイルなり外部データを参照すればよいのですが、本質的ではないので、直接書き込みます。
クエリ式全体としてはこのような形になります。

let
    所要時間取得 = (発駅, 着駅) =>
    let
        parameter = [origin=発駅, destination=着駅, transit="transit", key="APIキー"],
        response = Web.Contents("https://maps.googleapis.com/maps/api/directions/json", [Query=parameter]),
        seconds = Json.Document(response)[routes]{0}[legs]{0}[duration][value]
    in
        #duration(0, 0, 0, seconds),
    発駅リスト = {"相原駅", "青井駅", "青砥駅"},
    着駅リスト = {"新宿駅", "渋谷駅", "品川駅"},
    発着表 = Table.ExpandListColumn(Table.AddColumn(Table.FromList(発駅リスト, null, {"発駅"}), "着駅", each 着駅リスト), "着駅"),
    所要時間 = Table.AddColumn(発着表, "所要時間", each 所要時間取得([発駅], [着駅])),
    ピボット = Table.Pivot(所要時間, 着駅リスト, "着駅", "所要時間")
in
    ピボット

クエリ式全体

結果確認

適用したステップから「発着表」を選択すると

発着表

総当たりの表が構築されていることが確認でき、続く「所要時間」では

所要時間

時間列が追加されていることがわかります。最終的な「ピボット」はもちろん

ピボット

行列分解されてマトリックスが所要時間で埋められていることが確認できます。

Excelへの取り込み

「閉じて読み込む」を押すと

閉じて読み込む

無事、Excelに取り込まれました。

Excelに取り込まれた

所要時間はもちろん時間情報として認識されているので、Excel上でも書式設定してやれば

だいたいの形になったでしょうか。

最後に

ExcelとしてはPowerQueryから結果を受け取っているだけですので、クエリの更新を行わない限りは再実行される心配はありません。

問題はAPIキーを埋め込んでしまっていることでしょうか。ここもやろうと思えば対策はありますが、またの機会に。

追記

Distance Matrix APIを使うと、さらに効率よくマトリックスで所要時間を得ることができます。

https://developers.google.com/maps/documentation/distance-matrix

ただし、返されたJSONを解析して表形式にするのは、元記事でも指摘されているように割と面倒です。(あとAPIドキュメントが間違っている罠もありそうです。)

let
    発駅リスト = {"相原駅", "青井駅", "青砥駅"},
    着駅リスト = {"新宿駅", "渋谷駅", "品川駅"},
    origins = Text.Combine(発駅リスト, "|"),
    destinations = Text.Combine(着駅リスト, "|"),
    Query = [origins=origins, destinations=destinations, mode="TRANSIT", language="ja-JP", key="APIキー"],
    response = Web.Contents("https://maps.googleapis.com/maps/api/distancematrix/json", [Query=Query]),
    json = Json.Document(response),
    durations = List.Transform(json[rows], each List.Transform([elements], each #duration(0, 0, 0, [duration][value]))), 
    records = List.Transform(durations, each Record.FromList(_, 着駅リスト)),
    rows = Table.FromRows(List.Zip({発駅リスト, records}), {"発駅", "着駅リスト"}),
    table = Table.ExpandRecordColumn(rows, "着駅リスト", 着駅リスト)
in
    table

Discussion