Recuperando nomes de subpastas no intervalo S3 do boto3

93

Usando boto3, posso acessar meu intervalo AWS S3:

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

Agora, o depósito contém a pasta first-level, que por sua vez contém várias subpastas nomeadas com um carimbo de data / hora, por exemplo 1456753904534. Eu preciso saber o nome dessas subpastas para outro trabalho que estou fazendo e me pergunto se eu poderia ter boto3 recuperado para mim.

Então eu tentei:

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

que fornece um dicionário, cuja chave 'Conteúdo' me dá todos os arquivos de terceiro nível em vez dos diretórios de carimbo de data / hora de segundo nível, na verdade, recebo uma lista contendo coisas como

{u'ETag ':' "etag" ', u'Key': primeiro nível / 1456753904534 / parte-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 '}

você pode ver que os arquivos específicos, neste caso, part-00014são recuperados, enquanto eu gostaria de obter apenas o nome do diretório. Em princípio, eu poderia retirar o nome do diretório de todos os caminhos, mas é feio e caro recuperar tudo no terceiro nível para chegar ao segundo nível!

Também tentei algo relatado aqui :

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

mas não obtenho as pastas no nível desejado.

Há uma maneira de resolver isto?

mar tin
fonte
Então você está dizendo que isso não funciona? Você poderia postar o que acontece quando você executa isso?
Jordon Phillips
1
@JordonPhillips Tentei as primeiras linhas do link que você enviou, que colei aqui, e recebo os arquivos de texto no primeiro nível do balde e nenhuma pasta.
março
@mar tin Você já resolveu esse problema. Estou enfrentando um dilema semelhante, em que preciso do primeiro elemento em cada subpasta de intervalos.
Ted Taylor of Life
1
@TedTaylorofLife Sim, nenhuma outra maneira a não ser pegar todos os objetos e dividir /para obter as subpastas
mar tin
1
@mar tin A única maneira que fiz foi pegar a saída, colocá-la em um formato de texto e delimitar por vírgula "/" e, em seguida, copiar e colar o primeiro elemento. O que é um pé no saco.
Ted Taylor of Life

Respostas:

65

S3 é um armazenamento de objeto, não possui uma estrutura de diretório real. O "/" é bastante cosmético. Uma razão pela qual as pessoas desejam ter uma estrutura de diretório, é porque podem manter / podar / adicionar uma árvore ao aplicativo. No S3, você trata essa estrutura como uma espécie de índice ou tag de pesquisa.

Para manipular objetos no S3, você precisa de boto3.client ou boto3.resource, por exemplo, para listar todos os 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

Na verdade, se o nome do objeto s3 for armazenado usando o separador '/'. A versão mais recente de list_objects (list_objects_v2) permite que você limite a resposta às chaves que começam com o prefixo especificado.

Para limitar os itens a itens em certas subpastas:

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

Documentação

Outra opção é usar a função python os.path para extrair o prefixo da pasta. O problema é que isso exigirá a listagem de objetos de diretórios indesejados.

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]

Um lembrete sobre boto3: boto3.resource é uma boa API de alto nível. Existem prós e contras usando boto3.client vs boto3.resource. Se você desenvolver uma biblioteca compartilhada interna, o uso de boto3.resource lhe dará uma camada de caixa preta sobre os recursos usados.

mootmoot
fonte
2
Isso me dá o mesmo resultado que obtenho com minha tentativa na pergunta. Acho que terei que resolver da maneira mais difícil pegando todas as chaves dos objetos retornados e dividindo a string para obter o nome da pasta.
março,
1
@martina: um python preguiçoso divide e pega os últimos dados da lista, por exemplo, filename = keyname.split ("/") [- 1]
mootmoot
1
@martin directory_name = os.path.dirname(directory/path/and/filename.txt)andfile_name = os.path.basename(directory/path/and/filename.txt)
jkdev
109

O trecho de código abaixo retorna SOMENTE as 'subpastas' em uma 'pasta' do balde 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 obter mais detalhes, você pode consultar https://github.com/boto/boto3/issues/134

Dipankar
fonte
12
E se eu quiser listar o conteúdo de uma subpasta específica?
azhar22k
1
@ azhar22k, suponho que você poderia simplesmente executar a função recursivamente para cada 'subpasta'.
Serban Cezar
E se houver mais de 1000 prefixos diferentes?
Kostrahb
42

