Python 中最快解压 zip 文件的方法

简介:

假设现在的上下文(注:context,计算机术语,此处意为业务情景)是这样的:一个 zip 文件被上传到一个Web 服务中,然后 Python 需要解压这个 zip 文件然后分析和处理其中的每个文件。这个特殊的应用查看每个文件各自的名称和大小,并和已经上传到 AWS S3 上的文件进行比较,如果文件(和 AWS S3 上的相比)有所不同或者文件本身更新,那么就将它上传到 AWS S3。

Uploads today

Uploads today

挑战在于这些 zip 文件太大了。它们的平均大小是 560MB 但是其中一些大于 1GB。这些文件中大多数是文本文件,但是其中同样也有一些巨大的二进制文件。不同寻常的是,每个 zip 文件包含 100 个文件但是其中 1-3 个文件却占据了多达 95% 的 zip 文件大小。

最开始我尝试在内存中解压文件,并且每次只处理一个文件。在各种内存爆炸和 EC2 耗尽内存的情况下,这个方法壮烈失败了。我觉得这个原因是这样的。最开始你有 1GB 文件在内存中,然后你现在解压每个文件,在内存中大约就要占用 2-3GB。所以,在很多次测试之后,解决方案是将这些 zip 文件复制到磁盘上(在临时目录 /tmp 中),然后遍历这些文件。这次情况好多了但是我仍然注意到了整个解压过程花费了巨量的时间。是否可能有方法优化呢?

原始函数

首先是下面这些模拟对 zip 文件中文件实际操作的普通函数:

def _count_file(fn):
 with open(fn, 'rb') as f:
   return _count_file_object(f)
def _count_file_object(f):
 # Note that this iterates on 'f'.
 # You *could* do 'return len(f.read())'
 # which would be faster but potentially memory
 # inefficient and unrealistic in terms of this
 # benchmark experiment.
 total = 0
 for line in f:
   total += len(line)
 return total

这里是可能最简单的另一个函数:

def f1(fn, dest):
 with open(fn, 'rb') as f:
   zf = zipfile.ZipFile(f)
   zf.extractall(dest)
 total = 0
 for root, dirs, files in os.walk(dest):
   for file_ in files:
     fn = os.path.join(root, file_)
     total += _count_file(fn)
 return total

如果我更仔细地分析一下,我将会发现这个函数花费时间 40% 运行 extractall,60% 的时间在遍历各个文件并读取其长度。

第一步尝试

我的第一步尝试是使用线程。先创建一个 zipfile.ZipFile 的实例,展开其中的每个文件名,然后为每一个文件开始一个线程。每个线程都给它一个函数来做“实质工作”(在这个基准测试中,就是遍历每个文件然后获取它的名称)。实际业务中的函数进行的工作是复杂的 S3、Redis 和 PostgreSQL 操作,但是在我的基准测试中我只需要制作一个可以找出文件长度的函数就好了。线程池函数:

def f2(fn, dest):
    def unzip_member(zf, member, dest):
        zf.extract(member, dest)
        fn = os.path.join(dest, member.filename)
        return _count_file(fn)
    with open(fn, 'rb') as f:
        zf = zipfile.ZipFile(f)
        futures = []
        with concurrent.futures.ThreadPoolExecutor() as executor:
            for member in zf.infolist():
                futures.append(
                    executor.submit(
                        unzip_member,
                        zf,
                        member,
                        dest,
                    )
                )
            total = 0
            for future in concurrent.futures.as_completed(futures):
                total += future.result()
    return total

结果:加速 ~10%

第二步尝试

所以可能是 GIL(LCTT 译注:Global Interpreter Lock,一种全局锁,CPython 中的一个概念)阻碍了我。最自然的想法是尝试使用多线程在多个 CPU 上分配工作。但是这样做有缺点,那就是你不能传递一个非可 pickle 序列化的对象(LCTT 译注:意为只有可 pickle 序列化的对象可以被传递),所以你只能发送文件名到之后的函数中:

def unzip_member_f3(zip_filepath, filename, dest):
    with open(zip_filepath, 'rb') as f:
        zf = zipfile.ZipFile(f)
        zf.extract(filename, dest)
    fn = os.path.join(dest, filename)
    return _count_file(fn)
def f3(fn, dest):
    with open(fn, 'rb') as f:
        zf = zipfile.ZipFile(f)
        futures = []
        with concurrent.futures.ProcessPoolExecutor() as executor:
            for member in zf.infolist():
                futures.append(
                    executor.submit(
                        unzip_member_f3,
                        fn,
                        member.filename,
                        dest,
                    )
                )
            total = 0
            for future in concurrent.futures.as_completed(futures):
                total += future.result()
    return total

结果: 加速 ~300%

这是作弊

使用处理器池的问题是这样需要存储在磁盘上的原始 .zip 文件。所以为了在我的 web 服务器上使用这个解决方案,我首先得要将内存中的 zip 文件保存到磁盘,然后调用这个函数。这样做的代价我不是很清楚但是应该不低。

好吧,再翻翻看又没有损失。可能,解压过程加速到足以弥补这样做的损失了吧。

但是一定记住!这个优化取决于使用所有可用的 CPU。如果一些其它的 CPU 需要执行在 gunicorn 中的其它事务呢?这时,这些其它进程必须等待,直到有 CPU 可用。由于在这个服务器上有其他的事务正在进行,我不是很确定我想要在进程中接管所有其他 CPU。

