文档中心 对象存储 操作指南 使用python boto3 SDK访问S3对象存储
在这篇文章中:

    使用python boto3 SDK访问S3对象存储

    boto3 SDK简介

    Boto3是亚马逊AWS提供的python SDK,最为常用的功能是S3对象存储的访问。交大云对象存储兼容S3 API, 可以使用该标准的S3 SDK进行访问。本文介绍如何使用 Boto3 SDK访问对象存储服务, 包括软件安装、管理存储空间、 管理文件。更多boto3访问对象存储的介绍,请参看:https://boto3.amazonaws.com/v1/documentation/api/latest/index.html

    示例中本地操作系统为CentOS8,其他系统请按需修改准备工作中软件安装命令。

    本文部分结果的验证用到了s3 browser,了解如何使用s3 browser访问对象存储:https://jcloud.sjtu.edu.cn/document/detail.html?mod=cos&id=1232

    软件安装

    1. 安装python,建议使用Python 3.6及以后的版本。

      1
      dnf install python3

      要验证安装,请输入以下命令检查Python版本。

      1
      python3 --version
    2. 安装python s3 boto3 SDK软件包

      1
      pip3 install boto3

    初始化

    Python SDK几乎所有的操作都是通过session.client()进行的。下面将详细说明如何初始化这个类。

    获取AK/SK

    使用boto3之前,需要先通过云平台控制台获取项目的的access key和secret_key,用于访问验证。

    1. 登录交大云平台:https://home.jcloud.sjtu.edu.cn

    2. 选择并确认需要创建对象存储的项目。

    3. 点击界面左侧的:存储、对象存储条目,进入对象存储管理界面。

    img

    1. 点击管理界面左上方的:获取AK/SK

    img

    确定EndPoint

    EndPoint是对象存储服务提供的服务地址及端口,交大云对象存储统一为:”https://s3.jcloud.sjtu.edu.cn:443"

    初始化session.client类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from boto3.session import Session
    import boto3 # 加载boto3 skd包

    # session.client初始化, access_key和secret_key填入上文获取的内容
    access_key = "您的AccessKey"
    secret_key = "您的SecretKey"
    url = "https://s3.jcloud.sjtu.edu.cn:443" # 交大云对象存储URL
    session = Session(access_key, secret_key)
    s3_client = session.client('s3', endpoint_url=url)

    代码解释:

    • access_key和secret_key是在对象存储服务中申请到的用于访问对象存储的认证信息。
    • url是对象存储服务提供的服务地址及端口,固定为:https://s3.jcloud.sjtu.edu.cn:443。
    • session对象承载了用户的认证信息。
    • session.client()对象用于服务相关的操作。

    管理存储空间

    存储空间(Bucket)是对象存储服务器上的命名空间,也是计费,权限控制,日志记录等高级功能的管理实体。

    1. 查看Bucket列表

    创建python文件ls_bucket.py 输入以下内容,并保存。

    1
    2
    3
    4
    5
    ... #初始化session.client()

    if __name__ == '__main__':
    for bucket in s3_client.list_buckets()['Buckets']:
    print([bucket['Name']])

    执行命令:

    1
    python3 ls_bucket.py

    执行结果:

    1
    ['01-bucket']

    2. 创建Bucket

    桶的名字必须是唯一的,如果Bucket已经被其他用户使用,则会创建失败。

    创建python文件create_bucket.py ,输入以下内容,并保存。

    1
    2
    3
    4
    5
    6
    7
    8
    ... #初始化session.client()

    if __name__ == '__main__':
    # 默认创建私有桶
    s3_client.create_bucket(Bucket = 'private_own_bucket')

    # 创建公开可读桶
    s3_client.create_bucket(Bucket = '02-bucket', ACL = 'public-read')
    • ACL可选:private, public-read, public-read-write authenticated-read。

    执行命令:

    1
    $python3 create_bucket.py

    查看结果:

    1
    2
    3
    4
    $python3 ls_bucket.py
    ['01-bucket']
    ['02-bucket']
    ['private_own_bucket']

    3. 查看Bucket内对象个数和使用空间

    1
    2
    3
    4
    5
    6
    ... #初始化session.client()

    if __name__ == '__main__':
    resp = s3_client.head_bucket(Bucket = "first-bucket")
    print('使用空间(字节):' + str(resp['ResponseMetadata']['HTTPHeaders']['x-rgw-bytes-used']))
    print('桶内对象个数: '+ str(resp['ResponseMetadata']['HTTPHeaders']['x-rgw-object-count']))

    执行命令:

    1
    2
    3
    $python3 resource_bucket.py 
    使用空间(字节):532
    桶内对象个数: 4

    4. 查看Bucket访问权限

    创建python文件query_acl.py 输入以下内容,并保存。

    1
    2
    3
    4
    5
    ... #初始化session.client()

    if __name__ == '__main__':
    res = s3_client.get_bucket_acl(Bucket = '02-bucket')
    print(res['Grants'])

    执行命令:

    1
    python3 query_acl.py

    执行结果:

    1
    2
    3
    [{'Grantee': {'Type': 'Group', 'URI': 'http://acs.amazonaws.com/groups/global/AllUsers'},
    'Permission': 'READ'}, {'Grantee': {'DisplayName': '****_project', 'ID': '*************',
    'Type': 'CanonicalUser'}, 'Permission': 'FULL_CONTROL'}]

    5. 设置Bucket访问权限

    创建python文件set_acl.py 输入以下内容,并保存。

    1
    2
    3
    4
    ... #初始化session.client()

    if __name__ == '__main__':
    res = s3_client.put_bucket_acl(Bucket = '02-bucket', ACL = 'public-read-write')

    执行命令:

    1
    python3 set_acl.py

    查看结果:

    1
    2
    3
    4
    5
    6
    $python3 query_acl.py 
    [{'Grantee': {'Type': 'Group', 'URI': 'http://acs.amazonaws.com/groups/global/AllUsers'},
    'Permission': 'READ'}, {'Grantee': {'Type': 'Group', 'URI':
    'http://acs.amazonaws.com/groups/global/AllUsers'}, 'Permission': 'WRITE'}, {'Grantee':
    {'DisplayName': '****_project', 'ID': '*************', 'Type': 'CanonicalUser'},
    'Permission': 'FULL_CONTROL'}]

    6. 删除bucket

    删除指定的桶。只有该桶中所有的对象被删除了,该桶才能被删除。另外,只有该桶的拥有者才能删除它,与桶的访问控制权限无关。

    创建python文件del_bucket.py 输入以下内容,并保存。

    1
    2
    3
    4
    5
    6
    7
    8
    ... #初始化session.client()
    from botocore.exceptions import ClientError

    if __name__ == '__main__':
    try:
    res = s3_client.delete_bucket(Bucket = '01-bucket')
    except ClientError as e:
    print (e.response['Error']['Code'])

    执行命令:

    1
    python3 del_bucket.py

    查看结果:

    1
    2
    3
    $python3 ls_bucket.py 
    ['02-bucket']
    ['private_own_bucket']

    管理文件

    通过Python SDK 用户可以上传、下载、罗列、删除、拷贝文件。也可以查看文件信息,更改文件元信息等。

    1. 上传文件

    对象存储有多种上传方式,不同的上传方式能够上传的数据大小也不一样。普通上传(PutObject)最多只能上传小于或者等于5GB的文件;而分片上传(MultipartUpload)每个分片可以达到5GB,合并后的文件能够达到5TB.

    首先介绍普通上传,我们会详细展示提供数据的各种方式,即方法中的data参数。其他上传接口类似的data参数。

    上传字符串

    上传内存中的字符串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ... #初始化session.client()

    if __name__ == '__main__':
    # 上传内存中的字符串
    res = s3_client.put_object(Bucket = '02-bucket', Key = 'local01.txt', Body =
    'local01')

    # 可以执行上传的是bytes
    res = s3_client.put_object(Bucket = '02-bucket', Key = 'local02.txt', Body = b'0101')

    # 可以执行为unicode
    res = s3_client.put_object(Bucket = '02-bucket', Key = 'local03.txt', Body = u'嗨,你
    好')

    执行命令:

    1
    python3 upload_str.py

    执行结果:

    1
    2
    3
    4
    5
    # 利用s3cmd查看文件列表
    $ s3cmd ls s3://02-bucket
    2021-12-31 05:30 133 s3://02-bucket/local01.txt
    2021-12-31 05:27 4 s3://02-bucket/local02.txt
    2021-12-31 05:27 12 s3://02-bucket/local03.txt

    上传本地文件

    把当前目录下的local01.txt文件上传到对象存储服务器

    1
    2
    3
    4
    5
    ... #初始化session.client()

    if __name__ == '__main__':
    res = s3_client.put_object(Bucket = '02-bucket', Key = 'local01.txt', Body =
    open("local01.txt")

    执行命令:

    1
    python3 upload_file.py

    执行结果:

    1
    2
    3
    4
    5
    6
    # 利用s3cmd查看文件列表
    $ s3cmd ls s3://02-bucket
    2021-12-31 05:36 133 s3://02-bucket/local.txt
    2021-12-31 05:30 133 s3://02-bucket/local01.txt
    2021-12-31 05:27 4 s3://02-bucket/local02.txt
    2021-12-31 05:27 12 s3://02-bucket/local03.txt

    分片上传

    当需要上传的本地文件很大,或者网络状况不理想,往往会出现上传到中途就失败了。此时,如果对已经上传的数据重新上传,既浪费时间又占用了网络资源。Python SDK提供了分片上传接口,用于断点续传本地文件或者并发上传文件不同的区域块来加速上传。

    采用分片上传,用户可以对上传做更为精细的控制。这适用于诸如预先不知道文件大小、并发上传、自定义断点续传等场景。一次分片上传可以分为三个步骤:

    • 初始化(createMultipartUpload): 获得Upload ID。
    • 上传分片(uploadPart) :这一步可以并发执行。
    • 完成上传(completeMultipartUpload):合并分片,生成文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    ... #初始化session.client()

    from boto3.session import Session
    import boto3

    #准备上传的文件
    file = open('./testfile.docx','rb')
    part_info = {
    'Parts': []
    }
    #分片上传功能的函数
    def create_multipart_upld():
    # step1:初始化
    response = s3_client.create_multipart_upload(
    Bucket = '02-bucket', Key = 'testfile')
    part_no = 1 # 用于标识分片上传时的数据块号,即 PartNumber
    with open('/root/testfile.docx', 'rb') as f:
    while True:
    bytes_data = f.read(1024 * 1024 * 6)
    if not bytes_data:
    break
    print('upload ID:', response['UploadId'])
    #step2:上传分片
    part = s3_client.upload_part(
    Body=bytes_data,
    Bucket='02-bucket',
    Key='testfile',
    PartNumber=part_no, # 这个号要唯一,否则会把前面相同的PartNumber数据覆盖掉
    UploadId=response['UploadId'],
    )
    # 每个 PartNumber 和 ETag 要对应起来
    part_info['Parts'].append({
    'PartNumber': part_no,
    'ETag': part['ETag']
    })
    part_no += 1
    print('part_info:', part_info)

    # step3:完成上传
    complete_rsp = s3_client.complete_multipart_upload(Bucket='02-bucket',
    Key='testfile',
    UploadId=response['UploadId'],
    MultipartUpload=part_info)
    print('complete_rsp:', complete_rsp)

    if __name__ == '__main__':
    create_multipart_upld()

    执行命令:

    1
    python3 multi_upload_file.py

    执行结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    part_info: {'Parts': [{'PartNumber': 1, 'ETag': '"fad477c7c37c930be51b07f35dfda1de"'},
    {'PartNumber': 2, 'ETag': '"eb6cc5a24133f05681cd6725b4e22ca7"'}]}
    complete_rsp: {'ResponseMetadata': {'RequestId': 'tx000000000000010fa604d-0061d53872-
    41dd138-zone-00864d', 'HostId': '', 'HTTPStatusCode': 200, 'HTTPHeaders': {'server':
    'openresty/1.13.6.1', 'date': 'Wed, 05 Jan 2022 06:19:30 GMT', 'content-type':
    'application/xml', 'content-length': '253', 'connection': 'keep-alive', 'x-amz-request-
    id': 'tx000000000000010fa604d-0061d53872-41dd138-zone-00864d', 'access-control-allow-
    origin': '*', 'access-control-allow-methods': 'POST, GET, OPTIONS, DELETE, PUT, PATCH',
    'access-control-allow-headers': 'content-type,x-amz-server-side-encryption,range,x-amz-
    user-agent,x-amz-copy-source,x-amz-content-sha256,x-amz-date,authorization,x-amz-
    acl,etag,content-encoding,x-requested-with'}, 'RetryAttempts': 0}, 'Bucket': '02-bucket',
    'Key': 'testfile', 'ETag': '"282963790da05e43425250eb604359ed-2"'}

    2. 下载文件

    下载到本地文件

    把对象存储服务器上指定bucket里面的文件下载到本地文件

    1
    2
    3
    4
    5
    6
    ... #初始化session.client()

    if __name__ == '__main__':
    res = s3_client.get_object(Bucket = '02-bucket', Key = 'local03.txt')
    with open('down-local-03.txt', 'wb') as f:
    f.write(res['Body'].read())

    执行命令:

    1
    python3 download_file.py

    查看结果:

    1
    2
    $ cat down-local-03.txt
    嗨,你好

    下载部分内容

    下载对象存储服务器上指定bucket里面的文件的前10个字节:

    1
    2
    3
    4
    5
    6
    7
    ... #初始化session.client()

    if __name__ == '__main__':
    res = s3_client.get_object(Bucket = '02-bucket', Key = 'local03.txt', Range='bytes=0-
    4')
    with open('down-local-03.txt', 'wb') as f:
    f.write(res['Body'].read())

    执行命令:

    1
    python3 download_part.py

    查看结果:

    1
    2
     cat down-local-03.txt

    断点续传

    当需要下载的文件很大,或者网络状况不够理想,往往下载到中途就失败了。如果下次重试,还需要重新下载,就会浪费时间和带宽。断点续传的过程大致如下:

    1
    2
    3
    1. 在本地创建一个临时文件,文件名由原始文件名加上一个随机的后缀组成。
    2. 通过制定HTTP请求的Range头,按照范围读取对象存储服务器上的文件,并写入到文件里相应的位置。
    3. 下载完成后,把临时文件重命名为目标文件。

    3.查看文件

    罗列全部文件列表

    查看指定bucket里所有文件列表

    1
    2
    3
    4
    5
    6
    ... #初始化session.client()

    if __name__ == '__main__':
    res = s3_client.list_objects(Bucket = '02-bucket')
    for obj in res['Contents']:
    print (obj['Key'])

    执行结果:

    1
    2
    3
    4
    5
    $ python ls_bucket_file.py
    local.txt
    local01.txt
    local02.txt
    local03.txt

    按前缀罗列文件列表

    只列举前缀为’local0’的所有文件

    1
    2
    3
    4
    5
    6
    ... #初始化session.client()

    if __name__ == '__main__':
    res = s3_client.list_objects(Bucket = '02-bucket', Prefix = 'local0')
    for obj in res['Contents']:
    print (obj['Key'])

    执行结果:

    1
    2
    3
    4
    $ python3 ls_bucket_file_part.py
    local01.txt
    local02.txt
    local03.txt

    模拟文件夹

    对象存储的存储空间(Bucket)本身是扁平结构,并没有文件夹或目录的概念。用户可以通过在文件名里加入“/”来模拟文件夹。在列举的时候,则要设置delimiter参数(目录分隔符)为“/”,并通过是否“在CommonPrefixes”来判断是否为文件夹,例如:02-bucket下结构如下:

    1
    2
    3
    4
    5
    6
    s3cmd ls s3://02-bucket/
    DIR s3://02-bucket/test/
    2021-12-31 05:36 133 s3://02-bucket/local.txt
    2021-12-31 05:30 133 s3://02-bucket/local01.txt
    2021-12-31 05:27 4 s3://02-bucket/local02.txt
    2021-12-31 05:27 12 s3://02-bucket/local03.txt

    列举02-bucket下的文件夹和文件(不递归列出子文件夹下的文件和子文件夹)

    1
    2
    3
    4
    5
    6
    7
    8
    ... #初始化session.client()

    if __name__ == '__main__':
    res = s3_client.list_objects(Bucket = '02-bucket', Delimiter = '/')
    for obj in res.get('CommonPrefixes'):
    print(obj.get('Prefix'))
    for obj in res.get('Contents'):
    print(obj.get('Key'))

    执行结果:

    1
    2
    3
    4
    5
    6
    7
    $ python ls_bucket_file.py
    /
    test/
    local.txt
    local01.txt
    local02.txt
    local03.txt

    列举出02-bucket下的文件夹test下的文件夹和文件(不递归列出文件夹下的文件和文件夹)

    1
    2
    3
    4
    5
    6
    7
    8
    ... #初始化session.client()

    if __name__ == '__main__':
    res = s3_client.list_objects(Bucket = '02-bucket', Delimiter = '/', Prefix='test/')
    for obj in res.get('CommonPrefixes'):
    print(obj.get('Prefix'))
    for obj in res.get('Contents'):
    print(obj.get('Key'))

    执行结果:

    1
    2
    3
    python ls_bucket_file.py
    test/sub_tags/
    test/local0-01.txt

    查看文件是否存在

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ... #初始化session.client()
    from botocore.exceptions import ClientError

    if __name__ == '__main__':
    try:
    s3_client.head_object(Bucket = '02-bucket', Key = 'local01.txt')
    print('EXITS')
    except:
    print('NOT FOUND')

    查询结果:

    1
    2
    $ python has_file.py
    EXITS

    查看文件访问权限

    1
    2
    3
    4
    5
    6
    ... #初始化session.client()

    if __name__ == '__main__':
    res = s3_client.get_object_acl(Bucket = '01-bucket', Key = 'local03.txt')
    print(res['Grants'])
    print(res['Owner'])

    执行结果:

    1
    2
    3
    4
    $  python3 query_acl_file.py 
    [{'Grantee': {'DisplayName': '****_project', 'ID': '7a3856dc8df348c1ae22dd58d4197749',
    'Type': 'CanonicalUser'}, 'Permission': 'FULL_CONTROL'}]
    {'DisplayName': '****_project', 'ID': '7a3856dc8df348c1ae22dd58d4197749'}

    4. 设置文件

    设置文件访问权限

    1
    2
    3
    4
    5
    ... #初始化session.client()

    if __name__ == '__main__':
    res = s3_client.put_object_acl(Bucket = '02-bucket', ACL = 'public-read-write',
    Key = 'local03.txt')

    执行结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    $ s3cmd info s3://02-bucket/local03.txt
    s3://02-bucket/local03.txt (object):
    File size: 12
    Last mod: Fri, 31 Dec 2021 06:42:58 GMT
    MIME type: binary/octet-stream
    Storage: STANDARD
    MD5 sum: 402cd29fb7d4d73855c517d3e7acee0a
    SSE: none
    Policy: none
    CORS: none
    ACL: *anon*: READ
    ACL: *anon*: WRITE
    ACL: ***_project: FULL_CONTROL
    URL: http://s3.jcloud.sjtu.edu.cn:443/02-bucket/local03.txt

    设置元数据

    上传文件并且设置元数据

    1
    2
    3
    4
    5
    6
    ... #初始化session.client()

    if __name__ == '__main__':
    #上传对象并且设置元数据
    s3_client.put_object(Bucket = '02-bucket', Key = 'big-file', Metadata = {
    'key1': 'value1', 'key2': 'value2'}, Body = 'big-file.txt')

    查看结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    s3cmd info s3://02-bucket/big-file
    s3://02-bucket/big-file (object):
    File size: 12
    Last mod: Fri, 31 Dec 2021 06:25:30 GMT
    MIME type: binary/octet-stream
    Storage: STANDARD
    MD5 sum: 03b038e5fbb30eaa5077e21ce1df44d2
    SSE: none
    Policy: none
    CORS: none
    ACL: ****_project: FULL_CONTROL
    x-amz-meta-key1: value1
    x-amz-meta-key2: value2

    修改已有对象的元数据

    1
    2
    3
    4
    5
    6
    7
    8
    ... #初始化session.client()

    if __name__ == '__main__':
    #python sdk目前对象没有post方法,因此建议在上传对象时就设置下面使用copy来实现对
    #已有的对象的元数据更新
    s3_client.copy_object(Bucket = '02-bucket', Key = 'big-file',
    CopySource = str('02-bucket/big-file'), MetadataDirective = 'REPLACE', Metadata =
    {'key1': 'replace-value1'})

    执行结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $ s3cmd info s3://02-bucket/big-file
    s3://02-bucket/big-file (object):
    File size: 12
    Last mod: Fri, 31 Dec 2021 06:28:43 GMT
    MIME type: binary/octet-stream
    Storage: STANDARD
    MD5 sum: 03b038e5fbb30eaa5077e21ce1df44d2
    SSE: none
    Policy: none
    CORS: none
    ACL: ****_project: FULL_CONTROL
    x-amz-meta-key1: replace-value1

    5. 删除文件

    删除单个文件

    1
    2
    3
    4
    ... #初始化session.client()

    if __name__ == '__main__':
    res = s3_client.delete_object(Bucket = '02-bucket', Key = 'big-file')

    执行命令:

    1
    $ python3 delete_file.py

    查看结果:

    1
    2
    3
    4
    5
    $ s3cmd ls s3://02-bucket
    2021-12-31 05:36 133 s3://02-bucket/local.txt
    2021-12-31 05:30 133 s3://02-bucket/local01.txt
    2021-12-31 05:27 4 s3://02-bucket/local02.txt
    2021-12-31 05:27 12 s3://02-bucket/local03.txt

    删除多个文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ... #初始化session.client()

    if __name__ == '__main__':
    objects_to_delete = [{'Key': 'local01.txt'}, {'Key': 'local02.txt'}]

    s3_client.delete_objects(
    Bucket = '02-bucket',
    Delete={
    'Objects': objects_to_delete
    }
    )

    执行命令:

    1
    $ python3 delete_files.py

    查看结果:

    1
    2
    3
    $ s3cmd ls s3://02-bucket
    2021-12-31 05:36 133 s3://02-bucket/local.txt
    2021-12-31 05:27 12 s3://02-bucket/local03.txt

    6. 拷贝文件

    将02-bucket下的local03.txt文件拷贝到first-bucket下且命名为target.txt文件

    1
    2
    3
    4
    5
    ... #初始化session.client()

    if __name__ == '__main__':
    res = s3_client.copy_object(Bucket = 'first-bucket', Key = 'target.txt',
    CopySource = str('02-bucket' + '/' + 'local03.txt'))

    执行命令:

    1
    $ python3 copy_file.py

    查看结果:

    1
    2
    3
    4
    5
    6
    $ s3cmd ls s3://first-bucket
    2021-12-31 06:02 133 s3://first-bucket/local.txt
    2021-12-31 06:04 133 s3://first-bucket/local01.txt
    2021-12-31 06:04 133 s3://first-bucket/local02.txt
    2021-12-31 06:04 133 s3://first-bucket/local03.txt
    2021-12-31 06:37 12 s3://first-bucket/target.txt