如何在AWS S3中不下载zip文件的情况下计算其中的文件数量?

9

案例: 在S3存储桶中,有一个包含大量图像的大型zip文件。是否有一种方法可以不下载整个文件,读取元数据或其他内容,以知道zip文件中有多少个文件?

当文件在本地时,在Python中我可以将其打开为zipfile(),然后调用namelist()方法,该方法返回所有文件的列表,我可以对其进行计数。但是不确定在文件驻留在S3中时如何做到这一点而无需下载它。如果使用Lambda实现可能最好。


如果您想以最小的带宽使用量来完成此操作(尽管可能还可以进行优化;)),请查看此处使用的列表逻辑 - Janaka Bandara
有一个适用于 .net 环境的 github 项目,供任何需要的人使用。 - hkutluay
5个回答

10

我认为这将解决你的问题:

import zlib
import zipfile
import io

def fetch(key_name, start, len, client_s3):
    """
    range-fetches a S3 key
    """
    end = start + len - 1
    s3_object = client_s3.get_object(Bucket=bucket_name, Key=key_name, Range="bytes=%d-%d" % (start, end))
    return s3_object['Body'].read()


def parse_int(bytes):
    """
    parses 2 or 4 little-endian bits into their corresponding integer value
    """
    val = (bytes[0]) + ((bytes[1]) << 8)
    if len(bytes) > 3:
        val += ((bytes[2]) << 16) + ((bytes[3]) << 24)
    return val


def list_files_in_s3_zipped_object(bucket_name, key_name, client_s3):
    """

    List files in s3 zipped object, without downloading it. Returns the number of files inside the zip file.
    See : https://dev59.com/3J7ha4cB1Zd3GeqPjG7W
    Based on : https://dev59.com/GlUK5IYBdhLWcg3wuSFD


    bucket_name: name of the bucket
    key_name:  path to zipfile inside bucket
    client_s3: an object created using boto3.client("s3")
    """

    bucket = bucket_name
    key = key_name

    response = client_s3.head_object(Bucket=bucket_name, Key=key_name)
    size = response['ContentLength']

    eocd = fetch(key_name, size - 22, 22, client_s3)

    # start offset and size of the central directory
    cd_start = parse_int(eocd[16:20])
    cd_size = parse_int(eocd[12:16])

    # fetch central directory, append EOCD, and open as zipfile!
    cd = fetch(key_name, cd_start, cd_size, client_s3)
    zip = zipfile.ZipFile(io.BytesIO(cd + eocd))

    print("there are %s files in the zipfile" % len(zip.filelist))

    for entry in zip.filelist:
        print("filename: %s (%s bytes uncompressed)" % (entry.filename, entry.file_size))
    return len(zip.filelist)

if __name__ == "__main__":
    import boto3
    import sys

    client_s3 = boto3.client("s3")
    bucket_name = sys.argv[1]
    key_name = sys.argv[2]
    list_files_in_s3_zipped_object(bucket_name, key_name, client_s3)

1
有没有办法用于大于4GB的文件? - Luciano
1
这个想法仍然可行,但是代码需要进行大量重构才能包含那种情况。 - Daniel777

3

我改进了已有的解决方案 - 现在它还可以处理大于4GiB的文件:

import boto3
import io
import struct
import zipfile

s3 = boto3.client('s3')

EOCD_RECORD_SIZE = 22
ZIP64_EOCD_RECORD_SIZE = 56
ZIP64_EOCD_LOCATOR_SIZE = 20

MAX_STANDARD_ZIP_SIZE = 4_294_967_295

def lambda_handler(event):
    bucket = event['bucket']
    key = event['key']
    zip_file = get_zip_file(bucket, key)
    print_zip_content(zip_file)

def get_zip_file(bucket, key):
    file_size = get_file_size(bucket, key)
    eocd_record = fetch(bucket, key, file_size - EOCD_RECORD_SIZE, EOCD_RECORD_SIZE)
    if file_size <= MAX_STANDARD_ZIP_SIZE:
        cd_start, cd_size = get_central_directory_metadata_from_eocd(eocd_record)
        central_directory = fetch(bucket, key, cd_start, cd_size)
        return zipfile.ZipFile(io.BytesIO(central_directory + eocd_record))
    else:
        zip64_eocd_record = fetch(bucket, key,
                                  file_size - (EOCD_RECORD_SIZE + ZIP64_EOCD_LOCATOR_SIZE + ZIP64_EOCD_RECORD_SIZE),
                                  ZIP64_EOCD_RECORD_SIZE)
        zip64_eocd_locator = fetch(bucket, key,
                                   file_size - (EOCD_RECORD_SIZE + ZIP64_EOCD_LOCATOR_SIZE),
                                   ZIP64_EOCD_LOCATOR_SIZE)
        cd_start, cd_size = get_central_directory_metadata_from_eocd64(zip64_eocd_record)
        central_directory = fetch(bucket, key, cd_start, cd_size)
        return zipfile.ZipFile(io.BytesIO(central_directory + zip64_eocd_record + zip64_eocd_locator + eocd_record))


