shutil 模块提供了一系列对文件和文件集合的高阶操作,特别是提供了一些支持文件拷贝和删除的函数,主要功能有对文件、文件夹的 复制、删除、移动操作,以及压缩包的处理模块
本文章内容较多,如果需要查找某个特定的方法或属性,建议使用浏览器的 查找 ctrl + f 功能
在看下列内容并实际操作前,别忘记导入shutil 标准库
import shutil
1)、shutil.copyfileobj(fsrc, fdst[, length]):
将文件类对象 fsrc 的内容拷贝到文件类对象 fdst 中,整数值 length 如果给出则设为缓冲区大小,特别的,length为负值事表示拷贝数据时不对源数据进行分块循环处理;默认情况下会分块读取数据以避免不受控制的内存消耗。
演示代码:👇
# 复制文件A中的全部内容到文件B
with open(file_A, "r", encoding="utf-8") as f_r, open(file_B, "w", encoding="utf-8") as f_w:
shutil.copyfileobj(f_r, f_w)
如果 fsrc 对象的当前指针位置不为0,则会当前文件指针位置到文件末尾的内容会被拷贝
with open(file_A, "r", encoding="utf-8") as f_r, open(file_B, "w", encoding="utf-8") as f_w:
# 将文件A中光标指向3,即第二个字符为开头
# 注意对于UTF-8编码,每三间隔为一个字符,不为三的倍数将会抛出异常
f_r.seek(3)
shutil.copyfileobj(f_r, f_w)
2)、shutil.copyfile(src, dst, *, follow_symlinks=True):「常用」
将名为 src 的文件的内容(不包括元数据)拷贝到名为 dst 的文件并以尽可能高效的方式返回 dst(即目标文件路径)
参数如下:
如果 src 和 dst 指定了同一个文件,则将引发 SameFileError
目标位置必须是可写的,否则将引发 OSError 异常
演示代码:👇
不同于shutil.copyfileobj,此方法无需目标文件存在,会自动创建生成一个新的文件,如果目标文件已经存在,它将被替换
shutil.copyfile(file_A, file_B)
3)、shutil.copymode(src, dst, *, follow_symlinks=True):
从 src 拷贝权限位到 dst,文件的内容、所有者和分组将不受影响
参数如下:
演示代码:👇
shutil.copymode(file_A, file_B)
4)、shutil.copystat(src, dst, *, follow_symlinks=True):
从 src 拷贝状态信息,文件的内容、所有者和分组将不受影响,如:权限位(mode)、最近访问时间(atime)、最近修改时间(mtime)以及旗标(flags)到 dst。 在 Linux上,copystat() 还会在可能的情况下拷贝“扩展属性”。
参数如下:
演示代码:👇
shutil.copystat(file_A, file_B)
5)、shutil.copy(src, dst, *, follow_symlinks=True):「常用」
将文件 src 内容和权限 拷贝到文件或目录 dst,并返回目标文件所对应的路径。
参数如下:
源码:
源码其实很简单,其实就是同时调用了shutil.copyfile 和copymode,可以把shutil.copy 理解成这两个shutil.copyfile 和copymode的结合
def copy(src, dst, *, follow_symlinks=True):
"""Copy data and mode bits ("cp src dst"). Return the file's destination.
The destination may be a directory.
If follow_symlinks is false, symlinks won't be followed. This
resembles GNU's "cp -P src dst".
If source and destination are the same file, a SameFileError will be
raised.
"""
if os.path.isdir(dst):
dst = os.path.join(dst, os.path.basename(src))
copyfile(src, dst, follow_symlinks=follow_symlinks)
copymode(src, dst, follow_symlinks=follow_symlinks)
return dst
演示代码:👇
shutil.copy(file_A, file_B)
6)、shutil.copy2(src, dst, *, follow_symlinks=True):「常用」
类似于 shutil.copy,区别在于 shutil.copy2 在拷贝文件内容时还会拷贝文件状态的信息
参数如下:
源码:
和shutil.copy是一个套路,就是同时调用了shutil.copyfile 和shutil.copystat
def copy2(src, dst, *, follow_symlinks=True):
"""Copy data and metadata. Return the file's destination.
Metadata is copied with copystat(). Please see the copystat function
for more information.
The destination may be a directory.
If follow_symlinks is false, symlinks won't be followed. This
resembles GNU's "cp -P src dst".
"""
if os.path.isdir(dst):
dst = os.path.join(dst, os.path.basename(src))
copyfile(src, dst, follow_symlinks=follow_symlinks)
copystat(src, dst, follow_symlinks=follow_symlinks)
return dst
演示代码:👇
shutil.copy2(file_A, file_B)
1)、shutil.ignore_patterns(*patterns):
这个函数为shutil.copytree的辅助函数,作用于 shutil.copytree 的 ignore 可调用对象参数,以 glob 风格忽略 shutil.copytree 中的文件和目录,具体使用请移步到 2)、shutil.copytree
glob介绍:
glob 类似于正则表达式,但与正则表达式又不完全相同,可以简单的将其理解成一个简化的正则,glob非常强大的用途在于路径匹配,另外每个平台和开发语言所支持glob路径匹配可能不完全一样
演示代码:👇
# 比较常用的是忽略掉以.pyc结尾以及tmp开头的文件
shutil.ignore_patterns('*.pyc', 'tmp*')
# 忽略掉以.log和.tmp结尾的文件
shutil.ignore_patterns('*.log', '*.tmp')
2)、shutil.copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2, ignore_dangling_symlinks=False, dirs_exist_ok=False):「常用」
将src整个目录树拷贝到dst目录,会自动创建dst目录文件夹并返回目标目录的路径,目录的权限和时间会通过 shutil.copystat 来拷贝,单个文件则会使用 shutil.copy2 来拷贝
参数如下:
目标目录不能存在,否则会抛出FileExistsError异常
演示代码:👇
比较常用的调用方法
# 将文件夹A拷贝到文件夹B
shutil.copytree(folder_A, folder_B)
# 将文件夹A拷贝到文件夹B, 在此过程中忽略掉以.log和.tmp结尾的文件
shutil.copytree(folder_A, folder_B, ignore=shutil.ignore_patterns('*.log', '*.tmp'))
再结合copy_function参数,将原来的 shutil.copy2(拷贝内容和文件状态的信息) 更改为 shutil.copy(拷贝内容和权限)
# 将文件夹A拷贝到文件夹B, 拷贝文件的内容和权限
shutil.copytree(folder_A, folder_B, copy_function=shutil.copy)
在上诉的参数说明中提到过,copy_function 参数以允许提供定制的拷贝函数,没错我们可以使用自己自定义的函数对每个文件进行拷贝操作,如下方代码所示,结合了 shutil.copyfile 、shutil.copystat、shutil.copymode 对文件进行拷贝
def copy_mode(*args, **kwargs):
shutil.copyfile(*args, **kwargs)
shutil.copystat(*args, **kwargs)
shutil.copymode(*args, **kwargs)
# 将文件夹A拷贝到文件夹B, 并以自己自定义的拷贝文件方式拷贝
shutil.copytree(folder_A, folder_B, copy_function=copy_mode)
注意在使用copy_function参数时,应传入函数本身而不是加上括号调用函数
3)、shutil.rmtree(path, ignore_errors=False, οnerrοr=None):「常用」
删除一个完整的目录树,此方法会经常用到
需要注意的是,文件夹分两种,一种是空文件夹,另外一种则是文件夹中有内容的非空文件夹,对于空文件夹来说,可以使用os标准模块下的os.rmdir将其删除,但如果对非空文件夹删除会出现 OSError 错误,对于删除非空文件夹,一种方法是使用os模块下的os.walk将文件与文件夹分别遍历删除,另外一种方法就是使用现所讲的shutil.rmtree。
详情请移步到文章 《Python 以优雅的姿势 操作文件》 下的删除文件夹
参数如下:
演示代码:👇
可以配合os标准库模块下的os.path.isdir判断是否为文件夹再进行删除
# 判断路径是否为文件夹
if os.path.isdir(path):
# 删除一个完整的目录树
shutil.rmtree(path)
利用 ignore_errors 以及 onerror 参数,将遇到删除失败时的异常变成自定义的报错
def myerr(*args, **kwargs):
raise Exception("删除文件夹时发生错误")
shutil.rmtree("不存在的路径", ignore_errors=False, onerror=myerr)
4)、shutil.move(src, dst, copy_function=copy2):「常用」
递归地将一个文件或目录 (src) 移至另一位置 (dst) 并返回目标位置路径
参数如下:
源码:
来看看源码,其实这个移动原理很简单,其实就是先将文件或文件夹拷贝到目标目录,再将原目录进行删除操作,注意看倒数第五、第六行
def move(src, dst, copy_function=copy2):
"""Recursively move a file or directory to another location. This is
similar to the Unix "mv" command. Return the file or directory's
destination.
If the destination is a directory or a symlink to a directory, the source
is moved inside the directory. The destination path must not already
exist.
If the destination already exists but is not a directory, it may be
overwritten depending on os.rename() semantics.
If the destination is on our current filesystem, then rename() is used.
Otherwise, src is copied to the destination and then removed. Symlinks are
recreated under the new name if os.rename() fails because of cross
filesystem renames.
The optional `copy_function` argument is a callable that will be used
to copy the source or it will be delegated to `copytree`.
By default, copy2() is used, but any function that supports the same
signature (like copy()) can be used.
A lot more could be done here... A look at a mv.c shows a lot of
the issues this implementation glosses over.
"""
sys.audit("shutil.move", src, dst)
real_dst = dst
if os.path.isdir(dst):
if _samefile(src, dst):
# We might be on a case insensitive filesystem,
# perform the rename anyway.
os.rename(src, dst)
return
real_dst = os.path.join(dst, _basename(src))
if os.path.exists(real_dst):
raise Error("Destination path '%s' already exists" % real_dst)
try:
os.rename(src, real_dst)
except OSError:
if os.path.islink(src):
linkto = os.readlink(src)
os.symlink(linkto, real_dst)
os.unlink(src)
elif os.path.isdir(src):
if _destinsrc(src, dst):
raise Error("Cannot move a directory '%s' into itself"
" '%s'." % (src, dst))
if (_is_immutable(src)
or (not os.access(src, os.W_OK) and os.listdir(src)
and sys.platform == 'darwin')):
raise PermissionError("Cannot move the non-empty directory "
"'%s': Lacking write permission to '%s'."
% (src, src))
copytree(src, real_dst, copy_function=copy_function,
symlinks=True)
rmtree(src)
else:
copy_function(src, real_dst)
os.unlink(src)
return real_dst
演示代码:👇
# 将文件夹A移动到文件夹B目标路径
shutil.move(folder_A, folder_B)
5)、shutil.disk_usage(path)
path 可以是一个文件或是一个目录,返回一个具名元组表示给定路径的磁盘使用统计数据,其中包含 total, used 和 free 属性,分别表示总计、已使用和未使用空间的字节数
演示代码:👇
print(shutil.disk_usage(folder_A))
usage(total=1000186310656, used=874484068352, free=125702242304)
shutil标准库模块提供了用于 创建和读取压缩 和 归档文件 的高层级工具,但其实这些功能依赖于 zipfile 和 tarfile 模块,换句话来说就等同于在使用这两个标准库模块
1)、shutil.make_archive(base_name, format[, root_dir[, base_dir[, verbose[, dry_run[, owner[, group[, logger]]]]]]])
创建一个归档文件「压缩包」,例如 zip 或 tar,并返回其归档(压缩包)的文件路径
参数如下:
演示代码:👇
基本用法
zip_path = shutil.make_archive(
# 压缩包文件名, 无后缀
"test1",
# 使用zip格式压缩包
"zip",
# 被压缩的路径
root_dir="./test/1.txt",
)
配合 base_dir 参数使用
zip_path = shutil.make_archive(
# 压缩包文件名, 无后缀
"test2",
# 使用zip格式压缩包
"zip",
# 被压缩的路径
root_dir="./test",
# 在root_dir的基础上,其路径下的文件或文件夹
base_dir="文件夹一"
)
2)、shutil.unpack_archive(filename[, extract_dir[, format]])
解包一个归档文件「压缩包」
参数如下:
如果文件名或文件夹名为中文,会出现乱码情况,估计内部使用的是ASCII编码,建议目录均以英文数字命名

