Recuperando nombres de subcarpetas en el bucket de S3 de boto3

85

Con boto3, puedo acceder a mi bucket de AWS S3:

s3 = boto3.resource('s3')
bucket = s3.Bucket('my-bucket-name')

Ahora, el depósito contiene una carpeta first-level, que a su vez contiene varias subcarpetas nombradas con una marca de tiempo, por ejemplo 1456753904534. Necesito saber el nombre de estas subcarpetas para otro trabajo que estoy haciendo y me pregunto si podría hacer que boto3 las recupere por mí.

Así que intenté:

objs = bucket.meta.client.list_objects(Bucket='my-bucket-name')

que da un diccionario, cuya clave 'Contenido' me da todos los archivos de tercer nivel en lugar de los directorios de marca de tiempo del segundo nivel, de hecho obtengo una lista que contiene cosas como

{u'ETag ':' "etag" ', u'Key': primer nivel / 1456753904534 / part-00014 ', u'LastModified': datetime.datetime (2016, 2, 29, 13, 52, 24, tzinfo = tzutc ()),
u'Owner ': {u'DisplayName': 'owner', u'ID ':' id '},
u'Size': size, u'StorageClass ':' storageclass '}

puede ver que los archivos específicos, en este caso, part-00014se recuperan, mientras que me gustaría obtener el nombre del directorio solo. En principio, podría eliminar el nombre del directorio de todas las rutas, ¡pero es feo y costoso recuperar todo en el tercer nivel para obtener el segundo nivel!

También probé algo reportado aquí :

for o in bucket.objects.filter(Delimiter='/'):
    print(o.key)

pero no obtengo las carpetas en el nivel deseado.

¿Hay una manera de resolver esto?

mar estaño
fuente
¿Estás diciendo que esto no funciona? ¿Podrías publicar qué sucede cuando ejecutas eso?
Jordon Phillips
1
@JordonPhillips Probé las primeras líneas de ese enlace que enviaste, que pegué aquí, y obtengo los archivos de texto en el primer nivel del depósito y sin carpetas.
mar tin
@mar tin ¿Alguna vez resolvió este problema? Me enfrento a un dilema similar en el que necesito el primer elemento en cada subcarpeta de depósitos.
Ted Taylor of Life
1
@TedTaylorofLife Sí, no hay otra forma que obtener todos los objetos y dividirlos /para obtener subcarpetas
mar tin
1
@ mar tin La única forma que he hecho es tomar la salida, lanzarla a un formato de texto y delimitar por comas con "/" y luego copiar y pegar el primer elemento. Qué fastidio.
Ted Taylor of Life

Respuestas:

56

S3 es un almacenamiento de objetos, no tiene una estructura de directorio real. El "/" es bastante cosmético. Una de las razones por las que la gente quiere tener una estructura de directorios es porque pueden mantener / podar / agregar un árbol a la aplicación. Para S3, trata dicha estructura como una especie de índice o etiqueta de búsqueda.

Para manipular un objeto en S3, necesita boto3.client o boto3.resource, por ejemplo, para enumerar todos los objetos

import boto3 
s3 = boto3.client("s3")
all_objects = s3.list_objects(Bucket = 'bucket-name') 

http://boto3.readthedocs.org/en/latest/reference/services/s3.html#S3.Client.list_objects

De hecho, si el nombre del objeto s3 se almacena usando el separador '/'. La versión más reciente de list_objects (list_objects_v2) le permite limitar la respuesta a las claves que comienzan con el prefijo especificado.

Para limitar los elementos a elementos de determinadas subcarpetas:

    import boto3 
    s3 = boto3.client("s3")
    response = s3.list_objects_v2(
            Bucket=BUCKET,
            Prefix ='DIR1/DIR2',
            MaxKeys=100 )

Documentación

Otra opción es usar la función python os.path para extraer el prefijo de la carpeta. El problema es que esto requerirá enumerar objetos de directorios no deseados.

