web-dev-qa-db-fra.com

Récupération des noms de sous-dossiers dans le compartiment S3 à partir de boto3

Grâce à boto3, je peux accéder à mon compartiment AWS S3:

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

Désormais, le compartiment contient le dossier first-level, qui contient lui-même plusieurs sous-dossiers nommés avec un horodatage, par exemple 1456753904534. J'ai besoin de connaître le nom de ces sous-dossiers pour un autre travail que je fais et je me demande si je pourrais les récupérer pour moi.

Alors j'ai essayé:

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

qui donne un dictionnaire, dont la clé 'Contenu' me donne tous les fichiers de troisième niveau au lieu des répertoires d’horodatage de deuxième niveau, en fait je reçois une liste contenant des choses comme

{u'ETag ':' "etag" ', u'Key': premier niveau/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 ': taille, u'StorageClass': 'storageclass'}

vous pouvez voir que les fichiers spécifiques, dans ce cas part-00014 sont récupérés, alors que je voudrais obtenir le nom du répertoire seul. En principe, je pourrais supprimer le nom du répertoire de tous les chemins, mais il est moche et cher de tout récupérer au troisième niveau pour obtenir le deuxième niveau!

J'ai aussi essayé quelque chose rapporté ici :

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

mais je ne reçois pas les dossiers au niveau souhaité.

Y a-t-il un moyen de résoudre ce problème?

43
mar tin

S3 est un stockage d'objet, il n'a pas de structure de répertoire réelle. Le "/" est plutôt cosmétique. L'une des raisons pour lesquelles les utilisateurs souhaitent disposer d'une structure de répertoires, car ils peuvent conserver/supprimer/ajouter une arborescence à l'application. Pour S3, vous traitez cette structure comme une sorte d’index ou de balise de recherche.

Pour manipuler un objet dans S3, vous avez besoin de boto3.client ou de boto3.resource, par exemple. Pour lister tous les objets

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

En fait, si le nom de l'objet s3 est stocké à l'aide du séparateur '/', vous pouvez utiliser la fonction python os.path pour extraire le préfixe du dossier.

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]

Rappel sur boto3: boto3.resource est une API de haut niveau de Nice. Il y a des avantages et des inconvénients à utiliser boto3.client vs boto3.resource. Si vous développez une bibliothèque partagée interne, l'utilisation de boto3.resource vous donnera une couche de boîte noire sur les ressources utilisées.

18
mootmoot

La partie de code ci-dessous renvoie UNIQUEMENT les "sous-dossiers" dans un "dossier" du compartiment 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')

Pour plus de détails, vous pouvez vous référer à https://github.com/boto/boto3/issues/134

64
Dipankar

Il m'a fallu beaucoup de temps pour comprendre, mais voici enfin un moyen simple de répertorier le contenu d'un sous-dossier dans le compartiment S3 à l'aide de boto3. J'espère que ça aide

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))
28
itz-azhar

