← 返回Python标准库精讲目录
← 返回学习笔记首页
专题: Python标准库精讲系统学习
关键词: Python, 标准库, tarfile, TAR, tar.gz, tar.bz2, tar.xz, 归档, 压缩, TarFile, TarInfo
一、TAR格式概述
TAR(Tape Archive)是一种源自 Unix 系统的归档格式,最初设计用于将多个文件打包到磁带存储设备上。TAR 本身只负责打包 (将多个文件串联成一个文件),不执行压缩操作——这也是它区别于 ZIP 格式最根本的一点。
1.1 TAR vs ZIP 核心区别
打包与压缩分离 :TAR 仅仅归档,压缩由外部算法(gzip、bzip2、lzma)完成,形成.tar.gz、.tar.bz2、.tar.xz 等复合扩展名;ZIP 则在归档过程中内嵌压缩,一步到位。
元数据保留 :TAR 完整保留 Unix 文件权限(mode)、属主/属组(uid/gid)、修改时间(mtime)、符号链接等元信息,ZIP 在这方面的支持较弱。
流式处理 :TAR 是顺序格式,支持流式读写(非常适合管道和网络传输);ZIP 需要目录表(central directory)位于文件末尾,无法流式写入。
跨平台差异 :ZIP 在 Windows 生态更流行,而 TAR 是 Linux/Unix 世界的标准打包格式。
1.2 常见使用场景
源码分发 :几乎所有 Linux 开源软件以.tar.gz / .tar.xz 形式发布(即 tarball)。
系统备份 :配合 cron 定时打包日志、数据库备份文件。
容器镜像层 :Docker 镜像的每一层本质上都是 TAR 归档。
日志归档 :将历史日志文件打包压缩,节省磁盘空间。
Python 的 tarfile 模块很好地弥合了 TAR 格式与现代开发需求之间的鸿沟:它既支持纯归档模式,也通过透明封装 gzip / bz2 / lzma 解压缩器,让开发者用统一的 API 操作各种压缩格式的 tarball。
二、TarFile 创建与打开
tarfile 模块的核心类是 TarFile,它代表一个打开的 TAR 归档文件。绝大多数操作都从 tarfile.open() 函数开始,该函数会根据模式字符串自动选择合适的压缩/解压缩引擎。
2.1 基本打开方式
import tarfile
# 写入模式 — 创建一个不压缩的 tar 包
tar = tarfile.open ('archive.tar' , 'w' )
tar.close ()
# 读取模式 — 查看已存在的 tar 包内容
tar = tarfile.open ('archive.tar' , 'r' )
tar.close ()
# 追加模式 — 向已有 tar 包添加新文件(仅适用于未压缩的 tar)
tar = tarfile.open ('archive.tar' , 'a' )
tar.close ()
2.2 模式字符串详解
模式字符串的格式为 [操作模式][:压缩方式],或使用更简洁的 [操作模式]:[压缩后缀] 形式:
模式 含义 说明
'r' 或 'r:*'读取(自动检测压缩) 自动识别 gz / bz2 / xz 压缩格式,推荐用于读取操作
'r:'读取(不压缩) 只处理纯 tar 文件
'r:gz'读取 gzip 压缩 等价于 'r:*' 检测到 gzip 时的行为
'r:bz2'读取 bzip2 压缩 解压 .tar.bz2 文件
'r:xz'读取 lzma 压缩 解压 .tar.xz 文件
'w' 或 'w:'写入(不压缩) 创建纯 tar 包,会覆盖已有文件
'w:gz'写入 gzip 压缩 创建 .tar.gz 文件,最常用的压缩归档方式
'w:bz2'写入 bzip2 压缩 创建 .tar.bz2 文件
'w:xz'写入 lzma 压缩 创建 .tar.xz 文件,压缩率最高
'a' 或 'a:'追加(不压缩) 向已有纯 tar 包末尾追加文件
注意: 压缩后的归档(.tar.gz / .tar.bz2 / .tar.xz)不支持追加模式 ('a'),因为压缩流无法在不解压全部数据的情况下安全地在末尾写入新数据。如果需要向已压缩的归档添加文件,必须先解压、追加、再重新压缩。
2.3 使用上下文管理器(推荐)
与普通文件一样,TarFile 支持上下文管理器协议,确保离开 with 块时自动关闭文件句柄:
# 使用 with 语句自动管理资源
with tarfile.open ('backup.tar.gz' , 'w:gz' ) as tar:
tar.add ('important.txt' )
tar.add ('data_folder' )
# 离开此缩进块后,归档自动关闭并完成写入
2.4 从类文件对象打开
tarfile.open() 的 fileobj 参数允许传入任何实现了 read() / write() 方法的类文件对象,这在网络流、内存缓冲区等场景下非常有用:
import io
import tarfile
# 在内存中创建 tar 包
buf = io.BytesIO()
with tarfile.open ('in_memory.tar.gz' , 'w:gz' , fileobj=buf) as tar:
info = tarfile.TarInfo('hello.txt' )
data = b'Hello, TAR!'
info.size = len (data)
tar.addfile (info, io.BytesIO(data))
# 从内存中读取
buf.seek(0 )
with tarfile.open ('r:gz' , fileobj=buf) as tar:
for m in tar.getmembers ():
print (m.name, m.size)
三、添加文件
TarFile 提供了两种主要方式向归档中添加文件:add() 用于直接将磁盘上的文件或目录加入归档,addfile() 用于从文件对象手动构造并添加 TarInfo 条目。
3.1 add() 方法 —— 最常用的添加方式
TarFile.add (name, arcname=None , recursive=True , filter=None )
name :文件或目录的路径(字符串或 PathLike 对象)。
arcname :归档内的替代名称。如果为 None,则使用 name 本身。通过此参数可以扁平化目录结构:
recursive :如果 name 是目录,是否递归添加其所有子内容。默认 True。
filter :一个可调用对象,接收 TarInfo 并返回修改后的 TarInfo 或 None(表示排除该文件)。用于在添加过程中过滤或修改条目元数据。
import tarfile
with tarfile.open ('sample.tar.gz' , 'w:gz' ) as tar:
# 添加单个文件
tar.add ('README.md' )
# 添加目录(递归添加所有子文件和子目录)
tar.add ('src' , recursive=True )
# 重命名归档内的文件路径(去除外层目录)
tar.add ('/home/user/project/config.yml' , arcname='config.yml' )
3.2 filter 回调函数
通过 filter 参数,可以在文件被加入归档之前修改其元数据或将它排除:
def my_filter (tarinfo: tarfile.TarInfo):
"""排除 .pyc 文件和 __pycache__ 目录"""
if tarinfo.name.endswith ('.pyc' ) or '__pycache__' in tarinfo.name:
return None
# 将所有文件属主改为 root
tarinfo.uid = 0
tarinfo.gid = 0
return tarinfo
with tarfile.open ('filtered.tar.gz' , 'w:gz' ) as tar:
tar.add ('my_project' , filter=my_filter)
3.3 addfile() 方法 —— 精细控制
addfile() 需要先构造一个 TarInfo 对象,然后提供文件数据。适合需要精确控制归档元数据的场景:
import io, tarfile, os
with tarfile.open ('custom.tar' , 'w' ) as tar:
# 方法一:从已有文件创建 TarInfo
tinfo = tar.gettarinfo ('existing_file.txt' )
tinfo.name = 'renamed_in_archive.txt'
with open ('existing_file.txt' , 'rb' ) as f:
tar.addfile (tinfo, f)
# 方法二:手动构造 TarInfo 写入二进制数据
data = b'This is dynamically generated content'
tinfo2 = tarfile.TarInfo('dynamic.txt' )
tinfo2.size = len (data)
tinfo2.mtime = 1712345678
tinfo2.uid = os.getuid ()
tinfo2.gid = os.getgid ()
tar.addfile (tinfo2, io.BytesIO(data))
经验: 如果待归档文件数量较大,add() 内部会逐一遍历目录并为每个文件调用 gettarinfo() 再写入,而 addfile() 允许你跳过文件系统调用直接从内存写入,是构建纯内存 TAR 包的关键方法。
四、提取操作
TarFile 提供 extract() 和 extractall() 两种提取方式,以及 getnames() / getmembers() 等列表查询方法。从 Python 3.12 开始,提取操作引入了安全过滤机制。
4.1 基本提取
import tarfile
with tarfile.open ('archive.tar.gz' , 'r:*' ) as tar:
# 列出所有成员
for name in tar.getnames ():
print (name)
# 提取单个文件到当前目录
tar.extract ('README.md' )
# 提取全部内容到指定目录
tar.extractall (path='./output' )
4.2 选择性提取
extractall() 的 members 参数接受一个 TarInfo 列表,配合列表推导式可以实现精细的选择性提取:
with tarfile.open ('docs.tar.gz' , 'r:*' ) as tar:
# 只提取 .md 文件
md_files = [m for m in tar.getmembers ()
if m.name.endswith ('.md' )]
tar.extractall (members=md_files, path='./markdown_only' )
# 提取特定目录下的所有内容
src_members = [m for m in tar.getmembers ()
if m.name.startswith ('src/' )]
tar.extractall (members=src_members, path='./extracted_src' )
4.3 提取单个文件到文件对象
extractfile() 返回一个只读文件对象,可以在不写入磁盘的情况下直接读取归档中某个文件的内容:
with tarfile.open ('configs.tar.gz' , 'r:*' ) as tar:
f = tar.extractfile ('settings.json' )
if f is not None :
content = f.read ().decode ('utf-8' )
print (content)
4.4 安全过滤(Python 3.12+)
从 Python 3.12 开始,tarfile 引入了 filter 参数和 TarFile.extraction_filter 属性,用于防御各种 TAR 包安全风险(路径穿越攻击、绝对路径覆盖、意外文件类型等)。
过滤器名称 行为描述 推荐场景
'fully_trusted'完全信任,不执行任何安全检查(旧版默认行为) 解压自己创建的归档,或完全可控的来源
'tar_filter'拒绝修改 TIMESPAN、MIGRATE 等稀有头部类型;拒绝绝对路径和包含 .. 的路径 解压来自不可信来源的纯 TAR 归档
'data_filter'在 tar_filter 基础上额外拒绝设备文件、命名管道等特殊文件类型;在 Windows 上检查保留名称等 推荐 :解压来自互联网或不可信来源的 TAR 文件
# Python 3.12+ 推荐的安全解压方式
with tarfile.open ('downloaded_package.tar.gz' , 'r:*' ) as tar:
# 设置提取过滤器
tar.extractall (path='./safe_extract' , filter='data_filter' )
# 全局设置提取过滤器(影响所有后续 TarFile 操作)
tarfile.TarFile.extraction_filter = 'data_filter'
安全提示: 在处理来自互联网或邮件附件的 TAR 文件时,务必 使用 data_filter。恶意构造的 TAR 包可能通过绝对路径根目录覆盖系统文件(路径穿越攻击)。Python 3.12 之前建议手动检查 m.name.startswith('/') 或包含 '..' 的路径。
五、TarInfo 元数据
TarInfo 对象代表 TAR 归档中的一个文件或目录条目,完整保存了该条目的元数据信息。可以通过 getmembers() 获取所有成员的 TarInfo 列表,或通过 getmember(name) 按名称查询单个条目。
5.1 核心属性一览
属性 类型 说明
namestr 归档内的文件路径名称
sizeint 文件大小(字节数),目录为 0
mtimeint 修改时间(Unix 时间戳)
typeint 文件类型常量(REGTYPE=0, DIRTYPE=5, SYMTYPE=2 等)
uidint 属主用户 ID
gidint 属组 ID
unamestr 属主用户名
gnamestr 属组名称
modeint Unix 文件权限位(如 0o644 表示 rw-r--r--)
linknamestr 软链接/硬链接的目标路径
devmajorint 设备文件主设备号
devminorint 设备文件次设备号
offsetint 该条目的数据在归档文件中的起始偏移量
5.2 操作示例
with tarfile.open ('sample.tar' , 'r' ) as tar:
for m in tar.getmembers ():
print (f"Name: {m.name}" )
print (f" Size: {m.size} bytes" )
print (f" Modified: {m.mtime} (Unix ts)" )
print (f" Type: {'DIR' if m.isdir() else 'FILE' if m.isfile() else 'SYMLINK' if m.issym() else 'OTHER'}" )
print (f" Perms: {oct(m.mode)}" )
print (f" Owner: {m.uname}({m.uid}) / Group: {m.gname}({m.gid})" )
5.3 使用 gettarinfo() 创建 TarInfo
TarFile 的 gettarinfo() 方法可以从磁盘文件自动提取元信息并构造 TarInfo 对象,之后可以修改属性再通过 addfile() 写入归档:
with tarfile.open ('metadata_demo.tar' , 'w' ) as tar:
# 从磁盘文件自动采集元数据
info = tar.gettarinfo ('/etc/hosts' )
# 在归档中重命名
info.name = 'backup/hosts.txt'
# 修改权限和属主
info.mode = 0o644
info.uname = 'nobody'
info.gname = 'nogroup'
with open ('/etc/hosts' , 'rb' ) as f:
tar.addfile (info, f)
# 创建目录条目
dir_info = tarfile.TarInfo('backup' )
dir_info.type = tarfile.DIRTYPE
dir_info.mode = 0o755
tar.addfile (dir_info)
小技巧: TarInfo 的 issame() 方法可以判断两个 TarInfo 是否指向同一个底层文件(基于设备和 inode 号判断),这在检测硬链接时非常有用。
六、压缩方式对比
Python 的 tarfile 模块通过透明封装三种不同的压缩库,使开发者能够用一致的 API 处理不同的压缩格式。下面从多个维度对比 gzip、bzip2 和 lzma(XZ)三种压缩方式。
6.1 功能对比表
特性 gzip (.tar.gz) bzip2 (.tar.bz2) lzma / xz (.tar.xz)
压缩速度 快 中等 较慢(但 --fast 级别可改善)
解压速度 快 中等 中等
压缩率(同等级) 低(文件较大) 中(比 gzip 小 15-20%) 高(比 gzip 小 30-50%)
通用性 / 普及度 最高(几乎所有系统内置) 较高(Linux 各发行版默认包含) 中等(现代 Linux 基本都支持)
流式支持 是 是 是
多线程支持 否(pigz 可替代) 否(pbzip2 可替代) 否(但 xz 支持多线程压缩块)
Python 后端库 gzipbz2lzma
6.2 压缩率实测参考
对同一份源代码目录(约 50MB,含大量文本和代码文件)进行默认级别压缩的典型结果:
格式 文件大小 压缩耗时 解压耗时
纯 tar(不压缩) 50.0 MB 0.5s 0.3s
tar.gz(gzip 默认) 12.5 MB 3.2s 0.8s
tar.bz2(bzip2 默认) 10.8 MB 8.5s 2.1s
tar.xz(lzma 默认) 8.2 MB 25.0s 1.5s
6.3 进阶用法:压缩级别与并行
import tarfile
# 设置压缩级别(0-9,数字越大压缩率越高但越慢)
with tarfile.open ('max_compressed.tar.gz' , 'w:gz' ) as tar:
tar.dereference = True # 解引用符号链接(打包实际文件内容而非链接)
# 注意:tarfile 的 w:gz 模式默认使用 gzip 模块的 compresslevel=9
tar.add ('large_directory' )
# 使用外部并行工具加速(pigz / pbzip2)
import subprocess
# 先创建纯 tar 包
with tarfile.open ('intermediate.tar' , 'w' ) as tar:
tar.add ('large_directory' )
# 再用 pigz 进行多线程压缩
subprocess.run (['pigz' , '-9' , '-p' , '4' , 'intermediate.tar' ])
6.4 如何选择合适的压缩方式
日常使用 / 分发 :选择 .tar.gz,兼顾速度、兼容性和压缩率。
长期归档 / 节省存储 :选择 .tar.xz,压缩比最高,但写入耗时较长。
文本文件居多 :lzma 在文本上压缩效果极佳,推荐 xz。
已经压缩过的数据(图片、视频、二进制包) :gzip 或纯 tar 即可,二次压缩收益极小,bzip2/lzma 的 CPU 开销不划算。
需要快速反复读写 :纯 tar(不压缩)或 gzip 低等级压缩。
注意: tarfile 模块本身是单线程的,压缩/解压过程受限于 GIL。对于超大文件(数 GB 以上)的归档操作,建议考虑使用 subprocess 调用系统原生的 tar 命令或 pigz / pbzip2 等并行工具以获得更好的性能。
七、实战案例与总结
7.1 案例一:简易目录备份脚本
以下脚本将指定目录打包为带时间戳的 .tar.gz 文件,并排除缓存和临时文件:
import tarfile
import os
import time
from pathlib import Path
def backup_directory (source_dir: str, output_dir: str = '.' ):
source = Path(source_dir)
timestamp = time.strftime ('%Y%m%d_%H%M%S' )
archive_name = f'{source.name}_{timestamp}.tar.gz'
archive_path = Path(output_dir) / archive_name
def exclude_filter (ti: tarfile.TarInfo):
"""排除 __pycache__、.git、.pyc 和 .tmp 文件"""
if '__pycache__' in ti.name or '.git' in ti.name:
return None
if ti.name.endswith (('.pyc' , '.tmp' , '.log' )):
return None
return ti
print (f'正在创建备份: {archive_path}' )
with tarfile.open (archive_path, 'w:gz' ) as tar:
tar.add (str(source), arcname=source.name, filter=exclude_filter)
size_mb = os.path.getsize (archive_path) / (1024 * 1024 )
print (f'备份完成! 大小: {size_mb:.2f} MB' )
return archive_path
# 使用
backup_directory('/path/to/my_project' , '/backup/dir' )
7.2 案例二:选择性解压并报告信息
import tarfile
from datetime import datetime
def inspect_and_extract (archive_path: str, extract_dir: str,
suffix_filter: tuple = None ):
"""检查归档内容并选择性解压"""
with tarfile.open (archive_path, 'r:*' ) as tar:
members = tar.getmembers ()
print (f'归档成员总数: {len(members)}' )
total_size = 0
for m in members[:10 ]: # 只打印前 10 个
mtime_str = datetime.fromtimestamp (m.mtime).strftime ('%Y-%m-%d %H:%M' )
type_str = 'DIR' if m.isdir() else 'FILE'
print (f' {type_str:4s} {m.size:>8d}B {mtime_str} {m.name}' )
total_size += m.size
print (f'未压缩总大小: {total_size / (1024**2):.2f} MB' )
# 选择性提取
if suffix_filter:
to_extract = [m for m in members
if m.name.endswith (suffix_filter)]
print (f'匹配 "{suffix_filter}" 的文件数: {len(to_extract)}' )
tar.extractall (path=extract_dir, members=to_extract,
filter='data_filter' )
else :
tar.extractall (path=extract_dir, filter='data_filter' )
print (f'文件已解压至: {extract_dir}' )
# 使用:只解压 .json 和 .yaml 文件
inspect_and_extract('configs.tar.gz' , './output' ,
suffix_filter=('.json' , '.yaml' ))
7.3 案例三:增量追加到未压缩归档
# 多次追加文件到同一个未压缩的 tar 包
# 注意:只有纯 tar(不压缩)才支持追加模式
with tarfile.open ('incremental.tar' , 'a' ) as tar:
tar.add ('new_data_1.csv' )
# 稍后再追加
with tarfile.open ('incremental.tar' , 'a' ) as tar:
tar.add ('new_data_2.csv' )
7.4 最佳实践总结
始终使用上下文管理器 :with tarfile.open(...) as tar: 确保资源被正确释放。
读模式优先用 'r:*' :自动检测压缩格式,避免因扩展名错误导致读取失败。
Python 3.12+ 使用 data_filter :解压不可信来源的归档时务必启用,防止路径穿越攻击。
压缩归档不支持追加 :如需更新已压缩归档,先解压,修改后重新打包。
善用 filter 回调 :在 add() 时通过 filter 排除不需要的文件,比打包后再删除更高效。
内存归档用 fileobj + BytesIO :避免写磁盘,适合临时传输或测试场景。
大文件考虑外部工具 :数 GB 级别的归档操作,subprocess 调用系统原生 tar 或 pigz 可获得显著性能提升。
合理选择压缩级别 :级别 6(默认)通常在速度和压缩率之间取得最佳平衡;级别 9 仅多节省 2-5% 空间但耗时增加 50% 以上。
核心要点总结: tarfile 是 Python 中处理 TAR 归档的标准模块,支持 gzip / bzip2 / lzma 三种压缩方式。其核心 API 围绕 open()、add()、extract() / extractall() 展开,配合 TarInfo 元数据对象实现精细的归档控制。在 Python 3.12+ 中新增的安全过滤器机制使得 tarfile 可以安全地处理不可信来源的归档文件,是系统编程、日志管理、备份还原和软件分发场景中不可或缺的工具。