tarfile模块 — TAR归档处理

Python标准库精讲专题 · 压缩与归档篇 · 掌握TAR归档处理

专题:Python标准库精讲系统学习

关键词:Python, 标准库, tarfile, TAR, tar.gz, tar.bz2, tar.xz, 归档, 压缩, TarFile, TarInfo

一、TAR格式概述

TAR(Tape Archive)是一种源自 Unix 系统的归档格式,最初设计用于将多个文件打包到磁带存储设备上。TAR 本身只负责打包(将多个文件串联成一个文件),不执行压缩操作——这也是它区别于 ZIP 格式最根本的一点。

1.1 TAR vs ZIP 核心区别

1.2 常见使用场景

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)
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属组名称
modeintUnix 文件权限位(如 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 MB0.5s0.3s
tar.gz(gzip 默认)12.5 MB3.2s0.8s
tar.bz2(bzip2 默认)10.8 MB8.5s2.1s
tar.xz(lzma 默认)8.2 MB25.0s1.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 如何选择合适的压缩方式

注意: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 最佳实践总结

核心要点总结:tarfile 是 Python 中处理 TAR 归档的标准模块,支持 gzip / bzip2 / lzma 三种压缩方式。其核心 API 围绕 open()add()extract() / extractall() 展开,配合 TarInfo 元数据对象实现精细的归档控制。在 Python 3.12+ 中新增的安全过滤器机制使得 tarfile 可以安全地处理不可信来源的归档文件,是系统编程、日志管理、备份还原和软件分发场景中不可或缺的工具。