如何创建一个文件夹的压缩包?

865

我该如何在Python中创建一个包含目录结构的zip压缩文件?


78
请使用在被接受答案下面的那个解决方案,使用shutil中的make_archive(如果您想要递归压缩单个目录)。不要使用被接受的答案中提出的解决方案。 - malana
是的,同意@malana - Martha Yi似乎未注册 - 那么是否有通过社区流程更改接受的答案的方法? - Romeo Kienzler
一个与shutil.make_archive有关的注意事项 - 它似乎不遵循符号链接。 - LRE
2
被接受的答案是唯一一个在从目录创建zip文件时实际上是线程安全的,因为每个文件都是单独打开的,锁定对它的读取访问直到文件关闭。 - Beefcake
30个回答

2119

最简单的方法是使用shutil.make_archive。它支持zip和tar格式。

import shutil
shutil.make_archive(output_filename, 'zip', dir_name)
如果你需要做一些比整个目录压缩(如跳过某些文件)更复杂的事情,那么你需要像其他人建议的那样深入了解zipfile模块。

282
shutil 是 Python 标准库的一部分。这应该是最佳答案。 - Alex
12
这是这里最简洁的答案,还有一个优点,可以直接添加所有子目录和文件到归档中,而不是将所有内容包含在顶层文件夹中(这会导致解压缩时文件结构中多余的级别)。 - aitch-hat
3
@cmcginty,请你更具体地说明哪个方面不是线程安全的?在一个线程调用时同时运行多个线程会导致解释器崩溃吗? - std''OrgnlDave
25
请注意,在Python 3.4之前,shutil.make_archive不支持ZIP64,创建大于2GB的ZIP文件将失败。 - dvs
9
不行。如果您查看错误报告(bugs.python.org/issue30511),您将看到“shutil.make_archive”使用“os.chdir()”。根据我所阅读的关于“os.chdir()”的内容,它具有全局作用。 - Sam Malayek
显示剩余16条评论

711

正如其他人指出的那样,你应该使用 zipfile。文档告诉你哪些函数可用,但并没有真正解释如何使用它们来压缩整个目录。我认为最简单的方法是通过一些示例代码来解释:

import os
import zipfile
    
def zipdir(path, ziph):
    # ziph is zipfile handle
    for root, dirs, files in os.walk(path):
        for file in files:
            ziph.write(os.path.join(root, file), 
                       os.path.relpath(os.path.join(root, file), 
                                       os.path.join(path, '..')))

with zipfile.ZipFile('Python.zip', 'w', zipfile.ZIP_DEFLATED) as zipf:
    zipdir('tmp/', zipf)

150
我会在write调用中添加第二个参数,传递os.path.relpath(os.path.join(root, file), os.path.join(path, '..'))。这样可以让你从任何工作目录压缩一个目录,而不必在归档文件中获取完整的绝对路径。 - Reimund
13
当我尝试将文件夹压缩并将结果输出到同一文件夹时,会发生一个有趣的递归现象。 :-) - Sibbs Gambling
39
shutil 使得只需一行代码就能非常容易地完成。请查看下面的答案。 - droidlabour
7
您可能会更感兴趣使用ziph.write(os.path.join(path,file), arcname=file),这样压缩包内的文件名就不是相对于硬盘的路径了。 - Christophe Blin
2
下一个答案与压缩相关的内容无关,它只是将结果存储在ZIP归档文件中。如果您正在寻找实际的压缩功能,则正确的答案是这个,而不是使用shutil的下一个答案。 - José L. Patiño
显示剩余17条评论

88

要将 mydirectory 目录下的所有文件和子目录添加到一个新的 zip 文件中:

import os
import zipfile

zf = zipfile.ZipFile("myzipfile.zip", "w")
for dirname, subdirs, files in os.walk("mydirectory"):
    zf.write(dirname)
    for filename in files:
        zf.write(os.path.join(dirname, filename))
zf.close()

