Подтвердить что ты не робот

Извлечение имен подпапок в ведро S3 из boto3

Используя boto3, я могу получить доступ к своему ковку AWS S3:

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

Теперь в ведре содержится папка first-level, которая сама содержит несколько подпапок с именем timestamp, например 1456753904534. Мне нужно знать имя этих подпапок для другой работы, которую я делаю, и мне интересно, могу ли я получить boto3 для них.

Итак, я попробовал:

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

который дает словарь, чей ключ "Содержание" дает мне все файлы третьего уровня вместо каталогов временных меток второго уровня, на самом деле я получаю список, содержащий вещи как

{u'ETag ':' "etag" ', u'Key': first-level/1456753904534/part-00014 ', u'LastModified': datetime.datetime(2016, 2, 29, 13, 52, 24, tzinfo = tzutc()),
u'Owner ': {u'DisplayName': 'owner', u'ID ': 'Идентификатор'},
u'Size ': size, u'StorageClass': 'storageclass'}

вы можете видеть, что определенные файлы, в этом случае part-00014 извлекаются, в то время как я хотел бы получить имя каталога в одиночку. В принципе, я мог бы вырезать имя каталога со всех путей, но это уродливо и дорого, чтобы получить все на третьем уровне, чтобы получить второй уровень!

Я также попытался что-то сообщить здесь:

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

но я не получаю папки на нужном уровне.

Есть ли способ решить эту проблему?

4b9b3361

Ответ 1

S3 - это хранилище объектов, оно не имеет реальной структуры каталогов. "/" Скорее косметический. Одна из причин, по которой люди хотят иметь структуру каталогов, потому что они могут поддерживать/сокращать/добавлять дерево в приложение. Для S3 вы рассматриваете такую структуру как своего рода индекс или поисковый тег.

Чтобы манипулировать объектом в S3, вам нужен boto3.client или boto3.resource, например, Перечислить все объекты

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

Фактически, если имя объекта s3 хранится с использованием разделителя '/', вы можете использовать функцию python os.path для извлечения префикса папки.

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]

Напоминание о boto3: boto3.resource - хороший API высокого уровня. Есть плюсы и минусы использования boto3.client против boto3.resource. Если вы разрабатываете внутреннюю разделяемую библиотеку, использование boto3.resource даст вам слой черного ящика над используемыми ресурсами.

Ответ 2

Ниже фрагмент кода возвращает ТОЛЬКО "подпапки" в "папке" из ведра 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')

Для получения дополнительной информации вы можете обратиться к https://github.com/boto/boto3/issues/134

Ответ 3

Мне потребовалось много времени, чтобы выяснить, но, наконец, вот простой способ перечислить содержимое подпапки в ведро S3 с помощью boto3. Надеюсь, что это поможет.

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

Ответ 4

Краткий ответ:

  • Используйте Delimiter='/'. Это позволяет избежать рекурсивного перечисления вашего сегмента. Некоторые ответы здесь ошибочно предлагают сделать полный листинг и использовать некоторые манипуляции со строками для получения имен каталогов. Это может быть ужасно неэффективно. Помните, что у S3 практически нет ограничений на количество объектов, которые может содержать ведро. Итак, представьте, что между bar/ и foo/ у вас есть триллион объектов: вы очень долго будете ждать, чтобы получить ['bar/', 'foo/'].

  • Используйте Paginators. По той же причине (S3 - инженерное приближение бесконечности), вы должны перечислять по страницам и избегать сохранения всего перечисления в памяти. Вместо этого рассмотрите ваш "список" как итератор и обработайте поток, который он создает.

  • Используйте boto3.client, а не boto3.resource. Версия resource, похоже, плохо справляется с опцией Delimiter. Если у вас есть ресурс, например, bucket = boto3.resource('s3').Bucket(name), вы можете получить соответствующего клиента с помощью: bucket.meta.client.

Длинный ответ:

