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

**※この記事は、<ins>["PBIJP Power Query 秘密特訓「虎の穴」炎の復活編 #14 (2022.05.21)](https://powerbi.connpass.com/event/247406/)</ins>で使用した記事です。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/05885c28-eca5-7d5e-b06a-64fe7fb00862.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を使います。

```text: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オプションを使って記述することができます。

```text: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"
                    ]
                ]
            )
        ),
```

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

```text:動的データソース
    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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/ac75448f-c996-0737-8cfe-d6e6f599379d.png)

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

```text:オプションで書き換える
    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を１行で書くと、Power BIサービスで使えないのでしょうか。

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/dfafdc76-1034-4b88-283c-cdc0717fdf4a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/0235120b-043d-87b4-f7ef-0466afe2daf5.png)


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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/63b47671-37cb-d851-356e-2387ae9472da.png)

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

# 2.Connpassのイベント一覧を取得
 connpassのイベントリストを取得するAPIは、<ins>[こちら](https://connpass.com/about/api/)</ins>にあります。

　series_idとstartにパラメータを割り当ててデータを取得するには、以下のように書きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/8be85421-31ec-8d4c-73d2-ec24dee611d9.png)

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

　これに続けて取得したデータからテーブルを作成する処理を記述していきます。（<ins>[全リスト](#eventtable)</ins>）

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/b1297aaf-eb09-847b-e0ff-7baf7358d992.png)

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

```text: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]には前の値が入っているので、１００を加えておく
                Result = 
                    try Function.InvokeAfter(
                        // 開始位置を[Start]から１００件後ろに移して取得 
                        ()=> 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で遅延を挟んでいます。
(参考 <ins>「[ページ分割されているWebでページ数がわからないデータを取得する](https://qiita.com/spumoni/items/2832b63fa627b20a5de2#4-%E3%83%9A%E3%83%BC%E3%82%B8%E5%88%86%E5%89%B2%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8Bweb%E3%81%A7%E3%83%9A%E3%83%BC%E3%82%B8%E6%95%B0%E3%81%8C%E3%82%8F%E3%81%8B%E3%82%89%E3%81%AA%E3%81%84%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B)」</ins>)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/8224785d-464b-7577-29d8-1e4f9787abe4.png)

# 3.Youtubeの動画リストを取得
　YoutubeのAPIは、「<ins>[Youtube API Search:List](https://developers.google.com/youtube/v3/docs/search/list?hl=ja)</ins>」を使います。
　YoutubeのAPIを利用するには、APIキーが必要なので、取得してパラメータにセットします。(see @shinkai_「<ins>[【Youtube】APIキーの取得手順（2021/04/08時点のキャプチャ）](https://qiita.com/shinkai_/items/10a400c25de270cb02e4)</ins>」)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/82125070-2120-af3a-7bbe-3948b942f794.png)

　他にチャンネルIDを調べておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/6d7fed3f-49c0-8e20-c57d-f059deee249a.png)

　１度に取得できる件数は５０件なので、それを超える場合は２回目以降の検索にpageTokenが必要となります。パラメータの設定はRequiredを外し、TypeはAnyにして、１回目の検索はnullで渡すようにしておきます。
(参考 「<ins>[省略可能なパラメータ](https://qiita.com/spumoni/items/6841aec922c02ee92a2f#3-%E7%9C%81%E7%95%A5%E5%8F%AF%E8%83%BD%E3%81%AA%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF)</ins>」)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/6f788d86-7a03-9522-058a-a9cb5418a37d.png)

　以上２つのパラメータを使って動画リストを取得するクエリを書きます。APIキーは、 **ApiKeyName** で **key** と名前を指定しておき、値は資格情報で設定します。１回目と２回目以降のアクセス方法を条件式で分けています。（<ins>[全リスト](https://qiita.com/drafts#videotable)</ins>）

```text:VideoTable(ApiKeyNameを使い、1回目2回目の式を分ける)
let
    Source = 
        Json.Document(
            if (Param_PageToken = null) then
                // １番最初の検索
                Web.Contents(
                    "https://www.googleapis.com/youtube/v3/search",
                    [
                        ApiKeyName = "key",
                        Query = [
                            part = "id,snippet",
                            type = "video",
                            maxResults = "50",
                            channelId = Param_ChannelId
                        ]
                    ]
                )
            else
                // ２番目以降の検索
                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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/986f3ccc-c80e-b2a0-cdac-37e0503fdbec.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/72c43c88-7029-a74f-0e73-02312b89b059.png)
　「Web API」を選択し、キーの項目にAPIキーを入力して接続すると、以下のようなテーブルが作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/722824e3-e779-06ea-e105-7331ba8d3c36.png)

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

```text: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 ?? {}
                        ]
                    ]
                )
        ),
```

　この方法は、<ins>["CHRIS WEBB'S BI BLOG"](https://blog.crossjoin.co.uk/2021/01/10/handling-multiple-url-query-parameters-with-the-same-name-using-web-contents-in-power-query-power-bi/)</ins>で紹介されています。**[Query=[postId={"1","2"}]]** と記述することで同じパラメータを2つ生成される一方、**[Query=[postId={}]]** の記述ではパラメータが生成されないという動作を利用しています。

　これを関数に変換します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/2ebfea94-e60e-426e-36a1-f1e6733ec850.png)

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

　YoutubeのAPIは、1つのキーに対して１日に使用できるアクセス数の制限があり、それを超えるとエラーが返されます。また、アクセス頻度については、[Googleアナリティクス](https://developers.google.com/analytics/devguides/reporting/mcf/v3/limits-quotas?hl=ja#:~:text=%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E3%81%A7%E3%81%AF%E3%80%81%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%94%E3%81%A8%E3%81%AB,%E3%81%AB%E5%88%B6%E9%99%90%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%E3%80%82)では１秒間に１０件のリクエスト数となっていましたので、間に0.2秒の遅延を挟むようにしました。
　ただ、この方式で取得できる動画は、５０１件までしか取得できませんでした。

```text: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
            // データ取得がエラーになったら終了。安全のため最大１０回までに制限。
            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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/0e28da7f-3a5d-81ff-3ca9-00a32d62d152.png)
　Power BIサービスでは、ApiKeyNameをサポートしていないようです。

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

```text: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で取得しておくこともできますが３０分から６０分くらいで有効期限が切れますので、そのまま使い続けることはできません。
　Power BIからOAuth2認証を使用するためには、カスタムコネクタを使う必要がありそうです。検索するとカスタムコネクタを使わず、Power Queryで記述する方法もありますが、安全でなく、信頼できません。また、カスタムコネクタを使用した場合は、Power BIサービスで利用するにはオンプレミスゲートウエイが必要となります。
　では、先ほどPower BIサービスで表示されたOAuth2は何かというと、Power BI DesktopでSharePointなどのMicrosoftのAzure Active Directoryの認証を行った場合、Power BIサービスではOAuth2認証が必要になるのです。

https://blog.crossjoin.co.uk/2021/08/29/connecting-to-rest-apis-with-oauth2-authentication-in-power-query-power-bi/

https://docs.microsoft.com/ja-jp/power-query/handlingauthentication

https://docs.microsoft.com/ja-jp/power-query/samples/github/readme

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

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

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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/85e2a4c6-f627-191f-de33-92efe45ec41f.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/45fe20ec-ad8b-7151-e1e4-78b8a852a289.png)

https://blog.crossjoin.co.uk/2019/11/17/troubleshooting-web-service-refresh-problems-in-power-bi-with-the-power-query-diagnostics-feature/

# 全リスト
## EventTable
```text: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 クエリから呼び出す。

```text: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

```text:(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
            // データ取得がエラーになったら終了。安全のため最大１０回までに制限。
            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
```

# 関連記事

- [Mから始めよう #5 〜Power BIでWebコンテンツの利用と個人用ゲートウェイ〜](https://qiita.com/spumoni/items/f66bb43f0c3f13308d79)
- [[Power BI] Power Query の Web.Contents 関数でREST APIを使う](https://qiita.com/spumoni/items/419ffd1f07947a07cad0)
- [(Power BI) Mから始めよう #8 〜クエリ・パラメータの各種使い方](https://qiita.com/spumoni/items/4badf29d0b3e170fa15b#b%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E9%96%A2%E6%95%B0%E3%81%A8url%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%A7web%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0)
- [ページ分割されているWebでページ数がわからないデータを取得する](https://qiita.com/spumoni/items/2832b63fa627b20a5de2#4-%E3%83%9A%E3%83%BC%E3%82%B8%E5%88%86%E5%89%B2%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8Bweb%E3%81%A7%E3%83%9A%E3%83%BC%E3%82%B8%E6%95%B0%E3%81%8C%E3%82%8F%E3%81%8B%E3%82%89%E3%81%AA%E3%81%84%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B)
- [(Power BI) Mから始めよう #10 エラー処理とHTTPステータスコード](https://qiita.com/spumoni/items/cd5843e0104fed9b4874)
- [[Power BI] Power QueryでCSSセレクタを使ってWebページからデータを取得する](https://qiita.com/spumoni/items/cad005c1deaa522531d8)