Saltar al contenido

Recuperando nombres de subcarpetas en el bucket de S3 de boto3

Hacemos una revisión completa cada sección de nuestra web con la meta de enseñarte siempre la información certera y actual.

Solución:

El siguiente fragmento de código devuelve SOLAMENTE 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

S3 es un almacenamiento de objetos, no tiene una estructura de directorio real. La “/” es más bien cosmética. 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 keys que comienzan con lo especificado prefix.

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 la carpeta prefix. 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 frente a boto3.resource. Si desarrolla una biblioteca compartida interna, el uso de boto3.resource le dará una capa de caja negra sobre los recursos utilizados.

Respuesta corta:

  • Usar Delimiter='/'. Esto evita hacer una lista recursiva de su depósito. Algunas respuestas aquí sugieren erróneamente hacer una lista completa y usar algunos string manipulación 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 obtener ['bar/', 'foo/'].

  • Usar Paginators. Por la misma razón (S3 es la aproximación del infinito de un ingeniero), debe enumere las páginas y evite almacenar todo el listado en la memoria. En su lugar, considere su “lister” como un iterador y maneje el flujo que produce.

  • Usar boto3.client, no boto3.resource. los resource la versión no parece manejar bien el Delimiter opción. Si tiene un recurso, diga un bucket = boto3.resource('s3').Bucket(name), puede obtener el cliente correspondiente con: 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 del 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='hin'.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 corrección del código dado anteriormente para s3list para inspeccionar las respuestas del paginator, puedes observar algunos datos divertidos:

  • los Marker es realmente exclusivo. Dado Marker=topdir + 'mixed/0500_foo_a' hará que la lista comience después ese key (según la API de AmazonS3), es decir, con .../mixed/0500_foo_b. Esa es la razon de __prev_str().

  • Utilizando Delimiter, al enumerar mixed/, cada respuesta del paginator contiene 666 keys 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 keys).

  • Pasando un límite en forma de PaginationConfig='MaxItems': limit limita solo el número de keys, no los prefijos comunes. Nos ocupamos de eso truncando aún más el flujo de nuestro iterador.

Si guardas algún reparo y capacidad de limar nuestro enunciado puedes realizar una aclaración y con deseo lo leeremos.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *