Câu trả lời ngắn gọn :
Sử dụng Delimiter='/'
. Điều này tránh thực hiện một danh sách đệ quy nhóm của bạn. Một số câu trả lời sai ở đây đề xuất thực hiện một danh sách đầy đủ và sử dụng một số thao tác chuỗi để truy xuất tên thư mục. Điều này có thể không hiệu quả khủng khiếp. Hãy nhớ rằng S3 hầu như không có giới hạn về số lượng đối tượng mà một thùng có thể chứa. Vì vậy, hãy tưởng tượng rằng, giữa bar/
và foo/
, bạn có một nghìn tỷ đối tượng: bạn sẽ đợi rất lâu để có được ['bar/', 'foo/']
.
Sử dụng Paginators
. Vì lý do tương tự (S3 là giá trị gần đúng của kỹ sư), bạn phải liệt kê qua các trang và tránh lưu trữ tất cả danh sách trong bộ nhớ. Thay vào đó, hãy coi "trình nghe" của bạn như một trình lặp và xử lý luồng nó tạo ra.
Sử dụng boto3.client
, không boto3.resource
. Các resource
phiên bản dường như không xử lý tốt các Delimiter
tùy chọn. Nếu bạn có một tài nguyên, nói một bucket = boto3.resource('s3').Bucket(name)
, bạn có thể nhận được các khách hàng tương ứng với: bucket.meta.client
.
Câu trả lời dài :
Sau đây là một trình lặp mà tôi sử dụng cho các nhóm đơn giản (không xử lý phiên bản).
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
Kiểm tra :
Sau đây là hữu ích để kiểm tra hành vi của paginator
và list_objects
. Nó tạo ra một số dirs và tệp. Vì các trang có tới 1000 mục nhập, chúng tôi sử dụng nhiều mục trong số đó cho dirs và tệp. dirs
chỉ chứa các thư mục (mỗi thư mục có một đối tượng). mixed
chứa hỗn hợp các dir và đối tượng, với tỷ lệ 2 đối tượng cho mỗi dir (tất nhiên là cộng với một đối tượng dưới dir; S3 chỉ lưu trữ các đối tượng).
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())
Cấu trúc kết quả là:
./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
Với một chút tiến sĩ về mã được đưa ra ở trên s3list
để kiểm tra các phản hồi từ paginator
, bạn có thể quan sát một số sự kiện thú vị:
Thực Marker
sự là độc quyền. Given Marker=topdir + 'mixed/0500_foo_a'
sẽ làm cho danh sách bắt đầu sau khóa đó (theo API AmazonS3 ), tức là với .../mixed/0500_foo_b
. Đó là lý do cho __prev_str()
.
Sử dụng Delimiter
, khi liệt kê mixed/
, mỗi phản hồi từ paginator
chứa 666 khóa và 334 tiền tố chung. Nó khá tốt trong việc không tạo ra các phản hồi lớn.
Ngược lại, khi liệt kê dirs/
, mỗi phản hồi từ paginator
chứa 1000 tiền tố chung (và không có khóa).
Vượt qua một giới hạn dưới dạng PaginationConfig={'MaxItems': limit}
giới hạn chỉ số lượng khóa, không phải các tiền tố chung. Chúng tôi giải quyết vấn đề đó bằng cách cắt bớt luồng của trình lặp của chúng tôi.