Respuesta corta :
Utilice Delimiter='/'
. Esto evita hacer una lista recursiva de su depósito. Algunas respuestas aquí sugieren erróneamente hacer una lista completa y usar alguna manipulación de cadenas para recuperar los nombres de directorio. Esto podría resultar terriblemente ineficaz. Recuerde que S3 prácticamente no tiene límite en la cantidad de objetos que puede contener un depósito. Entonces, imagina que, entre bar/
y foo/
, tienes un billón de objetos: esperarías mucho tiempo para obtenerlos ['bar/', 'foo/']
.
Utilice Paginators
. Por la misma razón (S3 es la aproximación de infinito de un ingeniero), debe enumerar páginas y evitar almacenar todas las listas en la memoria. En su lugar, considere su "lister" como un iterador y maneje el flujo que produce.
Utilice boto3.client
, no boto3.resource
. La resource
versión no parece manejar bien la Delimiter
opción. Si usted tiene un recurso, por ejemplo una bucket = boto3.resource('s3').Bucket(name)
, se puede obtener el cliente correspondiente a: bucket.meta.client
.
Respuesta larga :
El siguiente es un iterador que utilizo para depósitos simples (sin manejo de versiones).
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
Prueba :
Lo siguiente es útil para probar el comportamiento de paginator
y list_objects
. Crea varios directorios y archivos. Dado que las páginas tienen hasta 1000 entradas, usamos un múltiplo de eso para directorios y archivos. dirs
contiene solo directorios (cada uno con un objeto). mixed
contiene una mezcla de directorios y objetos, con una proporción de 2 objetos por cada directorio (más un objeto debajo de dir, por supuesto; S3 almacena solo objetos).
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())
La estructura resultante es:
./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
Con un poco de revisión del código proporcionado anteriormente para s3list
inspeccionar las respuestas del paginator
, puede observar algunos datos divertidos:
El Marker
es realmente exclusivo. Dado Marker=topdir + 'mixed/0500_foo_a'
hará que el listado comience después de esa clave (según la API de AmazonS3 ), es decir, con .../mixed/0500_foo_b
. Esa es la razón __prev_str()
.
Usando Delimiter
, al enumerar mixed/
, cada respuesta del paginator
contiene 666 claves y 334 prefijos comunes. Es bastante bueno para no generar respuestas enormes.
Por el contrario, al enumerar dirs/
, cada respuesta del paginator
contiene 1000 prefijos comunes (y no claves).
Pasar un límite en forma de PaginationConfig={'MaxItems': limit}
límites solo el número de claves, no los prefijos comunes. Nos ocupamos de eso truncando aún más el flujo de nuestro iterador.