如何使用Google Cloud API获取给定存储桶中文件夹列表

34

我想使用Google Cloud Storage API获取给定Google Cloud bucket或文件夹中的所有文件夹。

例如,如果gs://abc/xyz包含三个文件夹gs://abc/xyz/x1gs://abc/xyz/x2gs://abc/xyz/x3。该API应返回gs://abc/xyz中所有三个文件夹。

可以轻松地使用gsutil完成此操作。

gsutil ls gs://abc/xyz

但我需要使用Python和Google Cloud Storage API来完成它。


2
你说你想获取xyz文件夹内的文件夹,但是命令gsutil ls gs://abc/xyz返回xyz中的所有对象,包括非文件夹项。那么,你要求的是什么?所有文件夹还是所有项目,包括文件夹? - Robino
15个回答

35

这个问题是关于列举存储桶(文件夹)内的子文件夹。针对所有建议都没有奏效,经过尝试使用google.cloud.storage软件开发工具包(SDK),我怀疑截至2019年11月,这是不可能的。但是使用REST API是可能的,因此我写了这个小包装器...

from google.api_core import page_iterator
from google.cloud import storage

def _item_to_value(iterator, item):
    return item

def list_directories(bucket_name, prefix):
    if prefix and not prefix.endswith('/'):
        prefix += '/'

    extra_params = {
        "projection": "noAcl",
        "prefix": prefix,
        "delimiter": '/'
    }

    gcs = storage.Client()

    path = "/b/" + bucket_name + "/o"

    iterator = page_iterator.HTTPIterator(
        client=gcs,
        api_request=gcs._connection.api_request,
        path=path,
        items_key='prefixes',
        item_to_value=_item_to_value,
        extra_params=extra_params,
    )

    return [x for x in iterator]
例如,如果您有包含 my-bucket 的内容:
  • dog-bark
    • datasets
      • v1
      • v2
然后调用 list_directories('my-bucket', 'dog-bark/datasets') 将返回: ['dog-bark/datasets/v1', 'dog-bark/datasets/v2'].

太棒了。我将编辑您的答案,将前几个“路径”实例替换为“前缀”,以避免与定义要传递给HTTPIterator的路径混淆。 - RNHTTR
看起来有点像黑客,使用“私有”成员_connection。使用list_blobs(..)会更容易/更安全的方法。 - Robino
我在前缀条件上添加了一点内容,以解决一个错误,即您无法使用''前缀列出存储桶的根目录 :) 除此之外,这对我来说完美地起作用了,感谢您的发布! - Yet Another User

17

这里是对回答帖子的更新:

from google.cloud import storage

# Instantiates a client
storage_client = storage.Client()

# Get GCS bucket
bucket = storage_client.get_bucket(bucket_name)

# Get blobs in bucket (including all subdirectories)
blobs_all = list(bucket.list_blobs())

# Get blobs in specific subirectory
blobs_specific = list(bucket.list_blobs(prefix='path/to/subfolder/'))

4
尽管这可能适用于列出对象,但本问题是关于列出子文件夹,并且这种方法并不能实现。@AntPhitlok的答案是正确的。 - RNHTTR
1
@RNHTTR。你说得对 :) 为了后人留存,我会把它留在这里。 - Ekaba Bisong

12
您可以使用Python GCS API客户端库。请参阅Google Cloud Storage示例和库文档页面以获取相关文档和下载链接。
在您的情况下,首先我要指出您混淆了“bucket”这个术语。建议阅读文档中的关键术语页面。您所谈论的是对象名称前缀。
您可以从GitHub上的list-objects.py示例开始。查看列表参考页面,您需要传递bucket=abcprefix=xyz/delimiter=/

3
当我们使用prefixdelimiter调用objects().list()时,我们会得到一组匹配物体和匹配前缀的列表。如@jterrace所回答的那样,如果我们在prefix=abc/xyzdelimiter=/中传递,则会获取所有名称以abc/xyz开头的对象以及前缀,这些前缀可以被逻辑上视为子文件夹。 - Shamshad Alam
我不理解这个答案。如果“url”是gs://abc/xyz,那么存储桶将是abc。如果您还在前缀中传递存储桶名称,则可能无法匹配任何内容,而且肯定不是您想要的。 - Robino
2
@Robino 你是对的 - 我搞砸了。已更新答案。 - jterrace

8

获取存储桶中文件夹列表,您可以使用下面的代码片段:

import googleapiclient.discovery


