使用boto3从S3存储桶中检索子文件夹名称

177

使用boto3包,我可以访问我的AWS S3存储桶:

s3 = boto3.resource('s3')
bucket = s3.Bucket('my-bucket-name')
现在,这个桶中包含文件夹first-level,它本身包含几个以时间戳命名的子文件夹,例如1456753904534
我需要知道这些子文件夹的名称,以便进行另一个工作,我想知道是否可以让boto3为我检索它们。
所以我尝试了:
objs = bucket.meta.client.list_objects(Bucket='my-bucket-name')

提供的字典中,键为“Contents”的值给出了所有第三级文件而不是第二级时间戳目录的列表,实际上,我得到的是一个包含以下内容的列表:

{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': 'id'},
u'Size': size, u'StorageClass': 'storageclass'}

您可以看到,特定的文件,在这种情况下是part-00014,被检索出来,而我想只获取目录名。原则上,我可以从所有路径中剥离出目录名,但这很丑陋,并且获取所有第三级内容来获取第二级内容是昂贵的!

我还尝试了一些在这里报告的方法:

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

但是我没有得到所需级别的文件夹。

有没有解决这个问题的方法?


所以你的意思是这个不起作用?你能发一下运行时的情况吗? - Jordon Phillips
1
@JordonPhillips 我已经尝试了你发送的链接的前几行代码,我将它们粘贴到这里,但我只得到了存储桶最顶层目录下的文本文件,没有子文件夹。 - mar tin
@mar tin,你解决了这个问题吗?我面临着类似的困境,需要获取每个桶子文件夹中的第一个元素。 - Ted Taylor of Life
1
@TedTaylorofLife 是的,没有其他方法,只能获取所有对象并按“/”拆分以获取子文件夹。 - mar tin
1
@马丁,我唯一的做法是将输出转换为文本格式并用“ /”逗号分隔,然后复制和粘贴第一个元素。真是太麻烦了。 - Ted Taylor of Life
我尝试使用上述代码行,但出现了AttributeError: Error,例如AttributeError:'S3'对象没有'Bucket'属性 - a zEnItH
20个回答

177

以下代码仅返回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


17
如果我想列出特定子文件夹的内容,该怎么办? - azhar22k
2
@azhar22k,我认为你可以为每个“子文件夹”递归运行该函数。 - Serban Cezar
15
如果有超过1000个不同的前缀,会怎样? - Kostrahb

118

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对象名称使用“/”作为分隔符进行存储,较新版本的list_objects(list_objects_v2)允许您将响应限制为以指定前缀开头的键。

要将项限制为某些子文件夹下的项:

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

文档

另一个选项是使用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将为您提供对所使用资源的黑匣子层。