import os
s3_key = 'first-level/1456753904534/part-00014'
filename = os.path.basename(s3_key) 
foldername = os.path.dirname(s3_key)

# if you are not using conventional delimiter like '#' 
s3_key = 'first-level#1456753904534#part-00014
filename = s3_key.split("#")[-1]

Un recordatorio sobre boto3: boto3.resource es una buena API de alto nivel. Existen pros y contras de usar boto3.client vs boto3.resource. Si desarrolla una biblioteca compartida interna, el uso de boto3.resource le dará una capa de caja negra sobre los recursos utilizados.

mootmoot
fuente
1
Esto me da el mismo resultado que obtengo con mi intento en la pregunta. Supongo que tendré que resolver el camino difícil tomando todas las claves de los objetos devueltos y dividiendo la cadena para obtener el nombre de la carpeta.
mar tin
1
@martina: una python perezosa se divide y recoge los últimos datos dentro de la lista, por ejemplo, filename = keyname.split ("/") [- 1]
mootmoot
1
@martin directory_name = os.path.dirname(directory/path/and/filename.txt)yfile_name = os.path.basename(directory/path/and/filename.txt)
jkdev
106

El siguiente fragmento de código devuelve SÓLO las 'subcarpetas' en una 'carpeta' del depósito s3.

import boto3
bucket = 'my-bucket'
#Make sure you provide / in the end
prefix = 'prefix-name-with-slash/'  

client = boto3.client('s3')
result = client.list_objects(Bucket=bucket, Prefix=prefix, Delimiter='/')
for o in result.get('CommonPrefixes'):
    print 'sub folder : ', o.get('Prefix')

Para obtener más detalles, puede consultar https://github.com/boto/boto3/issues/134

Dipankar
fuente
12
¿Qué sucede si quiero enumerar el contenido de una subcarpeta en particular?
azhar22k
1
@ azhar22k, supongo que podría ejecutar la función de forma recursiva para cada 'subcarpeta'.
Serban Cezar
¿Qué pasa si hay más de 1000 prefijos diferentes?
Kostrahb
38

Me tomó mucho tiempo averiguarlo, pero finalmente aquí hay una forma simple de enumerar el contenido de una subcarpeta en el depósito S3 usando boto3. Espero eso ayude

prefix = "folderone/foldertwo/"
s3 = boto3.resource('s3')
bucket = s3.Bucket(name="bucket_name_here")
FilesNotFound = True
for obj in bucket.objects.filter(Prefix=prefix):
     print('{0}:{1}'.format(bucket.name, obj.key))
     FilesNotFound = False
if FilesNotFound:
     print("ALERT", "No file in {0}/{1}".format(bucket, prefix))
azhar22k
fuente
3
¿Qué pasa si su carpeta contiene una enorme cantidad de objetos?
Pierre D
3
mi punto es que esta es una solución terriblemente ineficaz. S3 está diseñado para lidiar con separadores arbitrarios en las claves. Por ejemplo '/',. Eso le permite omitir "carpetas" llenas de objetos sin tener que paginarlos. Y luego, incluso si insiste en una lista completa (es decir, el equivalente 'recursivo' en aws cli), debe usar paginadores o enumerará solo los primeros 1000 objetos.
Pierre D
Esta es una respuesta genial. Para aquellos que lo necesitan, le he aplicado un limiten mi respuesta derivada .
Acumenus
38

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 resourceversión no parece manejar bien la Delimiteropció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)
        # note: need to use a string just smaller than start, because
        # the list_object API specifies that start is excluded (the first
        # result is *after* 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']]
        # note: even with sorted lists, it is faster to sort(a+b)
        # than heapq.merge(a, b) at least up to 10K elements in each list
        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 paginatory 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. dirscontiene solo directorios (cada uno con un objeto). mixedcontiene 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 s3listinspeccionar las respuestas del paginator, puede observar algunos datos divertidos:

  • El Markeres 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 paginatorcontiene 666 claves y 334 prefijos comunes. Es bastante bueno para no generar respuestas enormes.

  • Por el contrario, al enumerar dirs/, cada respuesta del paginatorcontiene 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.

