คำตอบสั้น ๆ :
ใช้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
มันสร้าง dirs และไฟล์จำนวนมาก เนื่องจากเพจมีมากถึง 1,000 รายการเราจึงใช้หลายหน้าสำหรับ dirs และไฟล์ dirs
มีไดเร็กทอรีเท่านั้น (แต่ละอันมีอ็อบเจ็กต์เดียว) mixed
มีส่วนผสมของ dirs และอ็อบเจ็กต์โดยมีอัตราส่วน 2 อ็อบเจกต์สำหรับแต่ละ dir (แน่นอนว่าบวกอ็อบเจ็กต์หนึ่งอันภายใต้ dir 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
เป็นพิเศษจริงๆ ป.ร. ให้ไว้Marker=topdir + 'mixed/0500_foo_a'
จะทำให้การเริ่มต้นรายการหลังจากที่สำคัญที่ (ตามAmazonS3 API ) .../mixed/0500_foo_b
คือมี __prev_str()
นั่นเป็นเหตุผลที่
การใช้Delimiter
เมื่อแสดงรายการmixed/
แต่ละคำตอบจากpaginator
คีย์ประกอบด้วย 666 คีย์และคำนำหน้าทั่วไป 334 รายการ ค่อนข้างดีที่จะไม่สร้างคำตอบมากมาย
ในทางตรงกันข้ามเมื่อแสดงรายการdirs/
แต่ละคำตอบจากpaginator
คำนำหน้าทั่วไป 1,000 คำ (และไม่มีคีย์)
ผ่านขีด จำกัด ในรูปแบบของการPaginationConfig={'MaxItems': limit}
จำกัด จำนวนคีย์เท่านั้นไม่ใช่คำนำหน้าทั่วไป เราจัดการกับสิ่งนั้นโดยตัดทอนสตรีมของตัววนซ้ำของเราเพิ่มเติม