zipfile 压缩文件
当程序需要把课程资料打包、交付或解压时,如何确认归档结构正确、路径不越界,并控制展开后的资源规模?zipfile 模块用于创建、读取和解压 ZIP 文件。ZIP 可以把多个文件和目录打包成一个文件,并根据配置压缩其中的数据。
本章完成一条完整流程:
- 把课程资料写入 ZIP。
- 查看并校验归档成员。
- 不解压直接读取一个文件。
- 检查路径和展开大小。
- 解压到明确的目标目录。
zipfile 只处理 ZIP 格式,不负责 RAR 或 7z 文件。
归档创建与写入
zipfile.ZipFile 使用模式控制打开行为:
| 模式 | 作用 | 注意事项 |
|---|---|---|
r | 读取已有 ZIP | 文件不存在或格式错误时会报错 |
w | 创建 ZIP | 已有文件会被覆盖 |
x | 创建新 ZIP | 已有文件会抛出 FileExistsError |
a | 追加成员 | 可能产生同名成员 |
创建新压缩包时,如果不允许覆盖已有文件,优先使用 x 模式。
ZipFile 应配合 with 使用,离开代码块时会自动写完目录信息并关闭文件:
from pathlib import Path
import zipfile
archive_path = Path("course.zip")
with zipfile.ZipFile(archive_path, "x") as zip_file:
pass压缩方式:
ZipFile 默认使用 ZIP_STORED,只打包文件,不压缩数据。需要常见的 ZIP 压缩时,应明确使用 ZIP_DEFLATED:
with zipfile.ZipFile(
archive_path,
"x",
compression=zipfile.ZIP_DEFLATED,
compresslevel=6,
) as zip_file:
pass其中:
ZIP_STORED:只存储,不压缩。ZIP_DEFLATED:使用常见的 Deflate 算法压缩。compresslevel=6:在压缩速度和文件大小之间取一个常用平衡值。
图片、视频等文件本身可能已经压缩,再放入 ZIP 后不一定会明显变小。很小的文件还可能因为归档信息而略微变大。
写入文件与内部路径:
ZipFile.write() 把磁盘文件写入 ZIP:
zip_file.write(source_path)如果不指定归档内部名称,源路径的一部分可能被带入 ZIP。推荐通过 arcname 主动控制:
zip_file.write(
source_path,
arcname="course_materials/readme.txt",
)批量写入目录时,可以使用 Path.rglob():
from pathlib import Path, PurePosixPath
import zipfile
source_dir = Path("source_files")
archive_path = Path("course.zip")
with zipfile.ZipFile(
archive_path,
"x",
compression=zipfile.ZIP_DEFLATED,
compresslevel=6,
) as zip_file:
for file_path in sorted(source_dir.rglob("*")):
if not file_path.is_file():
continue
relative_path = file_path.relative_to(source_dir)
archive_name = PurePosixPath("course_materials", *relative_path.parts)
zip_file.write(file_path, archive_name.as_posix())ZIP 内部路径统一使用 /。PurePosixPath 只负责生成归档名称,不会访问文件系统。
归档读取与完整性检查
以读取模式打开 ZIP 后,可以通过 infolist() 得到所有 ZipInfo 对象:
import zipfile
with zipfile.ZipFile("course.zip", "r") as zip_file:
for info in zip_file.infolist():
print(info.filename)
print(info.is_dir())
print(info.file_size)
print(info.compress_size)| 属性或方法 | 含义 |
|---|---|
filename | ZIP 中保存的成员名称 |
is_dir() | 成员是否为目录 |
file_size | 原始数据大小 |
compress_size | 压缩后数据大小 |
namelist() 只返回成员名称列表;需要大小等详细信息时,直接使用 infolist() 更方便。
不解压读取成员:
read() 可以直接读取 ZIP 中某个成员,返回字节数据:
import zipfile
with zipfile.ZipFile("course.zip", "r") as zip_file:
content = zip_file.read("course_materials/readme.txt")
print(content.decode("utf-8"))只需要查看配置或少量文本时,不必先把整个 ZIP 解压到磁盘。
归档结构与成员校验:
ZIP 结构判断:
zipfile.is_zipfile() 可以判断文件是否具有有效的 ZIP 结构:
import zipfile
if zipfile.is_zipfile("course.zip"):
print("这是一个 ZIP 文件")它只能判断 ZIP 结构,不能证明文件内容安全可信。
成员完整性检查:
testzip() 会读取归档成员并检查 CRC:
import zipfile
with zipfile.ZipFile("course.zip", "r") as zip_file:
bad_member = zip_file.testzip()
if bad_member is None:
print("成员检查通过")
else:
print(f"损坏成员:{bad_member}")归档较大时,完整检查需要读取全部成员,可能花费较长时间。
安全解压与边界检查
extract() 解压一个成员,extractall() 解压多个成员:
from pathlib import Path
import zipfile
extract_dir = Path("extracted")
with zipfile.ZipFile("course.zip", "r") as zip_file:
zip_file.extract("course_materials/readme.txt", path=extract_dir)
zip_file.extractall(path=extract_dir)目标目录不存在时会自动创建。目标位置已经有同名文件时,解压可能覆盖原有内容,因此应使用专门的空目录接收解压结果。
当前 CPython 3.14 会在解压时规范化明显的盘符、绝对路径和 .. 片段,但来源不明的 ZIP 仍然不应该直接交给 extractall()。解压前还需要检查成员名称、展开大小和目标范围。
解压前边界检查:
下面的函数完成三个基础检查:
- 拒绝绝对路径、上级目录和疑似 Windows 盘符。
- 确认计算后的目标仍位于解压目录中。
- 限制所有成员的总展开大小。
from pathlib import Path, PurePosixPath
MAX_TOTAL_SIZE = 10 * 1024 * 1024
def validate_members(zip_file, extract_dir):
extract_root = extract_dir.resolve()
total_size = 0
for info in zip_file.infolist():
normalized_name = info.filename.replace("\\", "/")
member_path = PurePosixPath(normalized_name)
if member_path.is_absolute() or ".." in member_path.parts:
raise ValueError(f"不安全的成员路径:{info.filename}")
if member_path.parts and ":" in member_path.parts[0]:
raise ValueError(f"不安全的成员路径:{info.filename}")
destination = (extract_root / Path(*member_path.parts)).resolve()
if not destination.is_relative_to(extract_root):
raise ValueError(f"成员超出解压目录:{info.filename}")
total_size += info.file_size
if total_size > MAX_TOTAL_SIZE:
raise ValueError("ZIP 展开后的总大小超过限制")
return total_size这个检查适合课程案例,但不能代替真实系统的完整安全策略。处理外部 ZIP 时,还要考虑文件数量、单个文件大小、磁盘空间、覆盖策略和资源限制。
文件名与内容编码
Python 创建包含中文名称的 ZIP 时,通常会自动使用 ZIP 的 UTF-8 标记:
zip_file.write(source_path, "课程资料/说明.txt")现代工具一般可以正确读取。旧软件生成的 ZIP 可能没有正确标记文件名编码,读取时才可能需要针对来源设置 metadata_encoding。不要在没有确认压缩包来源和编码的情况下随意猜测。
文件内容编码与文件名编码是两件事。通过 read() 读取文本内容后,仍然要使用正确编码解码,例如 decode("utf-8")。
案例:创建并安全解压课程资料
需求: 将课程资料打包为可交付的 ZIP,检查归档结构和成员完整性,读取其中的文本,并在确认路径与展开大小后解压到临时目录。
功能分析: 使用 ZIP_DEFLATED 和 arcname 创建稳定的内部路径,通过 is_zipfile()、infolist()、testzip() 完成校验,再用成员路径与总展开大小检查保护 extractall() 的目标范围。
案例提供下面的源文件:
01-safe-archive/
├── main.py
└── source_files/
├── readme.txt
├── data/
│ └── scores.csv
└── notes/
└── lesson.md按目录树创建三个源文件,程序会把它们写入 ZIP:
课程压缩案例源文件。name,score
张三,90
李四,85# ZIP 文件练习
使用 zipfile 创建和安全解压课程资料。程序在临时工作区中创建 ZIP、检查成员、读取文本并安全解压:
from pathlib import Path, PurePosixPath
from tempfile import TemporaryDirectory
import zipfile
MAX_TOTAL_SIZE = 10 * 1024 * 1024
def create_archive(source_dir, archive_path):
with zipfile.ZipFile(
archive_path,
"x",
compression=zipfile.ZIP_DEFLATED,
compresslevel=6,
) as zip_file:
for file_path in sorted(source_dir.rglob("*")):
if not file_path.is_file():
continue
relative_path = file_path.relative_to(source_dir)
archive_name = PurePosixPath("course_materials", *relative_path.parts)
zip_file.write(file_path, archive_name.as_posix())
def validate_members(zip_file, extract_dir):
extract_root = extract_dir.resolve()
total_size = 0
for info in zip_file.infolist():
normalized_name = info.filename.replace("\\", "/")
member_path = PurePosixPath(normalized_name)
if member_path.is_absolute() or ".." in member_path.parts:
raise ValueError(f"不安全的成员路径:{info.filename}")
if member_path.parts and ":" in member_path.parts[0]:
raise ValueError(f"不安全的成员路径:{info.filename}")
destination = (extract_root / Path(*member_path.parts)).resolve()
if not destination.is_relative_to(extract_root):
raise ValueError(f"成员超出解压目录:{info.filename}")
total_size += info.file_size
if total_size > MAX_TOTAL_SIZE:
raise ValueError("ZIP 展开后的总大小超过限制")
return total_size
def main():
script_dir = Path(__file__).resolve().parent
source_dir = script_dir / "source_files"
with TemporaryDirectory() as temp_dir:
workspace = Path(temp_dir)
archive_path = workspace / "course.zip"
extract_dir = workspace / "extracted"
create_archive(source_dir, archive_path)
print(f"ZIP 格式有效:{zipfile.is_zipfile(archive_path)}")
with zipfile.ZipFile(archive_path, "r") as zip_file:
print("归档成员:")
for info in zip_file.infolist():
print(f"- {info.filename}")
print(f"成员检查通过:{zip_file.testzip() is None}")
content = zip_file.read("course_materials/readme.txt")
print(f"读取内容:{content.decode('utf-8')}")
validate_members(zip_file, extract_dir)
zip_file.extractall(extract_dir)
extracted_root = extract_dir / "course_materials"
extracted_files = sorted(
path.relative_to(extracted_root).as_posix()
for path in extracted_root.rglob("*")
if path.is_file()
)
print("解压文件:")
for filename in extracted_files:
print(f"- {filename}")
if __name__ == "__main__":
main()运行结果:
ZIP 格式有效:True
归档成员:
- course_materials/data/scores.csv
- course_materials/notes/lesson.md
- course_materials/readme.txt
成员检查通过:True
读取内容:课程压缩案例源文件。
解压文件:
- data/scores.csv
- notes/lesson.md
- readme.txt案例生成的 ZIP 和解压目录都位于临时工作区,运行结束后会自动清理,不会污染课程目录。
小结
ZipFile的r、w、x、a模式分别用于读取、覆盖创建、安全创建和追加。- 默认的
ZIP_STORED不压缩数据,常见 ZIP 压缩使用ZIP_DEFLATED。 arcname用于控制 ZIP 内部路径,避免写入不需要的上级目录。infolist()可以查看成员名称、类型和大小。is_zipfile()检查 ZIP 结构,testzip()检查成员 CRC。- 解压前应检查成员路径、总展开大小和目标覆盖范围。
- ZIP 文件名编码和文件内容编码是两个不同问题。
练习
- 使用
ZIP_DEFLATED创建一个新的 ZIP,并通过arcname控制内部路径。 - 使用
infolist()输出每个成员的原始大小和压缩后大小。 - 使用
read()直接读取 ZIP 中的一个 UTF-8 文本文件。 - 修改安全检查函数,增加文件数量限制。
- 在临时目录中解压案例 ZIP,并验证解压后的文件内容。