def list_sub_directories(bucket_name, prefix):
    """Returns a list of sub-directories within the given bucket."""
    service = googleapiclient.discovery.build('storage', 'v1')

    req = service.objects().list(bucket=bucket_name, prefix=prefix, delimiter='/')
    res = req.execute()
    return res['prefixes']

# For the example (gs://abc/xyz), bucket_name is 'abc' and the prefix would be 'xyz/'
print(list_sub_directories(bucket_name='abc', prefix='xyz/'))

2
OP要求使用google.cloud.storage API... - Robino

8

1. 获取客户端对象的访问权限。

代码在哪里运行?

我(某个位置)在Google Cloud Platform(GCP)内部

如果您正在从GCP内部访问Google Cloud Storage(GCS),例如Google Kubernetes Engine(GKE),则应使用工作负载身份将您的GKE服务帐号配置为充当GCS服务帐号。 https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity

一旦您完成这个步骤,创建您的客户端就像这样简单:

import google.cloud.storage as gcs
client = gcs.Client()

走出GCP

如果你不在GCP内,比如在AWS、Azure、本地开发环境或其它地方,那么需要选择以下两种方式之一:要么创建一个服务账号并下载该账号的json文件(其中含有加密的私钥),要么使用工作负载身份联合,如AWS、Azure等提供的功能。

假设你决定下载新的GCS服务账号文件至/secure/gcs.json

PROJECT_NAME = "MY-GCP-PROJECT"
from google.oauth2.service_account import Credentials
import google.cloud.storage as gcs
client = gcs.Client(
    project=PROJECT_NAME,
    credentials=Credentials.from_service_account_file("/secure/gcs.json"),
)

2. 发送列出文件夹的请求到GCS

在这个操作中,我们试图获取存储桶abc中路径xyz内的文件夹列表。请注意,GCS中的路径不像Linux那样以/开头,但是它们应该以一个/结束。因此,我们将查找带有前缀xyz/ 的文件夹。也就是说,只是文件夹,而不是包含所有子文件夹的文件夹。

BUCKET_NAME = "abc"
blobs = client.list_blobs(
    BUCKET_NAME,
    prefix="xyz/",  # <- you need the trailing slash
    delimiter="/",
    max_results=1,
)

注意我们只要求一个 blob,这不是一个错误:blobs 是文件本身,我们只对文件夹感兴趣。将 max_results 设置为零并不起作用,详见下文。

3. 强制懒加载工作!

这里的一些回答遍历了迭代器中的每个元素 blobs,可能会遍历成千上万个,但我们不需要这样做。话虽如此,如果我们没有遍历任何元素,则 blobs 不会进行向 GCS 发出 API 请求的操作。

next(blobs, ...) # Force blobs to load.
print(blobs.prefixes)
blobs变量是一个迭代器,最多包含一个元素,但是如果您的文件夹(在其级别上)没有文件,则可能没有元素。如果没有元素,则next(blobs)将引发StopIteration异常。
第二个参数,省略号...,只是我选择的默认返回值,如果没有下一个元素。我觉得这比None更可读,因为它向读者暗示着这里发生了值得注意的事情。毕竟,请求值后立即在同一行中丢弃它的代码具有潜在错误的所有标志,因此让我们向读者保证这是有意为之的是很好的。
最后,假设我们在xyz下有一个树形结构,即aaabbbccc,然后在ccc下面有子文件夹zzz。输出将会是:
{'xyz/aaa', 'xyz/bbb', 'xyz/ccc'}

请注意,根据 OP 的要求,我们不会看到子文件夹 xyz/ccc/zzz

6
我还需要简单地列出一个存储桶的内容。最好能够提供与tf.gfile类似的功能。tf.gfile支持确定条目是文件还是目录。
我尝试了@jterrace上面提供的各种链接,但我的结果并不理想。尽管如此,还是值得展示一下结果。
对于包含“目录”和“文件”的存储桶,很难浏览“文件系统”以找到感兴趣的项目。我已经在代码中提供了一些注释,说明上述引用的代码的工作原理。
在任何情况下,我都使用带有笔记本电脑凭据的数据实验室笔记本电脑。根据结果,我需要使用字符串解析来确定特定目录中的文件。如果有人知道如何扩展这些方法或类似于tf.gfile的解析目录的替代方法,请回复。
第一种方法
import sys
import json
import argparse
import googleapiclient.discovery

BUCKET = 'bucket-sounds' 

def create_service():
    return googleapiclient.discovery.build('storage', 'v1')