Pierre D
fuente
@Mehdi: realmente no es muy complicado, para un sistema que ofrece una escala y una confiabilidad tan increíbles. Si alguna vez trata con más de unos pocos cientos de TB, apreciará lo que ofrecen. Recuerde, las unidades siempre tienen un MTBF> 0 ... Piense en las implicaciones para el almacenamiento de datos a gran escala. Descargo de responsabilidad: soy un usuario de AWS activo y feliz, sin otra conexión, excepto que he trabajado en datos de escala de petabytes desde 2007 y solía ser mucho más difícil.
Pierre D
16

El gran descubrimiento con S3 es que no hay carpetas / directorios, solo claves. La aparente estructura de carpetas simplemente se antepone al nombre del archivo para convertirse en la 'Clave', por lo que para enumerar el contenido de myBucket's some/path/to/the/file/puede probar:

s3 = boto3.client('s3')
for obj in s3.list_objects_v2(Bucket="myBucket", Prefix="some/path/to/the/file/")['Contents']:
    print(obj['Key'])

que te daría algo como:

some/path/to/the/file/yo.jpg
some/path/to/the/file/meAndYou.gif
...
CpILL
fuente
Esta es una buena respuesta, pero solo recuperará hasta 1000 objetos y no más. He producido una respuesta derivada que puede recuperar una mayor cantidad de objetos.
Acumenus
sí, @Acumenus supongo que tu respuesta es más compleja
CpILL
16

Tuve el mismo problema pero logré resolverlo usando boto3.clienty list_objects_v2con Buckety StartAfterparámetros.

s3client = boto3.client('s3')
bucket = 'my-bucket-name'
startAfter = 'firstlevelFolder/secondLevelFolder'

theobjects = s3client.list_objects_v2(Bucket=bucket, StartAfter=startAfter )
for object in theobjects['Contents']:
    print object['Key']

El resultado de salida para el código anterior mostraría lo siguiente:

firstlevelFolder/secondLevelFolder/item1
firstlevelFolder/secondLevelFolder/item2

Documentación de Boto3 list_objects_v2

Para eliminar solo el nombre del directorio secondLevelFolder, solo usé el método Python split():

s3client = boto3.client('s3')
bucket = 'my-bucket-name'
startAfter = 'firstlevelFolder/secondLevelFolder'

theobjects = s3client.list_objects_v2(Bucket=bucket, StartAfter=startAfter )
for object in theobjects['Contents']:
    direcoryName = object['Key'].encode("string_escape").split('/')
    print direcoryName[1]

El resultado de salida para el código anterior mostraría lo siguiente:

secondLevelFolder
secondLevelFolder

Documentación de Python split ()

Si desea obtener el nombre del directorio Y el nombre del elemento de contenido, reemplace la línea de impresión con lo siguiente:

print "{}/{}".format(fileName[1], fileName[2])

Y saldrá lo siguiente:

secondLevelFolder/item2
secondLevelFolder/item2

Espero que esto ayude

Sophie Muspratt
fuente
8

Lo siguiente funciona para mí ... Objetos S3:

s3://bucket/
    form1/
       section11/
          file111
          file112
       section12/
          file121
    form2/
       section21/
          file211
          file112
       section22/
          file221
          file222
          ...
      ...
   ...

Utilizando:

from boto3.session import Session
s3client = session.client('s3')
resp = s3client.list_objects(Bucket=bucket, Prefix='', Delimiter="/")
forms = [x['Prefix'] for x in resp['CommonPrefixes']] 

obtenemos:

form1/
form2/
...

Con:

resp = s3client.list_objects(Bucket=bucket, Prefix='form1/', Delimiter="/")
sections = [x['Prefix'] for x in resp['CommonPrefixes']] 

obtenemos:

