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() の返り値は、以下のような形のハッシュになっています。

{
    '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 件しか返せなかった場合、この IsTruncatedTrue になります。ちなみに、'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 を取得することができるようになりました!