您现在的位置是:亿华云 > 数据库
Python中最快解压zip文件的方法
亿华云2025-10-09 03:28:20【数据库】6人已围观
简介假设现在的上下文LCTT 译注:context,计算机术语,此处意为业务情景)是这样的:一个 zip 文件被上传到一个Web 服务中,然后 Python 需要解压这个 zip 文件然后分析和处理其中的
假设现在的中最上下文(LCTT 译注:context,计算机术语,快解此处意为业务情景)是文件这样的:一个 zip 文件被上传到一个Web 服务中,然后 Python 需要解压这个 zip 文件然后分析和处理其中的中最每个文件。这个特殊的快解应用查看每个文件各自的名称和大小,并和已经上传到 AWS S3 上的文件文件进行比较,如果文件(和 AWS S3 上的中最相比)有所不同或者文件本身更新,那么就将它上传到 AWS S3。快解
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 的实例,展开其中的每个文件名,b2b供应网然后为每一个文件开始一个线程。每个线程都给它一个函数来做“实质工作”(在这个基准测试中,就是遍历每个文件然后获取它的名称)。实际业务中的函数进行的工作是复杂的 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.pywget https://www.peterbe.com/unzip-in-parallel/symbols-2017-11-27T14_15_30.zip这里的 .zip 文件有 34MB。和在服务器上的相比已经小了很多。
hack.unzip-in-parallel.py 文件里是一团糟。它包含了大量可怕的修正和丑陋的代码,但是这只是一个开始。
很赞哦!(885)
相关文章
- 互联网中的地址是数字的IP地址,域名解析的作用主要就是为了便于记忆。
- 关于代码质量退化的思考
- 支付宝技术风险负责人陈亮:把事情做到极致,技术的差异性才会体现出来
- Microsoft Visual C++ 14.0 is required
- 为了避免将来给我们的个人站长带来的麻烦,在选择域名后缀时,我们的站长最好省略不稳定的后缀域名,比如n,因为我们不知道策略什么时候会改变,更不用说我们将来是否还能控制这个域名了。因此,如果站长不是企业,或者有选择的话,如果不能选择域名的cn类,最好不要选择它。
- Spring Cloud 上手实战-架构解析及实作
- 面试突击:一个表中可以有多个自增列吗?
- 走近科学,探究阿里闲鱼团队通过数据提升Flutter体验的真相
- 前面这两个步骤都是在本机完成的。到这里还没有涉及真正的域名解析服务器,如果在本机中仍然无法完成域名的解析,就会真正请求域名服务器来解析这个域名了。
- Go原生插件使用问题全解析
站长推荐
互联网其实拼的也是人脉,域名投资也是一个时效性很强的东西,一个不起眼的消息就会引起整个域名投资市场的动荡,因此拓宽自己的人脉圈,完善自己的信息获取渠道,让自己能够掌握更为多样化的信息,这样才更有助于自己的域名投资。
Java 之父 Jame:差点把 Java 命名成了 Silk(丝绸)
Python操作Redis缓存数据库
JS 中的事件委托是什么
其次,一般域名注册有一个获取密码的按钮,域名注册商点击后会向您发送密码。在得到域名注册商发送的密码后,将其传输到域名服务提供商网站,然后输入密码,此时域名呈现申请状态。提交申请后,原注册人通常会向您发送一封电子邮件,询问您是否同意转让。此时,您只需点击同意转移按钮,域名注册商就可以成功转移。
基于智能数据库的自助式机器学习
轻量级 Web 框架 Gin 结构分析
让我们一起聊聊什么是数组?