13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Power BI] Power Query の Web.Contents 関数でREST APIを使う(仮)

Last updated at Posted at 2022-06-03

※この記事は、"PBIJP Power Query 秘密特訓「虎の穴」炎の復活編 #14 (2022.05.21)で使用した記事です。
image.png

後日再構成して公開する予定なので、タイトルが(仮)になってます。

※元ネタ
https://blog.crossjoin.co.uk/2016/08/16/using-the-relativepath-and-query-options-with-web-contents-in-power-query-and-power-bi-m-code/

Web.Contents構文

Web.Contents(
    url as text, 
    optional options as nullable record
) as binary

1.RelativePathオプションとQueryオプション

Power QueryのWeb.Contents()関数を使うと、Webで公開されているデータを取得することができます。例えば、イギリス政府のオープンデータをAPIを使って取得するには以下のようなURLを使います。

Data UK
    Source =
        Json.Document(
            Web.Contents("https://data.gov.uk/api/3/action/package_search?q=cows&rows=20")
        ),

 上記では、全て1行のURLに記述していますが、以下のようにBase URLを https://data.gov.uk/api とし、RelativePathオプションとQueryオプションを使って記述することができます。

Data USA with web.contents options
    Source =
        Json.Document(
            Web.Contents(
                "https://data.gov.uk/api",
                [
                    RelativePath = "/3/action/package_search",
                    Query = [
                        q = "cows",
                        rows = "20"
                    ]
                ]
            )
        ),

 この記述は、1行で記述するより読みやすく、メンテナンス性に優れます。また、URLパラメータはエンコードされて渡されるため、SQLインジェクションの危険性が減ります。
 特に注意したいのは、URLパラメータの値を変化させて取得したい場合、最初の方法ではPower BI サービスでは以下の表な表示が出て取得できません。

動的データソース
    RowCount = List.Last({"1", "10", "20"}),
    Source =
        Json.Document(
            Web.Contents("https://data.gov.uk/api/3/action/package_search?q=cows&rows=" & RowCount)
        ),

image.png

 これを、RelativePathやQueryオプションに書き換えるとエラーにならずに取得することができます。

オプションで書き換える
    RowCount = List.Last({"1", "10", "20"}),
    Source =
        Json.Document(
            Web.Contents(
                "https://data.gov.uk/api",
                [
                    RelativePath = "/3/action/package_search",
                    Query = [
                        q = "cows",
                        rows = RowCount
                    ]

                ]
            )
        ),

データソースのアクセス許可

 なぜURLを1行で書くと、Power BIサービスで使えないのでしょうか。

 理由は、データソースに対するアクセス許可の設定にあります。1行でURLを記述した場合、アクセス許可とソースの設定は以下のようになります。

image.png

image.png

 一方、オプションを使った場合は以下のようになり、ソースの変更ボタンはグレーアウトして選択できません。

image.png

 つまり、1行に書いた場合、URL全体に対して1つのアクセス許可を与えますが、これが変動する場合、明示的なアクセス許可を与えることができないため、動的データソースのエラーが発生してしまうのです。しかし、オプションを使った場合、ベースURLに対してのアクセス許可だけで済みます。

2.Connpassのイベント一覧を取得

connpassのイベントリストを取得するAPIは、こちらにあります。

 series_idとstartにパラメータを割り当ててデータを取得するには、以下のように書きます。
image.png