Resposta curta :

  • Use Delimiter='/'. Isso evita fazer uma listagem recursiva do seu intervalo. Algumas respostas aqui sugerem erroneamente fazer uma lista completa e usar alguma manipulação de string para recuperar os nomes dos diretórios. Isso pode ser terrivelmente ineficiente. Lembre-se de que o S3 praticamente não tem limite para o número de objetos que um depósito pode conter. Então, imagine que, entre bar/e foo/, você tenha um trilhão de objetos: você esperaria muito tempo para obtê-lo ['bar/', 'foo/'].

  • Use Paginators. Pelo mesmo motivo (S3 é a aproximação do infinito de um engenheiro), você deve listar por meio de páginas e evitar armazenar toda a listagem na memória. Em vez disso, considere seu "lister" como um iterador e manipule o fluxo que ele produz.

  • Use boto3.client, não boto3.resource. A resourceversão não parece lidar bem com a Delimiteropção. Se você tem um recurso, digamos que um bucket = boto3.resource('s3').Bucket(name), você pode obter o cliente correspondente com: bucket.meta.client.

Resposta longa :

A seguir está um iterador que uso para baldes simples (sem manipulação de versão).

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

Teste :

O seguinte é útil para testar o comportamento do paginatore list_objects. Ele cria vários diretórios e arquivos. Como as páginas têm até 1000 entradas, usamos um múltiplo disso para diretórios e arquivos. dirscontém apenas diretórios (cada um com um objeto). mixedcontém uma mistura de dirs e objetos, com uma proporção de 2 objetos para cada dir (mais um objeto sob dir, é claro; o S3 armazena apenas 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())

A estrutura resultante é:

./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

Com um pouco de adulteração do código fornecido acima para s3listinspecionar as respostas do paginator, você pode observar alguns fatos interessantes:

  • O Markeré realmente exclusivo. Dado Marker=topdir + 'mixed/0500_foo_a'fará com que a listagem comece após essa chave (de acordo com a API AmazonS3 ), ou seja, com .../mixed/0500_foo_b. Essa é a razão para __prev_str().

  • Usando Delimiter, ao listar mixed/, cada resposta de paginatorcontém 666 chaves e 334 prefixos comuns. É muito bom em não construir respostas enormes.

  • Em contraste, ao listar dirs/, cada resposta do paginatorcontém 1000 prefixos comuns (e nenhuma chave).

  • Passar um limite na forma de PaginationConfig={'MaxItems': limit}limites apenas o número de chaves, não os prefixos comuns. Lidamos com isso truncando ainda mais o fluxo de nosso iterador.

Pierre D
fonte
@Mehdi: realmente não é muito complicado, para um sistema que oferece escala e confiabilidade tão inacreditáveis. Se você já lidou com mais do que algumas centenas de TB, será grato pelo que eles oferecem. Lembre-se, as unidades sempre têm um MTBF> 0 ... Pense nas implicações para o armazenamento de dados em grande escala. Isenção de responsabilidade: sou um usuário ativo e feliz da AWS, nenhuma outra conexão, exceto que trabalhei com dados em escala de petabyte desde 2007 e costumava ser muito mais difícil.
Pierre D
39

Levei muito tempo para descobrir, mas finalmente aqui está uma maneira simples de listar o conteúdo de uma subpasta no balde S3 usando boto3. Espero que ajude

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
fonte
3
e se sua pasta contiver um número enorme de objetos?
Pierre D
3
meu ponto é que esta é uma solução terrivelmente ineficiente. S3 foi desenvolvido para lidar com separadores arbitrários nas chaves. Por exemplo '/',. Isso permite que você pule "pastas" cheias de objetos sem ter que paginar sobre eles. E então, mesmo se você insistir em uma listagem completa (ou seja, o equivalente 'recursivo' em aws cli), você deve usar paginadores ou listará apenas os primeiros 1000 objetos.
Pierre D
Esta é uma ótima resposta. Para aqueles que precisam, apliquei limita em minha resposta derivada .
Acumenus
16

A grande descoberta com o S3 é que não há pastas / diretórios, apenas chaves. A aparente estrutura da pasta é apenas anexada ao nome do arquivo para se tornar a 'Chave', então para listar o conteúdo de myBucket, some/path/to/the/file/você pode tentar:

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

o que lhe daria algo como:

some/path/to/the/file/yo.jpg
some/path/to/the/file/meAndYou.gif
...
CpILL
fonte
Esta é uma boa resposta, mas irá recuperar até apenas 1000 objetos e não mais. Eu produzi uma resposta derivada que pode recuperar um grande número de objetos.
Acumenus
sim, @Acumenus, acho que sua resposta é mais complexa
CpILL
16