form1/section11/
form1/section12/
cem
fuente
6

La cli de AWS hace esto (presumiblemente sin buscar e iterar a través de todas las claves en el depósito) cuando se ejecuta aws s3 ls s3://my-bucket/, así que pensé que debe haber una forma de usar boto3.

https://github.com/aws/aws-cli/blob/0fedc4c1b6a7aee13e2ed10c3ada778c702c22c3/awscli/customizations/s3/subcommands.py#L499

Parece que de hecho usan Prefijo y Delimitador: pude escribir una función que me llevaría todos los directorios al nivel raíz de un depósito modificando un poco ese código:

def list_folders_in_bucket(bucket):
    paginator = boto3.client('s3').get_paginator('list_objects')
    folders = []
    iterator = paginator.paginate(Bucket=bucket, Prefix='', Delimiter='/', PaginationConfig={'PageSize': None})
    for response_data in iterator:
        prefixes = response_data.get('CommonPrefixes', [])
        for prefix in prefixes:
            prefix_name = prefix['Prefix']
            if prefix_name.endswith('/'):
                folders.append(prefix_name.rstrip('/'))
    return folders
Paul Zielinski
fuente
2

Aquí hay una posible solución:

def download_list_s3_folder(my_bucket,my_folder):
    import boto3
    s3 = boto3.client('s3')
    response = s3.list_objects_v2(
        Bucket=my_bucket,
        Prefix=my_folder,
        MaxKeys=1000)
    return [item["Key"] for item in response['Contents']]
ambigus9
fuente
1

Utilizando boto3.resource

Esto se basa en la respuesta de itz-azhar para aplicar un opcional limit. Obviamente, es sustancialmente más simple de usar que la boto3.clientversión.

import logging
from typing import List, Optional

import boto3
from boto3_type_annotations.s3 import ObjectSummary  # pip install boto3_type_annotations

log = logging.getLogger(__name__)
_S3_RESOURCE = boto3.resource("s3")

def s3_list(bucket_name: str, prefix: str, *, limit: Optional[int] = None) -> List[ObjectSummary]:
    """Return a list of S3 object summaries."""
    # Ref: https://stackoverflow.com/a/57718002/
    return list(_S3_RESOURCE.Bucket(bucket_name).objects.limit(count=limit).filter(Prefix=prefix))


if __name__ == "__main__":
    s3_list("noaa-gefs-pds", "gefs.20190828/12/pgrb2a", limit=10_000)

Utilizando boto3.client

Esto utiliza list_objects_v2y se basa en la respuesta de CpILL para permitir recuperar más de 1000 objetos.

import logging
from typing import cast, List

import boto3

log = logging.getLogger(__name__)
_S3_CLIENT = boto3.client("s3")

def s3_list(bucket_name: str, prefix: str, *, limit: int = cast(int, float("inf"))) -> List[dict]:
    """Return a list of S3 object summaries."""
    # Ref: https://stackoverflow.com/a/57718002/
    contents: List[dict] = []
    continuation_token = None
    if limit <= 0:
        return contents
    while True:
        max_keys = min(1000, limit - len(contents))
        request_kwargs = {"Bucket": bucket_name, "Prefix": prefix, "MaxKeys": max_keys}
        if continuation_token:
            log.info(  # type: ignore
                "Listing %s objects in s3://%s/%s using continuation token ending with %s with %s objects listed thus far.",
                max_keys, bucket_name, prefix, continuation_token[-6:], len(contents))  # pylint: disable=unsubscriptable-object
            response = _S3_CLIENT.list_objects_v2(**request_kwargs, ContinuationToken=continuation_token)
        else:
            log.info("Listing %s objects in s3://%s/%s with %s objects listed thus far.", max_keys, bucket_name, prefix, len(contents))
            response = _S3_CLIENT.list_objects_v2(**request_kwargs)
        assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
        contents.extend(response["Contents"])
        is_truncated = response["IsTruncated"]
        if (not is_truncated) or (len(contents) >= limit):
            break
        continuation_token = response["NextContinuationToken"]
    assert len(contents) <= limit
    log.info("Returning %s objects from s3://%s/%s.", len(contents), bucket_name, prefix)
    return contents