EventTable
    Source = 
        Json.Document(
            Web.Contents(
                "https://connpass.com/api/v1/event/",
                [
                    Query = [
                        series_id = Param_SeriesId,
                        start = Param_Start,
                        count = "100"
                    ]
                ]
            )
        ),

 これに続けて取得したデータからテーブルを作成する処理を記述していきます。(全リスト

 しかし、一度に最大100件までしか取得できないので、100件を超えるイベントを取得するには、数回に分ける必要があります。作成したクエリを右クリックし、コンテキストメニューから関数を作成します。

image.png

 この関数とList.Generateを使って、データを100件ずつ取得して結合するクエリを作成します。

QueryAndCombine
let
    Source = 
        List.Generate(
            () => [
                Start = 1,
                Result = try Function.InvokeAfter(
                    ()=> GetEventTable(Number.ToText(Start), Param_SeriesId),
                    #duration(0,0,0,0.2)       // 0.2秒遅延
                 ) otherwise null
            ],
            each [Result] <> null,           // 取得でエラーが発生したら終了
            each [
                Start = [Start] + 100,       // [Start]には前の値が入っているので、100を加えておく
                Result = 
                    try Function.InvokeAfter(
                        // 開始位置を[Start]から100件後ろに移して取得 
                        ()=> GetEventTable(Number.ToText([Start]+100), Param_SeriesId),
                        #duration(0,0,0,0.2)   // 0.2秒遅延
                     ) otherwise null
            ],
            each [Result]
        ),
    Combine = Table.Combine(Source)
in
    Combine

 最初はstartの値が1で取得し、次に100を加えて101で取得します。取得できる件数を超えるとエラーが発生し、Resultにはnullが入り終了となります。データ取得するGetEventTable関数は、Function.InvokeAfterで遅延を挟んでいます。
(参考 ページ分割されているWebでページ数がわからないデータを取得する)

 取得したデータを使って、ビジュアルを作成します。

image.png

3.Youtubeの動画リストを取得

 YoutubeのAPIは、「Youtube API Search:List」を使います。
 YoutubeのAPIを利用するには、APIキーが必要なので、取得してパラメータにセットします。(see @shinkai_【Youtube】APIキーの取得手順(2021/04/08時点のキャプチャ)」)
image.png

 他にチャンネルIDを調べておきます。
image.png

 1度に取得できる件数は50件なので、それを超える場合は2回目以降の検索にpageTokenが必要となります。パラメータの設定はRequiredを外し、TypeはAnyにして、1回目の検索はnullで渡すようにしておきます。
(参考 「省略可能なパラメータ」)
image.png

 以上2つのパラメータを使って動画リストを取得するクエリを書きます。APIキーは、 ApiKeyNamekey と名前を指定しておき、値は資格情報で設定します。1回目と2回目以降のアクセス方法を条件式で分けています。(全リスト

VideoTable(ApiKeyNameを使い、1回目2回目の式を分ける)
let
    Source = 
        Json.Document(
            if (Param_PageToken = null) then
                // 1番最初の検索
                Web.Contents(
                    "https://www.googleapis.com/youtube/v3/search",
                    [
                        ApiKeyName = "key",
                        Query = [
                            part = "id,snippet",
                            type = "video",
                            maxResults = "50",
                            channelId = Param_ChannelId
                        ]
                    ]
                )
            else
                // 2番目以降の検索
                Web.Contents(
                    "https://www.googleapis.com/youtube/v3/search",
                    [
                        ApiKeyName = "key",
                        Query = [
                            part = "id,snippet",
                            type = "video",
                            maxResults = "50",
                            channelId = Param_ChannelId,
                            pageToken = Param_PageToken
                        ]
                    ]
                )
        ),
・・・以下略・・・

 この状態では、まだApiキーが設定されていないので「資格情報の編集」をします。
image.png
image.png
 「Web API」を選択し、キーの項目にAPIキーを入力して接続すると、以下のようなテーブルが作成されます。
image.png

これで問題なく動きますが、1回目と2回目の2つの式に分けないで記述する方法があります。

VideoTable(ApiKeyNameを使い、1回目2回目の式を分けない)
let
    Source = 
        Json.Document(
                Web.Contents(
                    "https://www.googleapis.com/youtube/v3/search",
                    [
                        ApiKeyName = "key",
                        Query = [
                            part = "id,snippet",
                            type = "video",
                            maxResults = "50",
                            channelId = Param_ChannelId,
                            pageToken = Param_PageToken ?? {}
                        ]
                    ]
                )
        ),

 この方法は、"CHRIS WEBB'S BI BLOG"で紹介されています。[Query=[postId={"1","2"}]] と記述することで同じパラメータを2つ生成される一方、[Query=[postId={}]] の記述ではパラメータが生成されないという動作を利用しています。

 これを関数に変換します。
image.png

 この関数とList.Generateを使って動画リストを作成するクエリは以下のように書きました。pageTokenは、1回目はnullで渡し、2回目以降は**[Result]{0}[nextPageToken]** で前回の検索結果の1行目から取得するようにしています。最後のページの検索結果には "-" がセットされ、その次の検索はエラーとなって終了します。また、万一無限ループに入ってしまわないよう、検索回数の制限を付けています。

 YoutubeのAPIは、1つのキーに対して1日に使用できるアクセス数の制限があり、それを超えるとエラーが返されます。また、アクセス頻度については、Googleアナリティクスでは1秒間に10件のリクエスト数となっていましたので、間に0.2秒の遅延を挟むようにしました。
 ただ、この方式で取得できる動画は、501件までしか取得できませんでした。

Youtube(ApiKeyNameを使う場合)
let
    Source = 
        List.Generate(
            // initial as function
            () => [
                Count = 0, 
                Result = 
                    try 
                        Function.InvokeAfter(
                            ()=> GetVideoTable(Param_ChannelId, null),
                            #duration(0,0,0,0.2)       // 0.2秒遅延
                        )
                    otherwise null
            ],
            // condition as function
            // データ取得がエラーになったら終了。安全のため最大10回までに制限。
            each [Result] <> null and [Count] < 10,
            // next as function
            each [
                Count = [Count] + 1,
                Result = 
                    try 
                        Function.InvokeAfter(
                            ()=> GetVideoTable(Param_ChannelId, [Result]{0}[nextPageToken]),
                            #duration(0,0,0,0.2)       // 0.2秒遅延
                     ) otherwise null 
            ],
            // optional selector as nullable function
            each [Result]
        ),
    // 全ての検索結果をまとめてテーブルを作成
    Combine = Table.Combine(Source)
in
    Combine
    Combine

Power BIサービスでAPIキーは

 ApiKeyNameを使う方法は、APIキーを安全に取り扱うことができます。しかし、Power BIサービスに発行した後、APIキーを設定しようとすると、選択肢がありません。その代わり、OAuth2が選択肢として出てきます。
image.png
 Power BIサービスでは、ApiKeyNameをサポートしていないようです。

 結局、Power BIサービスに発行して使うには、APIキーをパラメータに設定し、以下のように書き換えました。

VideoTable(APIキーをパラメータに設定して使う場合)
    Source = 
        Json.Document(
            if (Param_PageToken = null) then
                Web.Contents(
                    "https://www.googleapis.com/youtube/v3/search",
                    [
                        // ApiKeyName = "key",
                        Query = [
                            key = Param_Key,
                            part = "id,snippet",
                            type = "video",
                            maxResults = "50",
                            channelId = Param_ChannelId,
                            pageToken = Param_PageToken ?? {}
                        ]
                    ]
                )
        ),

4.OAuth2認証でYoutubeの動画リストを取得する

 OAuth2認証でアクセスするには、アクセストークンを取得しなければなりません。取得出来たら、keyの代わりにaccess_tokenを使ってデータを取得できるようになります。アクセストークンはPostmanで取得しておくこともできますが30分から60分くらいで有効期限が切れますので、そのまま使い続けることはできません。
 Power BIからOAuth2認証を使用するためには、カスタムコネクタを使う必要がありそうです。検索するとカスタムコネクタを使わず、Power Queryで記述する方法もありますが、安全でなく、信頼できません。また、カスタムコネクタを使用した場合は、Power BIサービスで利用するにはオンプレミスゲートウエイが必要となります。
 では、先ほどPower BIサービスで表示されたOAuth2は何かというと、Power BI DesktopでSharePointなどのMicrosoftのAzure Active Directoryの認証を行った場合、Power BIサービスではOAuth2認証が必要になるのです。

5.Web.Contentsのオプション

 Web.Contentsで使用するオプションには、以下のようなものがあります。

Option Description
Query クエリ パラメーターをプログラムで URL に追加します。エスケープについて考える必要はありません。
ApiKeyName ターゲット サイトが API キーに対応している場合、このパラメーターを使用すると、URL で使用する必要があるキー パラメーターの名前 (値ではない) を指定できます。 実際のキー値は、資格情報で指定されます。
Headers この値をレコードとして指定すると、HTTP 要求に追加のヘッダーが指定されます。
Timeout この値を期間として指定すると、HTTP 要求のタイムアウトが変更されます。 既定値は 100 秒です。
ExcludedFromCacheKey この値をリストとして指定すると、これらの HTTP ヘッダー キーが、データをキャッシュする計算から除外されます。
IsRetry この論理値を true に指定すると、データをフェッチするときにキャッシュ内の既存の応答が無視されます。
ManualStatusHandling この値をリストとして指定すると、応答にこれらのいずれかの状態コードが含まれる HTTP 要求に対するビルトイン処理が回避されます。
RelativePath この値をテキストとして指定すると、要求を行う前にこれがベース URL に追加されます。
Content この値を指定すると、POST のコンテンツにオプションの値が使用され、Web 要求が GET から POST に変更されます。

6.Webアクセスを確認する方法

 Fiddlerというツールを使用するとhtmlパケットの中身を確認することができますが、新たなツールの使い方を覚える必要があります。

 先ほどのアクセスを見てみると、最後のアクセスはURLのpageTokenは"-"になり、レスポンスステータスコード400 Bad Requestでエラーとなっていることがわかります。

image.png

 Power BIの診断機能でも確認することができます。

image.png

全リスト

EventTable

EventTable
let
    Source = 
        Json.Document(
            Web.Contents(
                "https://connpass.com/api/v1/event/",
                [
                    Query = [
                        series_id = Param_SeriesId,
                        start = Param_Start,
                        count = "100"
                    ]
                ]
            )
        ),
    EventsList = Source[events],
    ConvertedToTable = 
        Table.FromList(
            EventsList, 
            Splitter.SplitByNothing(), 
            null, 
            null, 
            ExtraValues.Error
        ),
    ExpandedColumn = 
        Table.ExpandRecordColumn(
            ConvertedToTable, 
            "Column1", 
            {
                "event_id", 
                "title", 
                "catch", 
                "description", 
                "event_url", 
                "started_at", 
                "ended_at", 
                "hash_tag", 
                "accepted", 
                "waiting", 
                "place"
            }, 
            {
                "event_id", 
                "title", 
                "catch", 
                "description", 
                "event_url", 
                "started_at", 
                "ended_at", 
                "hash_tag", 
                "accepted", 
                "waiting", 
                "place"
            }
        ),
    AddedDateColumn = Table.AddColumn(ExpandedColumn, "date", each Text.Start([started_at],10)),
    AddedStartTimeColumn = Table.AddColumn(AddedDateColumn, "start_time", each Text.Middle([started_at], 11, 5)),
    AddedEndTimeColumn = Table.AddColumn(AddedStartTimeColumn, "end_time", each Text.Middle([ended_at], 11, 5)),
    RemovedColumns = Table.RemoveColumns(AddedEndTimeColumn,{"started_at", "ended_at"}),
    ChangedType = Table.TransformColumnTypes(RemovedColumns,{{"event_id", type text}, {"title", type text}, {"catch", type text}, {"description", type text}, {"event_url", type text}, {"hash_tag", type text}, {"accepted", Int64.Type}, {"waiting", Int64.Type}, {"place", type text}, {"date", type date}, {"start_time", type time}, {"end_time", type time}})
in
    ChangedType

VideoTable

 以下のクエリを関数に変換して Youtube クエリから呼び出す。

VideoTable(Apiキーをパラメータに設定して使う場合)
let
    Source = 
        Json.Document(
            Web.Contents(
                "https://www.googleapis.com/youtube/v3/search",
                [
                    // ApiKeyName = "key",
                    Query = [
                        key = Param_Key,
                        part = "id,snippet",
                        type = "video",
                        maxResults = "50",
                        channelId = Param_ChannelId,
                        pageToken = Param_PageToken ?? {}
                    ]
                ]
            )
        ),
    // nextPageTokenの取得
    // 最終行が取得されnextPageTokenがない場合は"-"を入れる
    NextPageToken = try Source[nextPageToken] otherwise "-",
    Items = Source[items],
    ConvertedToTable = Table.FromList(Items, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
    ExpandedColumn = Table.ExpandRecordColumn(ConvertedToTable, "Column1", {"id", "snippet"}, {"id", "snippet"}),
    ExpandedId = Table.ExpandRecordColumn(ExpandedColumn, "id", {"videoId"}, {"videoId"}),
    ExpandedSnippet = Table.ExpandRecordColumn(ExpandedId, "snippet", {"publishedAt", "title", "description", "thumbnails"}, {"publishedAt", "title", "description", "thumbnails"}),
    ExpandedThumbnails = Table.ExpandRecordColumn(ExpandedSnippet, "thumbnails", {"medium"}, {"medium"}),
    ExpandedMedium = Table.ExpandRecordColumn(ExpandedThumbnails, "medium", {"url"}, {"url"}),
    ChangedType = Table.TransformColumnTypes(ExpandedMedium,{{"videoId", type text}, {"publishedAt", type datetimezone}, {"title", type text}, {"description", type text}, {"url", type text}}),
    // videoIdからYoutubeのURLを作成
    TransferVideoUrl = 
        Table.TransformColumns(
            ChangedType,
            {{
                "videoId",
                each "https://youtu.be/" & _                
            }}
        ),
    // UTCからJSTに変換して、タイムゾーン情報を削除
    AddedPublishDate = Table.AddColumn(
        TransferVideoUrl, 
        "登録日", 
        each DateTimeZone.RemoveZone(DateTimeZone.SwitchZone([publishedAt], 9)),
        type datetime
    ),
    // 列を追加してnextPageTokenをセットする
    AddedNextPageToken = 
        Table.AddColumn(
            AddedPublishDate,
            "nextPageToken",
            each NextPageToken,
            type text
        ),
    RemovedColumns = Table.RemoveColumns(AddedNextPageToken,{"publishedAt"}),
    RenamedColumns = Table.RenameColumns(RemovedColumns,{{"title", "タイトル"}, {"description", "内容"}})
in
    RenamedColumns as table

Youtube

(Apiキーをパラメータに設定して使う場合)
let
    Source = 
        List.Generate(
            // initial as function
            () => [
                Count = 0, 
                Result = 
                    try 
                        Function.InvokeAfter(
                            // ApiKeyNameを使う場合
                            //()=> GetVideoTable(Param_ChannelId, null),
                            // APIキーをパラメータに設定して使う場合
                            ()=> GetVideoTable(Param_Key, Param_ChannelId, null),

                            #duration(0,0,0,1)       // 1秒遅延
                        )
                    otherwise null
            ],
            // condition as function
            // データ取得がエラーになったら終了。安全のため最大10回までに制限。
            each [Result] <> null and [Count] < 10,
            // next as function
            each [
                Count = [Count] + 1,
                Result = 
                    try 
                        Function.InvokeAfter(
                            // ApiKeyNameを使う場合
                            //()=> GetVideoTable(Param_ChannelId, [Result]{0}[nextPageToken]),
                            // APIキーをパラメータに設定して使う場合
                            ()=> GetVideoTable(Param_Key, Param_ChannelId, [Result]{0}[nextPageToken]),

                            #duration(0,0,0,1)       // 1秒遅延
                     ) otherwise null 
            ],
            // optional selector as nullable function
            each [Result]
        ),
    // 全ての検索結果をまとめてテーブルを作成
    Combine = Table.Combine(Source)
in
    Combine

関連記事

13
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?