MoreBeerMorePower

Power Platform中心だけど、ノーコード/ローコード系を書いてます。

ページネーションのあるAPIで全件取得して1つの配列にする

以前 Power BI を使った場合に考えた、『ページネーションのあるAPIで全件取得する』という課題を Power Automate/Logic Apps でやってみました。

ページングがある一覧取得系APIをPower BI で使うときのメモ - MoreBeerMorePower

Power Automateではこの手の処理を行う場合、「Apply to each/For each」、「Do until」のいずれかのループ処理を利用してページ数分だけAPIを実行して全件取得を行います。

その際にポイントになるのは以下2点かと思います。

  1. ページ数をどうやって定義するか
  2. 一つの配列にするにはどうするか

1については、取得対象の総件数を1回のリクエストで取れる件数で割って、切り上げすればページ数が得られます。これをPower Automateでどうするかです。(単純に式の書き方の問題)

2は少し難しいかもしれません。1回のリクエストで返ってきた結果は配列になっています。それがページ数分あるときにどうやってもとの順序性を保って 1つの配列にするのかというのはそれほど自明ではありません。

今回は、Hot pepperのAPIを想定しながら、ページネーションのあるAPIで全件取得する方法について2つ紹介します。

[準備] ページ数の定義

たいていの場合、タイムラインの取得のように件数が激しく変わるようなものでなければ、検索操作 または その他の操作で取得したい対象の総件数が得られます。

今回例にするHot pepperのレストラン検索では、レスポンスにresults_available というキーで総件数が返されます。

f:id:mofumofu_dance:20201110133934p:plain

一方の「1回のリクエストの取得上限」ですが、これはAPI仕様に記載されているかと思います。今回のAPIでは100件です。

ページネーションの仕様は様々ですが、とにかくまずは『何回リクエストを行えばいいのか』 を決める必要があります。総件数1400件であれば14回、1210件であれば13回、85件であれば1回という具合です。

この回数を求めるためには以下のような式をPower Automateで書きます。

add(1,int(split(string(div(variables('total_count'),100.0)),'.')?[0]))

総件数は数値型でtotal_countという変数に入っていると仮定しています。

ページ数は、まずこれを100 (1回の取得上限)で割り、文字列化。できた文字列を小数点で分割して整数部分を取り出す。そこに1を足すことで得られます。

これでページ数 = リクエスト回数 は求められました。あとは「Apply to each/For each」の入力にrange(0, 上の結果)を入れてループさせるだけです。

ループの結果を1つの配列にする方法

ページ数は求められました。「Apply to each」のアクションも追加しました。基本的にはまずその中でAPIを実行して結果を取得します。

問題はそのあとの処理で、ここがいくつかバリエーションがあります。

f:id:mofumofu_dance:20201110140721p:plain

梅コース : 簡単だが時間がかかる

まずは簡単な方法からご紹介します。この方法はフローを作るのが簡単な代わりに、ページ数が多い場合に遅くなります。 (並列化の多重度が1にする必要があるため)

この方法では、配列型の変数を定義して、その変数に取得結果を足していきます。

取得結果が単一のレコードであれば「配列変数への追加」が使えますが、API取得結果もまた配列なので、先に配列同士の合成を行います。 「作成」アクションを追加して以下のような式をセットします。

union(variables('result_array'), API取得結果)

API取得結果部分はそれぞれ実行するものに応じて変更してください。

この「作成」アクションの結果で配列変数を更新すれば完了です。

f:id:mofumofu_dance:20201110142620p:plain

注意点ですが、この方法で実行する場合には並列化の多重度 (コンカレンシー制御 の多重度) を 1 にしてください。

こうしないと実行結果の順序性が失われます。

参考までに、実行時間は以下の通りです。1654件 (17ページ) で 40秒程度でした。

f:id:mofumofu_dance:20201110143516p:plain

松コース : 少し複雑だが高速

この方法はxml, xpath, string, jsonなどの関数を多めに使うので少し複雑ですが、代わりに並列化の多重度を上げられるというメリットがあります。

同じ件数 (1654件) でフローを実行した結果は以下の通りです。フロー全体としても4秒!

f:id:mofumofu_dance:20201110144002p:plain

少し方法が複雑なので、フローの中を見る前に方法の概要から書いていきます。

ステップ1. JSON配列を文字列配列に変換

最終的にはxpathを使って全件をくっつけた配列を得るのですが、xpathで扱いやすい形にするために配列の中を文字列にしておきます。

f:id:mofumofu_dance:20201110150221p:plain

ステップ2. 1つの配列にまとめる

文字列配列に適当なキー (ここでは"val") をつけて、1つの配列にします。この段階ではまだ、目的の結果にはなっていません。ページ数分のレコードがある入れ子の配列になっているだけです。

f:id:mofumofu_dance:20201110150551p:plain

ステップ3. xml化する

ステップ2.で作った配列をxml化するために、さらに

{
   "root":
   {
       "items":
       [2.の配列]
   }
}

と、2階層作って入れ子にします。こうすると Power Automate のxml関数でJSONからxmlに変換することができます。

f:id:mofumofu_dance:20201110151957p:plain

4. xpathの評価

xmlが出来たら、それに対してxpathで要素を抽出します。 上の例では

/root/items/val/text()

このようなxpathを使うことで、<val>~</val>に含まれているすべての文字列 (値) が列挙されます。

f:id:mofumofu_dance:20201110152332p:plain

大まかにはこの4ステップでページに分かれている配列から、フラットな1つの配列が得られます。

フロー作成

フローの全体と、各ステップの関係は下図の通りです。

f:id:mofumofu_dance:20201110153221p:plain

ステップ1 の文字列化には「選択」アクションを使っています。

入力はAPIの結果得られる配列を、マップの部分には

string(item())

を入れるだけです。これで、配列内のJSONオブジェクトが文字列化されます。

f:id:mofumofu_dance:20201110154152p:plain

ステップ2は Pieter's methodを使います。 Apply to eachの中で、「作成」アクションを追加し以下を入力します。

{
  "val" : ステップ1の結果
}

Apply to eachの外に出て直後、「作成」アクションを追加し、式にoutputs('Compose_5') (Apply to eachで追加した「作成」の結果) を入力します。

これによって、まずは入れ子になった1つの配列ができます。

f:id:mofumofu_dance:20201110154501p:plain

ステップ3 は概要にも書いた通り、さらに2段階入れ子にして、xml()関数でxml化しています。

f:id:mofumofu_dance:20201110155110p:plain

ステップ4 いよいよxpathの評価です。最終的な配列を得るために「選択」アクションを追加します。

入力には、前ステップで作成したxmlを使って、xpath()関数を書きます。

xpath(outputs('Compose_4'),'/root/items/val/text()')

Map部分については今度はjson()関数でJSONオブジェクトに戻します。

json(item())

f:id:mofumofu_dance:20201110155418p:plain

これで目的の配列が生成されました。

f:id:mofumofu_dance:20201110155721p:plain

おわり

ページネーションのあるAPIで全件取得して1つの配列にまとめる方法を2パターン紹介しました。

  1. 配列変数を使って順次 union していく方法
  2. xpathで入れ子を解く方法

それぞれ複雑さや速度の面でメリデメがあると思うので、必要とされるシーンに合わせて選択できるのが良いかと思います。