Ниже приведен итератор, который я использую для простых блоков (без обработки версий).

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

Следующее полезно для проверки поведения paginator и list_objects. Создает несколько каталогов и файлов. Поскольку на страницах содержится до 1000 записей, мы используем их кратные для папок и файлов. dirs содержит только каталоги (каждый из которых имеет один объект). mixed содержит смесь каталогов и объектов с соотношением 2 объекта для каждого каталога (плюс, конечно, один объект под каталогом; S3 хранит только объекты).

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

Полученная структура:

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

Немного исправив код, приведенный выше для s3list, чтобы проверить ответы от paginator, вы можете наблюдать некоторые забавные факты:

  • Marker действительно эксклюзив. Учитывая, что Marker=topdir + 'mixed/0500_foo_a' запустит листинг после этого ключа (в соответствии с AmazonS3 API), то есть с .../mixed/0500_foo_b. Это причина для __prev_str().

  • Используя Delimiter, при перечислении mixed/ каждый ответ от paginator содержит 666 ключей и 334 общих префикса. Он неплохо умеет не создавать громадных откликов.

  • Напротив, при перечислении dirs/ каждый ответ от paginator содержит 1000 общих префиксов (и без ключей).

  • Передача ограничения в виде PaginationConfig={'MaxItems': limit} ограничивает только количество ключей, а не общие префиксы. Мы имеем дело с этим путем дальнейшего сокращения потока нашего итератора.

Ответ 5

У меня была та же проблема, но мне удалось ее решить, используя boto3.client и list_objects_v2 с параметрами Bucket и 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']

Результат вывода для вышеприведенного кода отображает следующее:

firstlevelFolder/secondLevelFolder/item1
firstlevelFolder/secondLevelFolder/item2

Boto3 list_objects_v2 Документация

Чтобы исключить только имя каталога для secondLevelFolder я просто использовал метод 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]

Результат вывода для вышеприведенного кода отображает следующее:

secondLevelFolder
secondLevelFolder

Документация Python split()

Если вы хотите получить имя каталога и имя элемента содержимого, замените строку печати следующим текстом:

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

И будет выведено следующее:

secondLevelFolder/item2
secondLevelFolder/item2

Надеюсь это поможет

Ответ 6

Большая реализация с S3 состоит в том, что нет никаких папок/каталогов, только ключи. Кажущаяся структура папок просто добавляется к имени файла, чтобы стать "ключом", поэтому для просмотра содержимого myBucket some/path/to/the/file/ вы можете попробовать:

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

что даст вам что-то вроде:

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

Ответ 8

Следующие работы для меня... Объекты S3:

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

Использование:

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

получаем:

form1/
form2/
...

С

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

получаем:

form1/section11/
form1/section12/

Ответ 9

AWS cli делает это (предположительно без выборки и повторения всех ключей в ковше), когда вы запускаете aws s3 ls s3://my-bucket/, поэтому я решил, что должен быть способ с помощью boto3.

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

Похоже, что они действительно используют Prefix и Delimiter - мне удалось написать функцию, которая бы доставила мне все каталоги на корневом уровне ведра, немного изменив этот код:

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

Ответ 10

Прежде всего, в S3 нет реальной концепции папки. У вас определенно есть файл @ '/folder/subfolder/myfile.txt' и ни папки, ни подпапки.

Чтобы "имитировать" папку в S3, вы должны создать пустой файл с именем "/" в конце его имени (см. Amazon S3 boto - как создать папку?)

Для вашей проблемы вы, вероятно, должны использовать метод get_all_keys с двумя параметрами: prefix и 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)

Ответ 11

Использование boto3.resource

Это основано на ответе Иц-Ажара на применение необязательного limit. Очевидно, что его существенно проще использовать, чем версию 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__)
_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)

Использование boto3.client

Он использует list_objects_v2 и основывается на ответе CpILL, что позволяет извлекать более 1000 объектов.

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)