AWS S3で1000件以上のオブジェクトを操作する方法

こんにちは。フューチャー株式会社TIG所属2020年新卒の大西です。
[フューチャー Advent Calendar 2020](https://qiita.com/advent-calendar/2020/future) 3日目を担当します。

現在業務でAWS（特にS3やGlue Jobなど）を使うことが多いのため、私自身の業務で躓いた経験を元に **「AWS初心者が躓きやすいポイントとその解消法」** というテーマでブログを書いていきます。

まず、今日扱うテーマは **「Boto3を用いた1,000件以上のS3オブジェクトの操作」** です。

## 躓きポイントの概要

[Boto3](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html)はPythonを介してAWSを操作するためのライブラリです。

Boto3を用いてAWSを操作する方は、 `list_objects_v2` や `objects.filter` 等の関数を使って複数のオブジェクトを取得する機会があるのではないでしょうか。

`list_object_v2` をもちいたオブジェクトの取得例

```python:get_objects.py
import boto3
s3 = boto3.resource("s3")
bucket = s3.Bucket(bucket_name)
prefix = "S3バケットへのパス"
objects = bucket.meta.client.list_objects_v2(Bucket=bucket.name, Prefix=prefix)
```

しかし、上記の関数には **「一度のリクエストで取得できるオブジェクトの数は1,000件まで」** というルールがあり、1001件目以降のオブジェクトは取得することができません。

![tech_blog_list_object_v2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124827/81e4718a-3fea-d861-c76f-16eb6e7b65e3.png)

![tech_blog_filter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124827/1e193efa-23e6-db93-2478-4e449c37d0f1.png)

[Boto3の公式ドキュメント内のS3に関する記述部分](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html)を読むと、S3では下記の関数に同様のルールが存在しているようです。

- get_bucket_replication()
- list_bucket_analytics_configurations()
- list_bucket_inventory_configurations()
- list_bucket_metrics_configurations()
- list_multipart_uploads()
- list_object_versions()
- list_objects()
- list_objects_v2()
- list_parts()
- put_bucket_analytics_configuration()
- put_bucket_inventory_configuration()
- put_bucket_lifecycle_configuration()
- put_bucket_metrics_configuration()
- put_bucket_replication()
- object_versions.filter()
- objects.filter()
- BucketLifecycleConfiguration.put()


## 解決方法

今回は例として `list_objects_v2` を用いる場合の解決方法を書きます。

list_objects_v2の返り値のdictには、結果が切り捨てられているかどうかを示す `IsTruncated` が含まれています。
そして、取得するオブジェクト数の上限を指定せず`list_objects_v2`を実行し、取得したオブジェクトの数が1000件を超えていた場合は、`objects["IsTruncated"]`にTrueがセットされて返ってきます。
また、list_objects_v2は引数の`StartAfter`を設定することで、指定したS3ファイルの次のファイルからオブジェクトを取得してくれます。

よって、`objects["IsTruncated"]==True`だった場合は、1000件目のS3キーをStartAfterに設定して再度list_objects_v2を実行するという処理を`["IsTruncated"]==False`になるまで繰り返すことで、1,000件以上のオブジェクトでも取得することができます。

### ソース

```python:get_objects.py
s3 = boto3.resource("s3")
bucket = s3.Bucket(source_bucket)
prefix = "S3バケットへのパス"
keys = get_all_keys(bucket.name, prefix, [], "")


def get_all_keys(bucket_name: str, prefix: str, keys: List[str], marker: str) -> List[str]:
    """
    S3の指定したパスに存在するオブジェクトのキーを全て取得する

    Parameters
    ----------
    bucket_name: String
        対象のBucket
    prefix: String
        対象のディレクトリのパス
    keys: List[str]
    marker: String
        関数の中から呼び出す時のための引数。通常はkeys = [], marker = "" で呼び出す

    Returns
    -------
    List[str]
        取得したキーのリスト
    """
    s3 = boto3.resource("s3")
    bucket = s3.Bucket(bucket_name)
    objects = bucket.meta.client.list_objects_v2(Bucket=bucket.name, Prefix=prefix, StartAfter=marker)
    if "Contents" in objects:
        keys.extend([content["Key"] for content in objects["Contents"]])
        # 返り値のIsTruncatedがTrueかどうかを確認する
        if objects.get("isTruncated"):
            # marker引数に取得したkeysの末尾の値を設定して再度get_all_keysを実行する
            return get_all_keys(bucket_name=bucket_name, prefix=prefix, keys=keys, marker=keys[-1])
    return keys
```

## 最後に
今回は **「AWS初心者が躓きやすいポイントとその解消法」** の第一弾として `IsTruncated` を用いた1,000件上限の解決についてまとめました。

大量のオブジェクトを一度に取得したい方はぜひ参考にしてみてください。