📕

AWS SDKとGoogle Cloud SDKにおけるページネーションの違い

2022/08/05に公開

概要

ストレージ内のファイルを列挙する等の、結果が膨大かもしれない処理では、結果を小分けにして返してくれるページネーション処理が用いられます。AWSとGoogle CloudのSDKでは、その作りが逆と言えます。まとめておきます。

  • AWS SDKは、素朴に使うと(低級APIでは)ページネーション無し
  • Google Cloud SDKは、既定でページネーション有り

本記事の説明ではC#とPythonを使いました。全部は把握していないのですがおそらく、低級なAPIを使う限りは言語問わず同じ結論が言えるはずです。例題として、ストレージ (Amazon S3 / Google Cloud Storage) のListObjects[1]操作を扱います。

以下公式ドキュメントを読めばだいたい終わりではありますが。
https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/paginators.html
https://cloud.google.com/storage/docs/paginate-results#code-samples-paginate-results

筆者の環境

AWS

AWS SDKは、素朴に使うとページネーション無し

C#

低級なAPI

ListObjects(V2)はデフォルトで1000件が上限で、バケットにもっと多数のオブジェクトがあったとしても1000件しか返しません。ListObjectsの結果から取れるNextContinuationTokenを次のリクエストのContinuationTokenに指定することで、次の1000件を得ることができます。低級なAPIでは言語問わずだいたい以下のようなイディオムを書くことになります。

using Amazon.S3;
using Amazon.S3.Model;

using var client = new AmazonS3Client();
var request = new ListObjectsV2Request
{
    BucketName = "my-bucket",
    Prefix = "foo/bar/",
};
do
{
    var response = await client.ListObjectsV2Async(request);
    foreach (var o in response.S3Objects)
    {
        ...
    }
    request.ContinuationToken = response.NextContinuationToken;
} while (!string.IsNullOrEmpty(request.ContinuationToken));

ページネーション有り

.NET向けSDKの場合、S3Clientなど一部のクライアントは、.Paginators でページネーション対応の処理を行えます。ContinuationTokenの処理を裏で行ってくれます。 https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/S3/TIS3PaginatorFactory.html

using var client = new AmazonS3Client();
var response = client.Paginators.ListObjects(new ListObjectsRequest
{
    BucketName = "my-bucket",
    Prefix = "foo/bar/"
});

await foreach (var o in response.S3Objects)
{
    ...
}

Python

Pythonの例も述べておきます。boto3も同じくpaginatorを用意しています。以下ページにある通りです。
https://boto3.amazonaws.com/v1/documentation/api/latest/guide/paginators.html

import boto3

client = boto3.client('s3')
paginator = client.get_paginator('list_objects')
page_iterator = paginator.paginate(Bucket='my-bucket')

for page in page_iterator:
    print(page['Contents'])

もちろん、S3のlist_objectsに関して言えば、boto3.resourceを使えばもっと高級に書けるのはboto3利用者にはおそらくご承知の通りです。

import boto3

s3 = boto3.resource('s3')
bucket = s3.Bucket('my-bucket')
for object in bucket.objects.all():
    print(object)

Google Cloud

Google Cloud SDKは、既定でページネーション有り

以下記載のように、裏で自動的にページネーション処理をしてくれて、全件の結果を得られます。
https://cloud.google.com/storage/docs/paginate-results#code-samples-paginate-results

C#

全件処理

全件欲しいなら、何も考える必要はありません。

using Google.Cloud.Storage.V1;

var pagedObjects = client.ListObjectsAsync("my-bucket", "foo/bar/");
await foreach (var o in pagedObjects)
{
    ...
}

System.Linq.Async を導入すると、例えばすぐ配列として得たいときにToArrayAsyncなど便利なメソッドが使えます。ただし、ものすごくファイル数が多い場所でToArrayなど全部をなめるようなことをしてしまうと、延々と処理が終わらずにメモリを食い尽くしていきます。欲しい量が決まっていれば、Take等で制御します。

var pagedObjects = client.ListObjectsAsync("my-bucket", "foo/bar/");

var allObjects = await pagedObjects.ToArrayAsync(); // やばい
var objects = await pagedObjects.Take(1000).ToArrayAsync();

ページごとに取得

あえてページ単位で結果を得ることもできます。AsRawResponses を使います、

var options = new ListObjectsOptions
{
    PageSize = 100,
};
var pagedObjects = client.ListObjectsAsync("my-bucket", "foo/bar/", options);
var raw = objects.AsRawResponses();

await foreach (var page in raw)
{
    Console.WriteLine(page.Items.Count); // 100
}

page.Itemsには、ここでは100件単位でオブジェクトの内容が入ります。

Python

同様に、ページネーションのことは考えなくても、普通にforを回せば全部取ってこれます。

from google.cloud import storage


client = storage.Client(project="my-project")

blobs = client.storage.list_blobs("my-bucket", prefix="foo/bar/")
for blob in blobs:
    ...

指定の件数だけ欲しければ、例えば itertools.islice を使う手があります。
ここもC#でのToArrayで述べたのと同様に、[x for x in blobs] のように安易に実体化するとメモリを食い尽くすかもしれません。できるだけイテレータのまま扱うようにします。

from itertools import islice
from google.cloud import storage


client = storage.Client(project="my-project")

all_blobs = client.storage.list_blobs("my-bucket", prefix="foo/bar/")
blobs = list(islice(all_blobs, 1000))
脚注
  1. PythonのGoogle Cloud Storage SDKでは list_blobs という名前になっており、言語により差がある場合があります。https://googleapis.dev/python/storage/latest/client.html#google.cloud.storage.client.Client.list_blobs ↩︎

Discussion