Eu tive o mesmo problema, mas consegui resolver usando boto3.cliente list_objects_v2com os parâmetros Buckete StartAfter.

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']

O resultado de saída para o código acima exibiria o seguinte:

firstlevelFolder/secondLevelFolder/item1
firstlevelFolder/secondLevelFolder/item2

Documentação Boto3 list_objects_v2

Para remover apenas o nome do diretório, secondLevelFolderacabei de usar o 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]

O resultado de saída para o código acima exibiria o seguinte:

secondLevelFolder
secondLevelFolder

Documentação Python split ()

Se você deseja obter o nome do diretório E o nome do item de conteúdo, substitua a linha de impressão pelo seguinte:

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

E o seguinte será gerado:

secondLevelFolder/item2
secondLevelFolder/item2

Espero que isto ajude

Sophie Muspratt
fonte
9

O seguinte funciona para mim ... objetos S3:

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

Usando:

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']] 

Nós temos:

form1/
form2/
...

Com:

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

Nós temos:

form1/section11/
form1/section12/
cem
fonte
Esta é a única solução que funcionou para mim porque eu precisava das "pastas" na raiz do balde, o prefixo deve ser '' ", enquanto que, caso contrário, deve terminar com" / "
Oliver
7

O AWS cli faz isso (presumivelmente sem buscar e iterar por todas as chaves no balde) quando você executa aws s3 ls s3://my-bucket/, então percebi que deve haver uma maneira de usar o boto3.

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

Parece que eles realmente usam Prefixo e Delimitador - consegui escrever uma função que me levaria a todos os diretórios no nível raiz de um intervalo, modificando um pouco esse 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
fonte
2

Aqui está uma solução possível:

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
fonte
2

Por que não usar o s3pathpacote que o torna tão conveniente quanto trabalhar pathlib? No entanto, se você deve usar boto3:

Usando boto3.resource

Isso se baseia na resposta de itz-azhar para aplicar um opcional limit. É obviamente substancialmente mais simples de usar do que a boto3.clientversão.

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)

Usando boto3.client

Isso usa list_objects_v2e se baseia na resposta de CpILL para permitir a recuperação de mais 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
fonte
0

Em primeiro lugar, não existe um conceito real de pasta no S3. Você definitivamente pode ter um arquivo @ '/folder/subfolder/myfile.txt'e nenhuma pasta ou subpasta.

Para "simular" uma pasta no S3, você deve criar um arquivo vazio com uma '/' no final de seu nome (consulte Amazon S3 boto - como criar uma pasta? )

Para o seu problema, você provavelmente deve usar o método get_all_keyscom os 2 parâmetros: prefixedelimiter

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
fonte
1
Infelizmente, não tenho o método get_all_keys no objeto de intervalo. Estou usando o boto3 versão 1.2.3.
março
Acabei de verificar o boto 1.2a: lá, o bucket tem um método listcom prefixe delimiter. Suponho que deve funcionar.
Pirheas
1
O objeto Bucket recuperado conforme eu postei na pergunta não tem esses métodos. Estou no boto3 1.2.6, a qual versão seu link se refere?
março de
0

Eu sei que o boto3 é o tópico que está sendo discutido aqui, mas acho que geralmente é mais rápido e intuitivo simplesmente usar o awscli para algo assim - o awscli retém mais recursos do que o boto3 para o que vale.

Por exemplo, se tenho objetos salvos em "subpastas" associadas a um determinado intervalo, posso listá-los todos com algo como este:

1) 'mydata' = nome do intervalo

2) 'f1 / f2 / f3' = "caminho" que leva a "arquivos" ou objetos

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

Portanto, podemos pensar no "caminho absoluto" que leva a esses objetos: 'mydata / f1 / f2 / f3 / foo2.csv' ...

Usando comandos awscli, podemos listar facilmente todos os objetos dentro de uma determinada "subpasta" via:

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

Nathan Benton
fonte
0

A seguir está o trecho de código que pode lidar com a paginação, se você estiver tentando buscar um grande número de objetos de intervalo 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
fonte
e se a primeira página estiver cheia de "CommonPrefixes" e não fornecer nenhuma chave de "Conteúdo". Eu acho que a implementação adequada deve ignorar a chave de conteúdo ausente e continuar com a próxima página.
Jan Vlcinsky
0

Quanto ao Boto 1.13.3, torna-se tão simples quanto isso (se você pular todas as considerações de paginação, que foram abordadas em outras respostas):

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
fonte