Boto 3 で Amazon S3 上の key を取得する方法、実装例、注意点

Boto 3 で、S3 Buckets 上にある key を取得するときには、`list_objects()` を使います。prefix を指定して、条件を絞ることもできます。S3 で key を取得するときにはよく使われるメソッドだと思います。

## 基本的な使いかた

たとえば、バケット名: `hoge-bucket` にある、プレフィックス: `xx/yy` の key を全て取得したい時は以下のようにします。

```sample1.py
from boto3 import Session
s3client = Session().client('s3')

response = s3client.list_objects(
    Bucket='hoge-bucket',
    Prefix='xx/yy/'
)

if 'Contents' in response:  # 該当する key がないと response に 'Contents' が含まれない
    keys = [content['Key'] for content in response['Contents']]
```
これで、Prefix で指定した条件の key があれば、keys には 

```
>>> keys
['xx/yy/a1', 'xx/yy/a2', 'xx/yy/a3', 'xx/yy/b1']
```
のように key 文字列の配列が代入されているはずです。

ちなみに、Prefix は`'xx/yy/a'` のように指定することもできます。その場合は以下のような結果が返ります。

```
>>> keys
['xx/yy/a1', 'xx/yy/a2', 'xx/yy/a3']
```

## 注意点

`list_objects()` には (正確には Amazon S3 API には)、一度に取得できるのは 1000 key までという制限があります。単純に、バケットの下にある、1000 件以上の key を全て取得したいのであれば、

```sample2.py
from boto3 import Session
s3res = Session().resource('s3')

bucket = s3res.Bucket('hoge-bucket')
keys = [obj.key for obj in bucket.objects.all()]
```

で良いでしょう。

## Prefix 条件をつけて 1000 件以上の key を取得するには
ただ、もし Prefix を指定したいときはどうなるでしょうか？ 上述した sample1.py では、keys に代入されるのは多くても 1000 件です。引数の MaxKeys に `1000000` を指定しようとも、仕様により 1000 件以上は返ってきません。

### 方法1: 一度全件取得してからフィルタする

`hoge-bucket` の下にある key が少ないときには以下のようなやり方でも良いかもしれません。

```sample3.py
from boto3 import Session
s3res = Session().resource('s3')

bucket = s3res.Bucket('hoge-bucket')
keys = [obj.key for obj in bucket.objects.all() if obj.key.startswith("xx/yy/")]
```

ただ、もし key が何万、何十万とあると、結果を得るためにかなりの時間がかかります。モデルで出来る部分は極力モデルに任せたい所です。


### 方法2: `list_objects()` で `Marker` を使う

`list_objects()` の返り値は、以下のような形のハッシュになっています。

```py3
{
    'IsTruncated': True|False,  # 結果が寸断されたか。されていれば True
    'Marker': 'string',
    'NextMarker': 'string',
    'Contents': [
        {
            'Key': 'string',
            'LastModified': datetime(2015, 1, 1),
            'ETag': 'string',
            'Size': 123,
            'StorageClass': 'STANDARD'|'REDUCED_REDUNDANCY'|'GLACIER',
            'Owner': {
                'DisplayName': 'string',
                'ID': 'string'
            }
        },
    ],
    'Name': 'string',
    'Prefix': 'string',
    'Delimiter': 'string',
    'MaxKeys': 123,
    'CommonPrefixes': [
        {
            'Prefix': 'string'
        },
    ],
    'EncodingType': 'url'
}
```

ここで重要なのは `IsTruncated` です。もし結果が 1000 件以上あるのに、1000 件しか返せなかった場合、この `IsTruncated` が `True` になります。ちなみに、`'Contents'` 配列は、常に `'Key'` のアルファベット順を基準に昇順で並べられています。

また `list_objects()` には、`Marker` という引数があり、指定した key を 1 件目として、結果を出力することができます。これで役者は揃いました。以下は `list_objects()` をラップして作った、件数を問わず指定した全ての key を取得する関数です。

```sample4.py
from boto3 import Session
s3client = Session().client('s3')

def get_all_keys(bucket: str='', prefix: str='', keys: []=[], marker: str='') -> [str]:
    """
    指定した prefix のすべての key の配列を返す
    """
    response = s3client.list_objects(Bucket=bucket, Prefix=prefix, Marker=marker)
    if 'Contents' in response:  # 該当する key がないと response に 'Contents' が含まれない
        keys.extend([content['Key'] for content in response['Contents']])
        if 'IsTruncated' in response:
            return get_all_keys(bucket=bucket, prefix=prefix, keys=keys, marker=keys[-1])
    return keys
```

`if 'IsTruncated' in response:` で、もし `IsTruncated` であれば、keys のケツ (`keys[-1]`) を marker にして自分自身を呼び出しています。response に Contents がなくなったら、あるいは IsTruncated でなくなれば一気に結果を返します。

これで、件数を気にせずに、S3 上の key を取得することができるようになりました!