def get_file_size(bucket, key):
    head_response = s3.head_object(Bucket=bucket, Key=key)
    return head_response['ContentLength']

def fetch(bucket, key, start, length):
    end = start + length - 1
    response = s3.get_object(Bucket=bucket, Key=key, Range="bytes=%d-%d" % (start, end))
    return response['Body'].read()

def get_central_directory_metadata_from_eocd(eocd):
    cd_size = parse_little_endian_to_int(eocd[12:16])
    cd_start = parse_little_endian_to_int(eocd[16:20])
    return cd_start, cd_size

def get_central_directory_metadata_from_eocd64(eocd64):
    cd_size = parse_little_endian_to_int(eocd64[40:48])
    cd_start = parse_little_endian_to_int(eocd64[48:56])
    return cd_start, cd_size

def parse_little_endian_to_int(little_endian_bytes):
    format_character = "i" if len(little_endian_bytes) == 4 else "q"
    return struct.unpack("<" + format_character, little_endian_bytes)[0]

def print_zip_content(zip_file):
    files = [zi.filename for zi in zip_file.filelist]
    print(f"{len(files)} files: {files}")

我们是否可以在不下载的情况下从大型ZIP文件中检索一个文件?我正在寻找这个问题的答案 https://stackoverflow.com/questions/68377520/stream-huge-zip-files-on-s3-using-lambda-and-boto3 - N Raghu
应该是可以的。我不需要实现它,但根据文档,它是可行的。基本上,你需要EOCD和CD,然后就可以找到本地头在哪里了。在本地头中,有关于相应文件大小的信息。当你有偏移量和大小时,你可以通过发送带有范围头的GET来下载单个文件。 - kwiecien
我认为parse_little_endian_to_int应该被解析为unsigned,否则我们可能会得到cd_start的负值... - Jan Rüegg
请问为什么我们要在zip64格式中添加eocd_record,例如在return zipfile.ZipFile(io.BytesIO(central_directory + zip64_eocd_record + zip64_eocd_locator + eocd_record))中...既然我们已经有了zip64_eocd_record,那么为什么在__zip64__代码块的return语句末尾还需要eocd_record呢? - Vivek Puurkayastha
@VivekPuurkayastha 请参考ZIP规范https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT “4.3.6整体ZIP文件格式” [中央目录头n] [zip64中央目录结束记录] [zip64中央目录定位器] [最后的中央目录记录]包括“标准”EOCD和zip64 EOCD。 - kwiecien
@kwiecien 感谢您指出结构问题...我是在参考维基百科关于ZIP格式的页面,他们对ZIP 64格式的描述可能需要更加明确一些... - Vivek Puurkayastha

0

您可以尝试下载存档的一部分(例如前1MB),并使用jar工具查看文件列表和属性:

jar vt < first-part-of-archive.zip

你可以使用 subprocess 模块在 Python 中获取这些数据。


我不熟悉Java,而且我们的项目中没有用Java编写的部分。我应该如何使用Python中的subprocess模块来获取数据?我点击了链接但是出现了404错误。 - alfredox
要获取zip归档的一部分,如果您有URL,可以使用此问题中描述的方法。 jar工具允许读取不完整zip文件的内容(Python模块或unzip工具将无法正常工作)。 - Stanislav Ivanov
1
这不会起作用,因为中央目录存储在文件的末尾。 - vy32

-1

尝试使用以下s3命令获取gz格式文件的计数

aws s3 cp <s3 file uri> - | gunzip -c | grep -i '<Search String>' | wc -l

例子

aws s3 cp s3://test-bucket/test/test.gz - | gunzip -c | grep -i 'test' | wc -l

-2

目前,您无法在不下载zip文件的情况下获取此类信息。上传到s3时,您可以将所需信息存储为zip文件的元数据。

正如您在问题中提到的那样,使用Python函数,我们能够在不解压缩的情况下获取文件列表。您可以使用相同的方法获取文件计数并将其添加为特定文件的元数据,然后将其上传到S3。

希望这有所帮助,谢谢。


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