红联Linux门户
Linux协助

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

发布时刻:2018-02-28 09:37:17来历:linux.cn作者:leemeans
假定现在的上下文(注:context,计算机术语,此处意为业务情形)是这样的:一个 zip 文件被上传到一个Web 服务中,然后 Python 需求解压这个 zip 文件然后剖析和处理其间的每个文件。这个特别的运用检查每个文件各自的称号和巨细,并和现已上传到 AWS S3 上的文件进行比较,假如文件(和 AWS S3 上的比较)有所不同或许文件自身更新,那么就将它上传到 AWS S3。
Python中最快解压zip文件的办法
应战在于这些 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(注:Global Interpreter Lock,一种大局锁,CPython 中的一个概念)阻止了我。最天然的主意是测验运用多线程在多个 CPU 上分配作业。可是这样做有缺点,那便是你不能传递一个非可 pickle 序列化的目标(注:意为只要可 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 文件里是一团糟。它包括了很多可怕的修正和丑恶的代码,可是这仅仅一个开端。
 
LINUX经过网站解压zip掩盖网站进行晋级失利,忘掉apache授权:http://www.138comgov138.com/linux/28467.html
Linux下解压ZIP压缩包乱码问题:http://www.138comgov138.com/linux/15823.html
在Ubuntu 14.10下解压zip文件呈现乱码的处理:http://www.138comgov138.com/linux/9952.html
linux解压.zip文件乱码处理的一个脚本:http://www.138comgov138.com/linux/27253.html
Linux操作体系下解压.zip文件的办法:http://www.138comgov138.com/linux/210.html