结论

一步一步地做这个任务的这个过程感觉挺好的。你被限制在一个 CPU 上但是表现仍然特别好。同样地,一定要看看在f1 和 f2 两段代码之间的不同之处!利用 concurrent.futures 池类你可以获取到允许使用的 CPU 的个数,但是这样做同样给人感觉不是很好。如果你在虚拟环境中获取的个数是错的呢?或者可用的个数太低以致无法从负载分配获取好处并且现在你仅仅是为了移动负载而支付营运开支呢?

我将会继续使用 zipfile.ZipFile(file_buffer).extractall(temp_dir)。这个工作这样做已经足够好了。

想试试手吗?

我使用一个 c5.4xlarge EC2 服务器来进行我的基准测试。文件可以从此处下载:

wget https://www.peterbe.com/unzip-in-parallel/hack.unzip-in-parallel.py
wget https://www.peterbe.com/unzip-in-parallel/symbols-2017-11-27T14_15_30.zip

这里的 .zip 文件有 34MB。和在服务器上的相比已经小了很多。

hack.unzip-in-parallel.py 文件里是一团糟。它包含了大量可怕的修正和丑陋的代码,但是这只是一个开始。

原    文:Fastest way to unzip a zip file in Python
译    文:Linux中国
作    者:Leemeans 译
目录
相关文章
|
1月前
|
测试技术 API Python
【10月更文挑战第1天】python知识点100篇系列(13)-几种方法让你的电脑一直在工作
【10月更文挑战第1天】 本文介绍了如何通过Python自动操作鼠标或键盘使电脑保持活跃状态,避免自动息屏。提供了三种方法:1) 使用PyAutoGUI,通过安装pip工具并执行`pip install pyautogui`安装,利用`moveRel()`方法定时移动鼠标;2) 使用Pymouse,通过`pip install pyuserinput`安装,采用`move()`方法移动鼠标绝对位置;3) 使用PyKeyboard,同样需安装pyuserinput,模拟键盘操作。文中推荐使用PyAutoGUI,因其功能丰富且文档详尽。
WK
|
19天前
|
Python
Python中format_map()方法
在Python中,`format_map()`方法用于使用字典格式化字符串。它接受一个字典作为参数,用字典中的键值对替换字符串中的占位符。此方法适用于从字典动态获取值的场景,尤其在处理大量替换值时更为清晰和方便。
WK
67 36
|
29天前
|
安全 Linux 数据安全/隐私保护
python知识点100篇系列(15)-加密python源代码为pyd文件
【10月更文挑战第5天】为了保护Python源码不被查看,可将其编译成二进制文件(Windows下为.pyd,Linux下为.so)。以Python3.8为例,通过Cython工具,先写好Python代码并加入`# cython: language_level=3`指令,安装easycython库后,使用`easycython *.py`命令编译源文件,最终生成.pyd文件供直接导入使用。
python知识点100篇系列(15)-加密python源代码为pyd文件
|
30天前
|
机器学习/深度学习 数据采集 数据挖掘
11种经典时间序列预测方法:理论、Python实现与应用
本文将总结11种经典的时间序列预测方法,并提供它们在Python中的实现示例。
63 2
11种经典时间序列预测方法:理论、Python实现与应用
|
11天前
|
开发者 Python
Python中__init__.py文件的作用
`__init__.py`文件在Python包管理中扮演着重要角色,通过标识目录为包、初始化包、控制导入行为、支持递归包结构以及定义包的命名空间,`__init__.py`文件为组织和管理Python代码提供了强大支持。理解并正确使用 `__init__.py`文件,可以帮助开发者更好地组织代码,提高代码的可维护性和可读性。
15 2
|
26天前
|
开发者 Python
Python中的魔法方法与运算符重载
在Python的奇妙世界里,魔法方法(Magic Methods)和运算符重载(Operator Overloading)是两个强大的特性,它们允许开发者以更自然、更直观的方式操作对象。本文将深入探讨这些概念,并通过实例展示如何利用它们来增强代码的可读性和表达力。
|
27天前
|
Java Python
> python知识点100篇系列(19)-使用python下载文件的几种方式
【10月更文挑战第7天】本文介绍了使用Python下载文件的五种方法,包括使用requests、wget、线程池、urllib3和asyncio模块。每种方法适用于不同的场景,如单文件下载、多文件并发下载等,提供了丰富的选择。
|
28天前
|
数据安全/隐私保护 流计算 开发者
python知识点100篇系列(18)-解析m3u8文件的下载视频
【10月更文挑战第6天】m3u8是苹果公司推出的一种视频播放标准,采用UTF-8编码,主要用于记录视频的网络地址。HLS(Http Live Streaming)是苹果公司提出的一种基于HTTP的流媒体传输协议,通过m3u8索引文件按序访问ts文件,实现音视频播放。本文介绍了如何通过浏览器找到m3u8文件,解析m3u8文件获取ts文件地址,下载ts文件并解密(如有必要),最后使用ffmpeg合并ts文件为mp4文件。
|
3月前
|
SQL JSON C语言
Python中字符串的三种定义方法
Python中字符串的三种定义方法
|
5月前
|
Python
python之字符串定义、切片、连接、重复、遍历、字符串方法
python之字符串定义、切片、连接、重复、遍历、字符串方法
python之字符串定义、切片、连接、重复、遍历、字符串方法