Краткий ответ :
Используйте Delimiter='/'
. Это позволяет избежать рекурсивного перечисления вашего ведра. Некоторые ответы здесь ошибочно предлагают сделать полный список и использовать некоторые строковые манипуляции для получения имен каталогов. Это могло быть ужасно неэффективным. Помните, что S3 практически не имеет ограничений на количество объектов, которые может содержать корзина. Итак, представьте, что между bar/
и у foo/
вас есть триллион объектов: вы очень долго ждете, чтобы получить ['bar/', 'foo/']
.
Используйте Paginators
. По той же причине (S3 - инженерное приближение бесконечности) вы должны листать страницы и избегать сохранения всего списка в памяти. Вместо этого рассмотрите свой «листер» как итератор и обработайте создаваемый им поток.
Используйте boto3.client
, а не boto3.resource
. resource
Версия не кажется, хорошо обрабатывать Delimiter
вариант. Если у вас есть ресурс, скажем bucket = boto3.resource('s3').Bucket(name)
, вы можете получить соответствующий клиент: bucket.meta.client
.
Длинный ответ :
Ниже приводится итератор, который я использую для простых корзин (без обработки версий).
import boto3
from collections import namedtuple
from operator import attrgetter
S3Obj = namedtuple('S3Obj', ['key', 'mtime', 'size', 'ETag'])
def s3list(bucket, path, start=None, end=None, recursive=True, list_dirs=True,
list_objs=True, limit=None):
"""
Iterator that lists a bucket's objects under path, (optionally) starting with
start and ending before end.
If recursive is False, then list only the "depth=0" items (dirs and objects).
If recursive is True, then list recursively all objects (no dirs).
Args:
bucket:
a boto3.resource('s3').Bucket().
path:
a directory in the bucket.
start:
optional: start key, inclusive (may be a relative path under path, or
absolute in the bucket)
end:
optional: stop key, exclusive (may be a relative path under path, or
absolute in the bucket)
recursive:
optional, default True. If True, lists only objects. If False, lists
only depth 0 "directories" and objects.
list_dirs:
optional, default True. Has no effect in recursive listing. On
non-recursive listing, if False, then directories are omitted.
list_objs:
optional, default True. If False, then directories are omitted.
limit:
optional. If specified, then lists at most this many items.
Returns:
an iterator of S3Obj.
Examples:
# set up
>>> s3 = boto3.resource('s3')
... bucket = s3.Bucket(name)
# iterate through all S3 objects under some dir
>>> for p in s3ls(bucket, 'some/dir'):
... print(p)
# iterate through up to 20 S3 objects under some dir, starting with foo_0010
>>> for p in s3ls(bucket, 'some/dir', limit=20, start='foo_0010'):
... print(p)
# non-recursive listing under some dir:
>>> for p in s3ls(bucket, 'some/dir', recursive=False):
... print(p)
# non-recursive listing under some dir, listing only dirs:
>>> for p in s3ls(bucket, 'some/dir', recursive=False, list_objs=False):
... print(p)
"""
kwargs = dict()
if start is not None:
if not start.startswith(path):
start = os.path.join(path, start)
kwargs.update(Marker=__prev_str(start))
if end is not None:
if not end.startswith(path):
end = os.path.join(path, end)
if not recursive:
kwargs.update(Delimiter='/')
if not path.endswith('/'):
path += '/'
kwargs.update(Prefix=path)
if limit is not None:
kwargs.update(PaginationConfig={'MaxItems': limit})
paginator = bucket.meta.client.get_paginator('list_objects')
for resp in paginator.paginate(Bucket=bucket.name, **kwargs):
q = []
if 'CommonPrefixes' in resp and list_dirs:
q = [S3Obj(f['Prefix'], None, None, None) for f in resp['CommonPrefixes']]
if 'Contents' in resp and list_objs:
q += [S3Obj(f['Key'], f['LastModified'], f['Size'], f['ETag']) for f in resp['Contents']]
q = sorted(q, key=attrgetter('key'))
if limit is not None:
q = q[:limit]
limit -= len(q)
for p in q:
if end is not None and p.key >= end:
return
yield p
def __prev_str(s):
if len(s) == 0:
return s
s, c = s[:-1], ord(s[-1])
if c > 0:
s += chr(c - 1)
s += ''.join(['\u7FFF' for _ in range(10)])
return s
Тест :
Следующее полезно для проверки поведения paginator
и list_objects
. Он создает ряд каталогов и файлов. Поскольку страницы содержат до 1000 записей, мы используем кратное количество записей для каталогов и файлов. dirs
содержит только каталоги (в каждом по одному объекту). mixed
содержит смесь каталогов и объектов с соотношением 2 объекта для каждого каталога (плюс, конечно, один объект в каталоге; S3 хранит только объекты).
import concurrent
def genkeys(top='tmp/test', n=2000):
for k in range(n):
if k % 100 == 0:
print(k)
for name in [
os.path.join(top, 'dirs', f'{k:04d}_dir', 'foo'),
os.path.join(top, 'mixed', f'{k:04d}_dir', 'foo'),
os.path.join(top, 'mixed', f'{k:04d}_foo_a'),
os.path.join(top, 'mixed', f'{k:04d}_foo_b'),
]:
yield name
with concurrent.futures.ThreadPoolExecutor(max_workers=32) as executor:
executor.map(lambda name: bucket.put_object(Key=name, Body='hi\n'.encode()), genkeys())
Полученная структура выглядит так:
./dirs/0000_dir/foo
./dirs/0001_dir/foo
./dirs/0002_dir/foo
...
./dirs/1999_dir/foo
./mixed/0000_dir/foo
./mixed/0000_foo_a
./mixed/0000_foo_b
./mixed/0001_dir/foo
./mixed/0001_foo_a
./mixed/0001_foo_b
./mixed/0002_dir/foo
./mixed/0002_foo_a
./mixed/0002_foo_b
...
./mixed/1999_dir/foo
./mixed/1999_foo_a
./mixed/1999_foo_b
Немного подправив приведенный выше код для s3list
проверки ответов от paginator
, вы можете увидеть некоторые забавные факты:
Это Marker
действительно эксклюзив. Given Marker=topdir + 'mixed/0500_foo_a'
запустит листинг после этого ключа (в соответствии с API AmazonS3 ), то есть с .../mixed/0500_foo_b
. Это причина __prev_str()
.
Используя Delimiter
при листинге mixed/
, каждый ответ из paginator
содержит 666 ключей и 334 общих префикса. Это довольно хорошо для того, чтобы не создавать огромных откликов.
Напротив, при перечислении dirs/
каждый ответ от paginator
содержит 1000 общих префиксов (и без ключей).
Передача лимита в виде PaginationConfig={'MaxItems': limit}
ограничивает только количество ключей, а не общие префиксы. Мы справляемся с этим путем дальнейшего усечения потока нашего итератора.