Kurze Antwort :
Verwenden Sie Delimiter='/'
. Dies vermeidet eine rekursive Auflistung Ihres Buckets. Einige Antworten hier schlagen fälschlicherweise vor, eine vollständige Liste zu erstellen und die Verzeichnisnamen mithilfe einer Zeichenfolgenmanipulation abzurufen. Dies könnte schrecklich ineffizient sein. Denken Sie daran, dass S3 praktisch keine Begrenzung für die Anzahl der Objekte hat, die ein Bucket enthalten kann. Stellen Sie sich also vor, Sie haben zwischen bar/
und foo/
eine Billion Objekte: Sie würden sehr lange warten, bis Sie sie erhalten ['bar/', 'foo/']
.
Verwenden Sie Paginators
. Aus dem gleichen Grund (S3 ist eine Annäherung der Unendlichkeit des Ingenieurs), Sie müssen durch die Seiten auflisten und vermeiden Sie alle Auflistung im Speicher zu speichern. Betrachten Sie stattdessen Ihren "Lister" als Iterator und behandeln Sie den von ihm erzeugten Stream.
Verwenden Sie boto3.client
nicht boto3.resource
. Die resource
Version scheint die Delimiter
Option nicht gut zu handhaben . Wenn Sie eine Ressource haben, sagen Sie a bucket = boto3.resource('s3').Bucket(name)
, können Sie den entsprechenden Client erhalten mit : bucket.meta.client
.
Lange Antwort :
Das Folgende ist ein Iterator, den ich für einfache Buckets verwende (keine Versionsbehandlung).
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
Test :
Das Folgende ist hilfreich, um das Verhalten von paginator
und zu testen list_objects
. Es werden eine Reihe von Verzeichnissen und Dateien erstellt. Da die Seiten bis zu 1000 Einträge umfassen, verwenden wir ein Vielfaches davon für Verzeichnisse und Dateien. dirs
enthält nur Verzeichnisse (jedes mit einem Objekt). mixed
enthält eine Mischung aus Verzeichnissen und Objekten mit einem Verhältnis von 2 Objekten für jedes Verzeichnis (plus natürlich ein Objekt unter Verzeichnis; S3 speichert nur Objekte).
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())
Die resultierende Struktur ist:
./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
Wenn Sie den oben angegebenen Code ein wenig bearbeiten s3list
, um die Antworten von zu überprüfen paginator
, können Sie einige interessante Fakten beobachten:
Das Marker
ist wirklich exklusiv. Mit gegeben Marker=topdir + 'mixed/0500_foo_a'
wird die Auflistung nach diesem Schlüssel beginnen (gemäß der AmazonS3-API ), dh mit .../mixed/0500_foo_b
. Das ist der Grund dafür __prev_str()
.
Verwenden Delimiter
, wenn die Auflistung mixed/
, jede Antwort von der paginator
enthält 666 Schlüssel und 334 gemeinsame Präfixe. Es ist ziemlich gut, keine enormen Antworten zu erstellen.
Im Gegensatz dazu enthält dirs/
jede Antwort von der Liste paginator
1000 gemeinsame Präfixe (und keine Schlüssel).
Übergeben eines Limits in Form von PaginationConfig={'MaxItems': limit}
Limits nur die Anzahl der Schlüssel, nicht die allgemeinen Präfixe. Wir beschäftigen uns damit, indem wir den Stream unseres Iterators weiter abschneiden.