def list_bucket(bucket):
    """Returns a list of metadata of the objects within the given bucket."""
    service = create_service()

    # Create a request to objects.list to retrieve a list of objects.
    fields_to_return = 'nextPageToken,items(name,size,contentType,metadata(my-key))'
    #req = service.objects().list(bucket=bucket, fields=fields_to_return)  # returns everything
    #req = service.objects().list(bucket=bucket, fields=fields_to_return, prefix='UrbanSound')  # returns everything. UrbanSound is top dir in bucket
    #req = service.objects().list(bucket=bucket, fields=fields_to_return, prefix='UrbanSound/FREE') # returns the file FREESOUNDCREDITS.TXT
    #req = service.objects().list(bucket=bucket, fields=fields_to_return, prefix='UrbanSound/FREESOUNDCREDITS.txt', delimiter='/') # same as above
    #req = service.objects().list(bucket=bucket, fields=fields_to_return, prefix='UrbanSound/data/dog_bark', delimiter='/') # returns nothing
    req = service.objects().list(bucket=bucket, fields=fields_to_return, prefix='UrbanSound/data/dog_bark/', delimiter='/') # returns files in dog_bark dir

    all_objects = []
    # If you have too many items to list in one request, list_next() will
    # automatically handle paging with the pageToken.
    while req:
        resp = req.execute()
        all_objects.extend(resp.get('items', []))
        req = service.objects().list_next(req, resp)
    return all_objects

# usage
print(json.dumps(list_bucket(BUCKET), indent=2))

这会生成像这样的结果:
[
  {
    "contentType": "text/csv", 
    "name": "UrbanSound/data/dog_bark/100032.csv", 
    "size": "29"
  }, 
  {
    "contentType": "application/json", 
    "name": "UrbanSound/data/dog_bark/100032.json", 
    "size": "1858"
  } stuff snipped]

第二种方法

import re
import sys
from google.cloud import storage

BUCKET = 'bucket-sounds'

# Create a Cloud Storage client.
gcs = storage.Client()

# Get the bucket that the file will be uploaded to.
bucket = gcs.get_bucket(BUCKET)

def my_list_bucket(bucket_name, limit=sys.maxsize):
  a_bucket = gcs.lookup_bucket(bucket_name)
  bucket_iterator = a_bucket.list_blobs()
  for resource in bucket_iterator:
    print(resource.name)
    limit = limit - 1
    if limit <= 0:
      break

my_list_bucket(BUCKET, limit=5)

这会生成如下的输出。
UrbanSound/FREESOUNDCREDITS.txt
UrbanSound/UrbanSound_README.txt
UrbanSound/data/air_conditioner/100852.csv
UrbanSound/data/air_conditioner/100852.json
UrbanSound/data/air_conditioner/100852.mp3

这是真正的答案!谢谢。 - Maelstorm
3
OP要求类似于“gsutil ls ...”这样的行为,它可以列出文件夹中的项目。 你的代码会递归地列出所有子文件夹中的所有项目。对于大型文件夹结构,你可能会得到比你期望的更多的结果! - Robino

4

我遇到了同样的问题,并通过使用此处描述的标准list_blobs方法解决了它:

from google.cloud import storage

storage_client = storage.Client()

# Note: Client.list_blobs requires at least package version 1.17.0.
blobs = storage_client.list_blobs(
    bucket_name, prefix=prefix, delimiter=delimiter
)

print("Blobs:")
for blob in blobs:
    print(blob.name)

if delimiter:
    print("Prefixes:")
    for prefix in blobs.prefixes:
        print(prefix)

然而,在我阅读AntPhitlok的回答并理解必须确保我的前缀以/结尾并且我也在使用/作为分隔符后,这才对我起作用。

因此,在“Blobs:”部分下,如果存在于前缀文件夹下,则我们将仅获取文件名,而不是文件夹。 所有子目录都将列在“前缀:”部分下。

需要注意的是,blobs实际上是一个迭代器,因此为了获得子目录,我们必须“打开”它。因此,如果在我们的代码中省略“Blobs:”部分,将导致blobs.prefixes内为空的set()

编辑: 用法示例-假设我有一个名为buck的存储桶,并在其中有一个名为dir的目录。在dir内,我有另一个名为subdir的目录。

为了列出dir内的目录,我可以使用:

from google.cloud import storage

storage_client = storage.Client()
blobs = storage_client.list_blobs('buck', prefix='dir/', delimiter='/')

print("Blobs:")
for blob in blobs:
    print(blob.name)

if delimiter:
    print("Prefixes:")
    for prefix in blobs.prefixes:
        print(prefix)

*请注意在前缀结尾和分隔符处使用 /