在未找到任何解包器的情况下,将引发 ValueError
演示代码:👇
shutil.unpack_archive(
"test.zip",
"./目标文件夹",
format="zip"
)
3)、shutil.register_archive_format(name, function[, extra_args[, description]])
为 name 格式注册一个归档器
4)、shutil.register_unpack_format(name, extensions, function[, extra_args[, description]])
注册一个解包格式
5)、shutil.unregister_archive_format(name)
从支持的格式中移除归档格式
name为格式的名称
6)、shutil.unregister_unpack_format(name)
撤销注册的一个解包格式
name 为格式的名称
7)、shutil.get_archive_formats()
返回支持的归档格式列表
所返回序列中的每个元素为一个元组(名字,描述)「name, description」
默认情况下 shutil 提供以下格式:
演示代码:👇
print(shutil.get_archive_formats())
[('bztar', "bzip2'ed tar-file"), ('gztar', "gzip'ed tar-file"), ('tar', 'uncompressed tar file'), ('xztar', "xz'ed tar-file"), ('zip', 'ZIP file')]
8)、shutil.get_unpack_formats()
返回所有已注册的解包格式列表。
所返回序列中的每个元素为一个元组(名字,扩展,描述)「name, extensions, description」
默认情况下 shutil 提供以下格式:
演示代码:👇
print(shutil.get_unpack_formats())
[('bztar', ['.tar.bz2', '.tbz2'], "bzip2'ed tar-file"), ('gztar', ['.tar.gz', '.tgz'], "gzip'ed tar-file"), ('tar', ['.tar'], 'uncompressed tar file'), ('xztar', ['.tar.xz', '.txz'], "xz'ed tar-file"), ('zip', ['.zip'], 'ZIP file')]
1)、shutil.get_terminal_size(fallback=(columns, lines)):
获取终端窗口的尺寸
会分别检查环境变量 columns 和 lines 两个维度。如果定义了这些变量并且其值为正整数,则将使用这些值。默认情况下未定义 columns 或 lines,则连接到 sys.__stdout__ 的终端将通过发起调用os.get_terminal_size() 被查询。
如果由于系统不支持查询,或是由于我们未连接到某个终端而导致查询终端尺寸不成功,则会使用在 fallback 形参中给出的值,fallback 默认为 (80, 24),这是许多终端模拟器所使用的默认尺寸。
源码:
因为本函数是涉及到os标准库模块中的功能,且实现代码比较简单,我认为还是有必要将源码放出来看看的
def get_terminal_size(fallback=(80, 24)):
"""Get the size of the terminal window.
For each of the two dimensions, the environment variable, COLUMNS
and LINES respectively, is checked. If the variable is defined and
the value is a positive integer, it is used.
When COLUMNS or LINES is not defined, which is the common case,
the terminal connected to sys.__stdout__ is queried
by invoking os.get_terminal_size.
If the terminal size cannot be successfully queried, either because
the system doesn't support querying, or because we are not
connected to a terminal, the value given in fallback parameter
is used. Fallback defaults to (80, 24) which is the default
size used by many terminal emulators.
The value returned is a named tuple of type os.terminal_size.
"""
# columns, lines are the working values
try:
columns = int(os.environ['COLUMNS'])
except (KeyError, ValueError):
columns = 0
try:
lines = int(os.environ['LINES'])
except (KeyError, ValueError):
lines = 0
# only query if necessary
if columns <= 0 or lines <= 0:
try:
size = os.get_terminal_size(sys.__stdout__.fileno())
except (AttributeError, ValueError, OSError):
# stdout is None, closed, detached, or not a terminal, or
# os.get_terminal_size() is unsupported
size = os.terminal_size(fallback)
if columns <= 0:
columns = size.columns
if lines <= 0:
lines = size.lines
return os.terminal_size((columns, lines))
演示代码:👇
返回的值是一个 os.terminal_size 类型的具名元组。
print(shutil.get_terminal_size())
os.terminal_size(columns=80, lines=24)
待更新,敬请期待