对我来说,这段代码抛出以下错误 TypeError: invalid file: <zipfile.ZipFile [closed]>。 - Nishad Up
23
你不能使用with代替手动在结尾调用close()吗? - ArtOfWarfare
with zipfile.ZipFile("myzipfile.zip", "w") as zf: pass - Subramanya Rao
3
这将重新构建在生成的压缩文件中到“mydirectory”的完整路径。即,仅当“mydirectory”在您的文件系统根目录下时才按预期工作。 - Maile Cupo
使用write函数的arcname参数可以解决整个目录分支被压缩而不仅仅是内容的问题。 - Prince

63

如何在Python中创建一个目录结构的zip归档文件?

在Python脚本中

在Python 2.7+版本中,shutil 模块提供了 make_archive 函数。

from shutil import make_archive
make_archive(
  'zipfile_name', 
  'zip',           # the archive format - or tar, bztar, gztar 
  root_dir=None,   # root for archive - current working dir if None
  base_dir=None)   # start archiving from here - cwd if None too

在这里,压缩文件将被命名为zipfile_name.zip。如果base_dirroot_dir更深,则会排除不在base_dir中的文件,但仍会归档父目录中到root_dir的文件。

我在Cygwin上使用2.7进行测试时遇到了问题 - 它希望有一个root_dir参数,用于当前工作目录:

make_archive('zipfile_name', 'zip', root_dir='.')

从命令行使用Python

你也可以使用Python的zipfile模块在命令行中完成这个操作:

$ python -m zipfile -c zipname sourcedir

其中zipname是您想要的目标文件名(如果需要,请添加.zip,它不会自动执行),sourcedir是目录的路径。

压缩Python文件(或者只是不想要父级目录):

如果您正在尝试压缩一个带有__init__.py__main__.py的Python包,并且您不想要父级目录,则应该