这个调用将打印出以下内容:

Prefixes:
subdir/

你没有说明前缀或分隔符需要使用哪些值。能否请您在回答中添加这些信息? - Robino
1
@Robino 我添加了一个例子。前缀用于列出仅以 prefix 开头的文件和文件夹。当您只想列出特定目录中的文件和文件夹时,它非常有用。重要的是,前缀应以“/”结尾。然而,分隔符有助于在该目录中分隔文件和文件夹。正如我所写的那样,我已经使用“/”作为分隔符。 - PeNpeL
1
尝试使用最新的 Google Cloud Storage 1.35.1,但是 blobs.prefixes 总是返回空集合,而实际上有一些以 / 开头的前缀。 - Rui Yang
1
@RuiYang 我在1.42和1.43上遇到了相同的问题(在OSX和Linux上测试过)。 - Robino
这在最新版本1.43上无法工作。 - Boorhin
显示剩余3条评论

2
这里有一个简单的解决方案。
from google.cloud import storage # !pip install --upgrade google-cloud-storage
import os

# set up your bucket 
client = storage.Client()
storage_client = storage.Client.from_service_account_json('XXXXXXXX')
bucket = client.get_bucket('XXXXXXXX')

# get all the folder in folder "base_folder"
base_folder = 'model_testing'
blobs=list(bucket.list_blobs(prefix=base_folder))
folders = list(set([os.path.dirname(k.name) for k in blobs]))
print(*folders, sep = '\n')

如果您只想获取所选文件夹中的文件夹

base_folder = base_folder.rstrip(os.sep) # needed to remove any slashes at the end of the string 
one_out = list(set([base_folder+ os.sep.join(k.split(base_folder)[-1].split(os.sep)[:2]) for k in folders]))
print(*one_out, sep = '\n')

当然,可以使用替代方法。
list(set())

你可以使用numpy。
import numpy as np
np.unique()

1
这将循环遍历存储桶中的每个文件路径。对于大型存储桶,这将需要大量的时间。GCP还会根据查找次数向您收费,所以要小心! - Robino
1
谢谢你提醒我,为了避免这种情况,我是否可以使用类似于你的答案,并使用 max_results=1,像这样 blobs=list(bucket.list_blobs(max_results=1, prefix=base_folder)) - Phillip Maire

1

跟随https://stackoverflow.com/users/2350164/yet-another-user的回答,我已经使用"标准"谷歌代替HTTPIterator创建了相同的函数。 假设我们有一个名为'bucket_name'的Bucket和一个名为'sub_folder_name'的子文件夹。

from google.api_core import page_iterator
from google.cloud import storage
storage_client = storage.Client(project = PROJECT_NAME)
def get_folders_list(storage_client, bucket_or_name, prefix = ''):
        """
        the function returns the list of folders within a bucket or its subdirectory
        :param storage_client: the GCS client
        :param bucket_or_name: the name of the bucket
        :param prefix: the prefix if you want subdirectory
        :return: list of folders
        """
        if prefix and not prefix.endswith('/'):
            prefix += '/'

    blobs = storage_client.list_blobs(
        bucket_or_name=bucket_or_name,
        prefix=prefix,
        delimiter="/",
        # max_results=1
    )
    next(blobs, ...)
    return list(blobs.prefixes)

您可以使用以下两个示例来针对存储桶或其子目录之一:

get_folders_list(storage_client = storage_client, bucket_or_name =
   'bucket_name')
get_folders_list(storage_client = storage_client, bucket_or_name = 'bucket_name', prefix = 'sub_folder_name')

1

您可以使用Python云存储库和一行代码来获取存储桶内深度为N的所有唯一前缀,例如当N=2时:

set(["/".join(blob.name.split('/',maxsplit=2)[0:2]) for blob in client.list_blobs(BUCKET_NAME)])

如果您想将搜索结果限制在特定的“文件夹”中,请添加前缀,例如:
set(["/".join(blob.name.split('/',maxsplit=2)[0:2]) for blob in client.list_blobs(BUCKET_NAME, prefix=PREFIX)])

因为你的前缀将是一个或多个级别,所以你需要调整N。例如,要获取已经有1个级别深度的前缀内2个级别深度的唯一前缀,N应该为3。
我也很惊讶没有人在这个帖子中提到gcsfs库,它允许你这样做。
gcs = gcsfs.GCSFileSystem()
gcs.ls(BUCKET_NAME)

我也很惊讶,没有人提到gcsfs。出于这个原因,这个答案应该得到支持。 - mac13k

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