SimpleDateFormat 24h

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

  • Используйте 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'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

Тест:

Для проверки поведения paginator и list_objects полезно следующее. Он создает несколько файлов и файлов. Поскольку страницы содержат до 1000 записей, мы используем несколько для файлов и файлов. dirs содержит только каталоги (каждый из которых имеет один объект). mixed содержит смесь dirs и объектов с отношением 2 объекта для каждого dir (плюс один объект под dir, конечно, 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} ограничивает количество ключей, а не общих префиксов. Мы имеем дело с этим путем дальнейшего обрезания потока нашего итератора.

29
задан nhaarman 29 August 2011 в 11:42
поделиться