$ python -m zipfile -c zipname sourcedir/*

而且

$ python zipname

请注意,如果从压缩存档文件运行Python子包作为入口点是不可行的,您只能运行包。

压缩Python应用程序:

如果您使用的是python3.5+,并且特别想要将Python包压缩起来,请使用zipapp

$ python -m zipapp myapp
$ python myapp.pyz

这是最好、最简单的解决方案。 - Cary

45

这个函数将递归地压缩目录树中的文件,并在归档中记录正确的相对文件名。归档条目与通过 zip -r output.zip source_dir 生成的条目相同。

import os
import zipfile
def make_zipfile(output_filename, source_dir):
    relroot = os.path.abspath(os.path.join(source_dir, os.pardir))
    with zipfile.ZipFile(output_filename, "w", zipfile.ZIP_DEFLATED) as zip:
        for root, dirs, files in os.walk(source_dir):
            # add directory (needed for empty dirs)
            zip.write(root, os.path.relpath(root, relroot))
            for file in files:
                filename = os.path.join(root, file)
                if os.path.isfile(filename): # regular files only
                    arcname = os.path.join(os.path.relpath(root, relroot), file)
                    zip.write(filename, arcname)

太好了,我在想能否在with语句中使用zipfile。 感谢指出它是可以的。 - Mitms

30

使用Python 3.9,pathlibzipfile模块,您可以从系统中的任何位置创建 zip 文件。

def zip_dir(dir: Union[Path, str], filename: Union[Path, str]):
    """Zip the provided directory without navigating to that directory using `pathlib` module"""

    # Convert to Path object
    dir = Path(dir)

    with zipfile.ZipFile(filename, "w", zipfile.ZIP_DEFLATED) as zip_file:
        for entry in dir.rglob("*"):
            zip_file.write(entry, entry.relative_to(dir))

它整洁、有类型,并且代码更少。


28

使用现代的Python(3.6+),使用pathlib模块来简洁地面向对象处理路径,以及使用pathlib.Path.rglob()进行递归式全局匹配。据我所知,这相当于George V. Reilly的答案:带有压缩的zip文件,顶层元素是目录,保留空目录,使用相对路径。

from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile

from os import PathLike
from typing import Union


def zip_dir(zip_name: str, source_dir: Union[str, PathLike]):
    src_path = Path(source_dir).expanduser().resolve(strict=True)
    with ZipFile(zip_name, 'w', ZIP_DEFLATED) as zf:
        for file in src_path.rglob('*'):
            zf.write(file, file.relative_to(src_path.parent))
注意:如可选的类型提示所示,zip_name 不能是一个路径对象(将在3.6.2+中修复)。

6
太棒了!简洁明了!现代化! - ingyhere

27

使用Python标准库中的shutil。

使用shutil非常简单(请参见下面的代码):

  • 第1个参数:生成的zip/tar文件名,
  • 第2个参数:zip/tar,
  • 第3个参数:目录名

代码:

import shutil
shutil.make_archive('/home/user/Desktop/Filename','zip','/home/username/Desktop/Directory')

1
在所有的shutil.make_archive示例中,它们都会创建空的根文件夹,一直延伸到你实际想要归档的文件夹。我不希望我的归档文件解压后显示为"/home/user/Desktop",这样每个人都可以看到感兴趣的文件夹最初的位置。我该如何只压缩"/Directory"并省略所有父文件夹的痕迹? - kravb
1
这已经说了3次了。而且这绝对不是最好的答案。 - José L. Patiño

13

如果要为生成的zip文件添加压缩功能,请查看这个链接

您需要更改:

zip = zipfile.ZipFile('Python.zip', 'w')
to
zip = zipfile.ZipFile('Python.zip', 'w', zipfile.ZIP_DEFLATED)

7
我已对Mark Byers提供的代码进行了一些更改。下面的函数还会添加空目录(如果有的话)到压缩包中。示例将更清晰地说明添加到zip中的路径是什么。
#!/usr/bin/env python
import os
import zipfile

def addDirToZip(zipHandle, path, basePath=""):
    """
    Adding directory given by \a path to opened zip file \a zipHandle

    @param basePath path that will be removed from \a path when adding to archive

    Examples:
        # add whole "dir" to "test.zip" (when you open "test.zip" you will see only "dir")
        zipHandle = zipfile.ZipFile('test.zip', 'w')
        addDirToZip(zipHandle, 'dir')
        zipHandle.close()

        # add contents of "dir" to "test.zip" (when you open "test.zip" you will see only it's contents)
        zipHandle = zipfile.ZipFile('test.zip', 'w')
        addDirToZip(zipHandle, 'dir', 'dir')
        zipHandle.close()

        # add contents of "dir/subdir" to "test.zip" (when you open "test.zip" you will see only contents of "subdir")
        zipHandle = zipfile.ZipFile('test.zip', 'w')
        addDirToZip(zipHandle, 'dir/subdir', 'dir/subdir')
        zipHandle.close()

        # add whole "dir/subdir" to "test.zip" (when you open "test.zip" you will see only "subdir")
        zipHandle = zipfile.ZipFile('test.zip', 'w')
        addDirToZip(zipHandle, 'dir/subdir', 'dir')
        zipHandle.close()

        # add whole "dir/subdir" with full path to "test.zip" (when you open "test.zip" you will see only "dir" and inside it only "subdir")
        zipHandle = zipfile.ZipFile('test.zip', 'w')
        addDirToZip(zipHandle, 'dir/subdir')
        zipHandle.close()

        # add whole "dir" and "otherDir" (with full path) to "test.zip" (when you open "test.zip" you will see only "dir" and "otherDir")
        zipHandle = zipfile.ZipFile('test.zip', 'w')
        addDirToZip(zipHandle, 'dir')
        addDirToZip(zipHandle, 'otherDir')
        zipHandle.close()
    """
    basePath = basePath.rstrip("\\/") + ""
    basePath = basePath.rstrip("\\/")
    for root, dirs, files in os.walk(path):
        # add dir itself (needed for empty dirs
        zipHandle.write(os.path.join(root, "."))
        # add files
        for file in files:
            filePath = os.path.join(root, file)
            inZipPath = filePath.replace(basePath, "", 1).lstrip("\\/")
            #print filePath + " , " + inZipPath
            zipHandle.write(filePath, inZipPath)

上面是一个简单的函数,适用于简单情况。你可以在我的Gist中找到更优雅的类: https://gist.github.com/Eccenux/17526123107ca0ac28e6

1
路径处理可以通过使用os.path大大简化。请查看我的回答。 - George V. Reilly
错误:zipHandle.write(os.path.join(root, ".")) 没有考虑basePath。 - Petter
是的,你可能是对的。我稍后对此进行了一些增强 ;-) https://gist.github.com/Eccenux/17526123107ca0ac28e6 - Nux

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