Réponse courte :

  • Utilisez Delimiter='/' . Cela évite de faire une liste récursive de votre seau. Certaines réponses suggèrent à tort de faire une liste complète et d’utiliser une manipulation de chaîne pour récupérer les noms de répertoires. Cela pourrait être horriblement inefficace. N'oubliez pas que S3 ne limite pratiquement pas le nombre d'objets qu'un compartiment peut contenir. Imaginez donc que, entre _bar/_ et _foo/_, vous avez un billion d’objets: vous attendriez très longtemps pour obtenir _['bar/', 'foo/']_.

  • Utilisez Paginators . Pour la même raison (S3 est une approximation de l'infini par un ingénieur), vous devez lister par pages et éviter de stocker toute la liste en mémoire. Au lieu de cela, considérez votre "listeur" comme un itérateur et gérez le flux qu'il produit.

  • Utilisez boto3.client , pas boto3.resource . La version resource ne semble pas bien gérer l’option Delimiter. Si vous avez une ressource, dites une bucket = boto3.resource('s3').Bucket(name), vous pouvez obtenir le client correspondant avec: _bucket.meta.client_.

Réponse longue :

Ce qui suit est un itérateur que j'utilise pour des compartiments simples (pas de gestion de version).

_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
_

Test :

Ce qui suit est utile pour tester le comportement de paginator et _list_objects_. Il crée un certain nombre de répertoires et de fichiers. Comme les pages contiennent jusqu'à 1000 entrées, nous utilisons un multiple de celui-ci pour les répertoires et les fichiers. dirs ne contient que des répertoires (chacun ayant un objet). mixed contient un mélange de répertoires et d'objets, avec un ratio de 2 objets pour chaque répertoire (plus un objet sous dir, bien sûr; S3 ne stocke que les objets).

_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 structure résultante est:

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

Avec un peu de falsification du code donné ci-dessus pour _s3list_ afin d'inspecter les réponses de paginator, vous pouvez observer quelques faits amusants:

  • Le Marker est vraiment exclusif. Étant donné _Marker=topdir + 'mixed/0500_foo_a'_, la liste commencera après cette clé (conformément à la AmazonS3 API ), c'est-à-dire avec _.../mixed/0500_foo_b_. C'est la raison pour __prev_str().

  • En utilisant Delimiter, lors de la liste _mixed/_, chaque réponse de paginator contient 666 clés et 334 préfixes communs. C'est assez bon pour ne pas construire d'énormes réponses.

  • En revanche, lors de la liste _dirs/_, chaque réponse de paginator contient 1 000 préfixes communs (et aucune clé).

  • Passer une limite sous la forme de _PaginationConfig={'MaxItems': limit}_ limite uniquement le nombre de clés, pas les préfixes communs. Nous réglons cela en tronquant davantage le flux de notre itérateur.

19
Pierre D

J'ai eu le même problème, mais j'ai réussi à le résoudre en utilisant boto3.client et list_objects_v2 avec les paramètres Bucket et 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']

Le résultat en sortie pour le code ci-dessus afficherait ce qui suit:

firstlevelFolder/secondLevelFolder/item1
firstlevelFolder/secondLevelFolder/item2

Documentation Boto3 list_objects_v2

Afin de supprimer uniquement le nom du répertoire pour secondLevelFolder je viens d'utiliser python méthode 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]

Le résultat en sortie pour le code ci-dessus afficherait ce qui suit:

secondLevelFolder
secondLevelFolder

Documentation Python split ()

Si vous souhaitez obtenir le nom du répertoire ET le nom de l'élément de contenu, remplacez la ligne d'impression par ce qui suit:

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

Et ce qui suit sera affiché:

secondLevelFolder/item2
secondLevelFolder/item2

J'espère que cela t'aides

13
Sophie Muspratt

La grande réalisation avec S3 est qu’il n’existe pas de dossiers/répertoires mais uniquement des clés. Le la structure de dossier apparente est juste ajoutée au nom du fichier pour devenir la 'clé', afin de lister le contenu de myBucket 'some/path/to/the/file/, vous pouvez essayer:

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

ce qui vous donnerait quelque chose comme:

some/path/to/the/file/yo.jpg
some/path/to/the/file/meAndYou.gif
...
13
CpILL

Ce qui suit fonctionne pour moi ... objets S3:

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

En utilisant:

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

on a:

form1/
form2/
...

Avec:

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

on a:

form1/section11/
form1/section12/
5
cem

AWS cli fait cela (probablement sans aller chercher ni parcourir toutes les clés du compartiment) lorsque vous exécutez aws s3 ls s3://my-bucket/, j'ai donc pensé qu'il devait y avoir un moyen d'utiliser boto3.

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

On dirait qu'ils utilisent effectivement Prefix et Delimiter - j'ai pu écrire une fonction qui me permettrait d'obtenir tous les répertoires situés au niveau racine d'un bucket en modifiant légèrement ce code:

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
4
Paul Zielinski

Tout d’abord, il n’existe pas de véritable concept de dossier dans S3. Vous pouvez certainement avoir un fichier @ '/folder/subfolder/myfile.txt' et aucun dossier ni sous-dossier.

Pour "simuler" un dossier dans S3, vous devez créer un fichier vide avec un '/' à la fin de son nom (voir Amazon S3 boto - Comment créer un dossier? )

Pour votre problème, vous devriez probablement utiliser la méthode get_all_keys avec les 2 paramètres: prefix et delimiter

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)
0
Pirheas

Utiliser boto3.resource

Ceci repose sur le réponse par itz-azhar pour appliquer un limit facultatif. Il est évidemment beaucoup plus simple à utiliser que la version boto3.client.

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__)


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(boto3.resource("s3").Bucket(bucket_name).objects.limit(count=limit).filter(Prefix=prefix))


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

Utiliser boto3.client

Ceci utilise list_objects_v2 et s'appuie sur le réponse par CpILL pour récupérer plus de 1000 objets.

import logging
from typing import cast, List

import boto3

log = logging.getLogger(__name__)


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/
    s3_client = boto3.client("s3")
    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 __== "__main__":
    s3_list("noaa-gefs-pds", "gefs.20190828/12/pgrb2a", limit=10_000)
0
Acumenus