if __name__ == "__main__":
    s3_list("noaa-gefs-pds", "gefs.20190828/12/pgrb2a", limit=10_000)
Acumenus
fuente
0

En primer lugar, no existe un concepto de carpeta real en S3. Definitivamente puede tener un archivo @ '/folder/subfolder/myfile.txt'y ninguna carpeta ni subcarpeta.

Para "simular" una carpeta en S3, debe crear un archivo vacío con una '/' al final de su nombre (consulte el boto de Amazon S3: ¿cómo crear una carpeta? )

Para su problema, probablemente debería usar el método get_all_keyscon los 2 parámetros: prefixydelimiter

https://github.com/boto/boto/blob/develop/boto/s3/bucket.py#L427

for key in bucket.get_all_keys(prefix='first-level/', delimiter='/'):
    print(key.name)
Pirheas
fuente
1
Me temo que no tengo el método get_all_keys en el objeto bucket. Estoy usando boto3 versión 1.2.3.
mar tin
Recién comprobado boto 1.2a: allí, bucket tiene un método listcon prefixy delimiter. Supongo que debería funcionar.
Pirheas
1
El objeto Bucket recuperado como publico en la pregunta no tiene esos métodos. Estoy en boto3 1.2.6, ¿a qué versión se refiere su enlace?
Mar tin
0

Sé que boto3 es el tema que se está discutiendo aquí, pero encuentro que generalmente es más rápido e intuitivo usar simplemente awscli para algo como esto: awscli retiene más capacidades que boto3 por lo que vale.

Por ejemplo, si tengo objetos guardados en "subcarpetas" asociadas con un depósito determinado, puedo enumerarlos todos con algo como esto:

1) 'mydata' = nombre del depósito

2) 'f1 / f2 / f3' = "ruta" que conduce a "archivos" u objetos

3) 'foo2.csv, barfar.segy, gar.tar' = todos los objetos "dentro" f3

Entonces, podemos pensar que la "ruta absoluta" que conduce a estos objetos es: 'mydata / f1 / f2 / f3 / foo2.csv' ...

Con los comandos awscli, podemos enumerar fácilmente todos los objetos dentro de una "subcarpeta" determinada mediante:

aws s3 ls s3: // mydata / f1 / f2 / f3 / --recursivo

Nathan Benton
fuente
0

A continuación, se muestra el fragmento de código que puede manejar la paginación, si está intentando obtener una gran cantidad de objetos de depósito de S3:

def get_matching_s3_objects(bucket, prefix="", suffix=""):

    s3 = boto3.client("s3")
    paginator = s3.get_paginator("list_objects_v2")

    kwargs = {'Bucket': bucket}

    # We can pass the prefix directly to the S3 API.  If the user has passed
    # a tuple or list of prefixes, we go through them one by one.
    if isinstance(prefix, str):
        prefixes = (prefix, )
    else:
        prefixes = prefix

    for key_prefix in prefixes:
        kwargs["Prefix"] = key_prefix

        for page in paginator.paginate(**kwargs):
            try:
                contents = page["Contents"]
            except KeyError:
                return

            for obj in contents:
                key = obj["Key"]
                if key.endswith(suffix):
                    yield obj
peterDriscoll
fuente
0

En cuanto a Boto 1.13.3, resulta tan simple como eso (si omite todas las consideraciones de paginación, que se cubrieron en otras respuestas):

def get_sub_paths(bucket, prefix):
s3 = boto3.client('s3')
response = s3.list_objects_v2(
    Bucket=bucket,
    Prefix=prefix,
    MaxKeys=1000)
return [item["Prefix"] for item in response['CommonPrefixes']]
Vitalii Kotliarenko
fuente