3
这让我得到了与我在问题中尝试时得到的相同结果。我猜我将不得不通过获取返回对象中的所有键并拆分字符串来获得文件夹名,以艰难的方式解决它。 - mar tin
2
@martina:一个懒惰的Python分割并获取列表中的最后一个数据,例如: filename = keyname.split("/")[-1] - mootmoot
2
@martin directory_name = os.path.dirname(directory/path/and/filename.txt)file_name = os.path.basename(directory/path/and/filename.txt) - jkdev
过于拘泥,但“真实目录结构”不是很清楚的意思。S3的目录抽象比典型的文件系统要薄,但它们都只是抽象,对于OP在这里的目的来说,在功能上是相同的。 - jberryman
真的想使用 os.path 实用程序来操作桶键吗?我在寻找一种合法的方式来编辑 S3 存储桶路径时发现了这个问题,正是为了避免自制的路径拼接和/或使用 os.path,它在 Linux 上似乎有点工作,但在 Windows 上肯定会失败,并且在概念上“完全错误”。 - nclark
对于具有许多对象的桶,all_objects = s3.list_objects(Bucket = 'bucket-name') 不会 检索所有对象(文档 - Petr Vepřek

86

简短回答:

  • 使用Delimiter='/'。这避免了对存储桶进行递归列出。在此处的某些答案错误地建议列出全部内容并使用某些字符串操作来检索目录名称。这可能非常低效。请记住,S3对存储桶中包含的对象数量几乎没有限制。因此,假设在bar / foo / 之间有一万亿个对象:您将等待很长时间才能获取['bar /', 'foo /']

  • 使用Paginators。出于同样的原因(S3是工程师对无限的近似),您必须通过页面进行列表,并避免将所有列表存储在内存中。相反,将您的“lister”视为迭代器,并处理其生成的流。

  • 使用boto3.client,而不是boto3.resourceresource版本似乎无法很好地处理Delimiter选项。如果您有一个资源,比如bucket = boto3.resource('s3').Bucket(name),则可以使用以下命令获取相应的客户端:bucket.meta.client

长回答:

以下是我用于简单桶(无版本处理)的迭代器。

import os
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('bucket-name')

        # iterate through all S3 objects under some dir
        >>> for p in s3list(bucket, 'some/dir'):
        ...     print(p)

        # iterate through up to 20 S3 objects under some dir, starting with foo_0010
        >>> for p in s3list(bucket, 'some/dir', limit=20, start='foo_0010'):
        ...     print(p)

        # non-recursive listing under some dir:
        >>> for p in s3list(bucket, 'some/dir', recursive=False):
        ...     print(p)

        # non-recursive listing under some dir, listing only dirs:
        >>> for p in s3list(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

测试:

以下内容有助于测试paginatorlist_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

稍微调整以上给出的代码以检查paginator响应结果,你可以观察到一些有趣的事实:

  • Marker真的很独特。给定Marker=topdir + 'mixed/0500_foo_a'将使列表从该键之后开始(根据Amazon S3 API),即从.../mixed/0500_foo_b开始。这就是__prev_str()的原因。

  • 使用Delimiter列出mixed/时,每个paginator响应包含666个键和334个公共前缀。 它非常擅长不构建巨大的响应。

  • 相比之下,列出dirs/时,每个paginator响应包含1000个公共前缀(没有键)。

  • 传递限制形式的PaginationConfig={'MaxItems': limit}仅限制键的数量,而不是公共前缀。 我们通过进一步截断迭代器的流来处理这个问题。


1
@Mehdi:对于一个提供如此惊人的规模和可靠性的系统来说,它真的不是很复杂。如果你曾经处理过超过几百TB的数据,你就会对他们所提供的内容有所欣赏。请记住,驱动器的MTBF始终> 0...考虑一下大规模数据存储的影响。免责声明:我是一名积极而快乐的AWS用户,除了我从2007年开始处理PB级别的数据并且以前更难之外,没有其他联系。 - Pierre D
2
为您的代码添加修复。如果有人想要列出存储桶中的所有目录,非递归方式,他们将发送以下内容:s3list(bucket, '', recursive=False, list_objs=False)。因此,我在 if not path.endswith('/') 中添加了 and len(path) > 0: - bladefist
像使用“kwargs”一样。这是一个不错的技巧,可以避免在有和没有ContinuationToken的情况下重复使用list_objects_v2。 - Andrey
只是想澄清一下,我的理解是列出的目录不保证唯一性?我猜在同一页内,commonPrefixes 只包含唯一的前缀,但在两个不同的页面之间,有些前缀可能会重复。 - Темирлан Мырзахметов
只有在 recursive=False, list_dirs=True 的情况下才会列出目录。列表中没有重复项。 - Pierre D
我确认“资源版本似乎无法很好地处理定界符选项”是非常正确的。 - hzitoun

55

我花了很多时间才找到了一个简单的方法来使用boto3列出S3存储桶中子文件夹的内容。希望这能有所帮助。

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
如果你的文件夹包含大量的对象,会怎么样? - Pierre D
6
我的观点是,这是一个非常低效的解决方案。S3被构建来处理键中任意的分隔符,例如 '/'。这可以让您跳过充满对象的“文件夹”,而无需对它们进行分页。即使您坚持要完整列表(即AWS CLI中的“递归”等效项),那么您也必须使用分页器,否则您只会列出前1000个对象。 - Pierre D
1
这是一个很好的答案。对于那些需要它的人,我在我的衍生答案中应用了一个limit - Asclepius
1
这是一个很棒的答案!有时候,我们并不关心性能,而是关心易于维护的简单代码。这段代码非常简单,而且运行得非常好! - mvallebr

21

我曾遇到同样的问题,但通过使用boto3.clientlist_objects_v2以及BucketStartAfter参数成功解决了。

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 行替换为以下内容:

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

以下内容将被输出:

secondLevelFolder/item2
secondLevelFolder/item2

希望这可以帮到你


19

S3的重要认识是没有文件夹/目录,只有键。所谓的文件夹结构只是在文件名前添加作为“键”的内容。因此,要列出myBucketsome/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
...

这是一个很好的答案,但它只能检索最多1000个对象。我已经提供了一个衍生答案,可以检索更多的对象。 - Asclepius
1
是的,@Acumenus,我想你的答案更复杂。 - CpILL

17

以下内容适用于我的 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/

这是唯一对我有效的解决方案,因为我需要“文件夹”在存储桶的根目录中,前缀必须为''",否则必须以"/"结尾。 - Oliver

8
AWS cli可以通过运行aws s3 ls s3://my-bucket/来实现这一点(可能不需要获取并迭代存储桶中的所有键),因此我想必须有一种使用boto3的方法。

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

看起来他们确实使用前缀和分隔符 - 我能够通过稍微修改那段代码编写一个函数,来获取存储桶根目录下的所有目录:

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

8

为什么不使用s3path包,使其像使用pathlib一样方便呢?但如果必须使用boto3

使用boto3.resource

这是在itz-azhar的回答基础上构建的,可以应用可选的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://dev59.com/uFsV5IYBdhLWcg3wwxSX#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查询S3存储桶中的对象,并在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://dev59.com/uFsV5IYBdhLWcg3wwxSX#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)

1
那个s3path库真是救命稻草!非常感谢你! - asosnovsky

3

对于我来说,这个方法在检索存储桶下面的一级文件夹时效果很好:

client = boto3.client('s3')
bucket = 'my-bucket-name'
folders = set()

for prefix in client.list_objects(Bucket=bucket, Delimiter='/')['CommonPrefixes']:
    folders.add(prefix['Prefix'][:-1])
    
print(folders)

您可以使用列表来做相同的事情,而不是集合,因为文件夹名称是唯一的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接