
用 System.IO.Compression 压缩文件为 ZIP 是最轻量、无需第三方库的方案,但默认不支持密码保护和分卷,且对中文路径需额外处理编码问题。这一点,很多初次接触的同学容易忽略。
用 ZipFile.CreateFromDirectory 批量压缩整个文件夹
这算是最省事的方案了,适合打包一个完整目录。它内部自动处理文件遍历、时间戳、目录结构,一调用就完事。但几个限制得心里有数:
- 源目录路径必须是绝对路径,或者确保相对路径能被正确解析,不然会抛出
DirectoryNotFoundException。 - 目标 ZIP 文件若已存在,会直接覆盖——没有提示,也不会抛异常,所以操作前最好自己确认一下。
- 压缩级别只能选
CompressionLevel.Optimal、Faster或NoCompression,不能设为自定义值,灵活性有限。 - 中文文件名是个特殊坑。.NET 6+ 默认使用 UTF-8 编码写入 ZIP 中央目录,解压兼容性不错;但 .NET Framework 4.8 及更早版本默认用系统 OEM 编码,这可能导致 WinRAR 等老工具解压时出现乱码。遇到这种情况,就得改用
ZipArchive手动控制编码了。
using System.IO.Compression; ZipFile.CreateFromDirectory(@"C:\myfolder", @"C:\output.zip", CompressionLevel.Optimal, includeBaseDirectory: false);
用 ZipArchive 精确控制单个文件/子路径压缩
当需要跳过某些文件、重命名 ZIP 内路径、添加空目录,或统一设置编码时,ZipArchive 是唯一选择。它底层更灵活,但同时也要自己管理 ZipArchiveEntry 的创建与流写入,工作量稍微大一些。
- 构造
ZipArchive时必须传入可写的FileStream,且lea veOpen: false更安全,避免文件句柄泄漏。 - 添加文件时,
archive.CreateEntry的第一个参数是 ZIP 内部路径,支持斜杠分隔的子目录形式,比如"docs/readme.txt",但不能以/开头。 - 写入内容必须通过
entry.Open()获取的流来完成,且必须完全写完再 Dispose,否则条目会损坏。 - 要支持中文名且兼顾旧解压工具,可在创建
ZipArchive时传入new ZipArchiveOptions { CodePage = 65001 }(仅 .NET 6+ 支持)。
using (var fs = File.Create(@"C:\custom.zip"))
using (var archive = new ZipArchive(fs, ZipArchiveMode.Create, lea veOpen: false))
{
var entry = archive.CreateEntry("config/appsettings.json");
using (var entryStream = entry.Open())
using (var fileStream = File.OpenRead(@"D:\appsettings.json"))
{
fileStream.CopyTo(entryStream);
}
}
压缩单个文件时别漏掉 includeBaseDirectory: false
如果误用 ZipFile.CreateFromDirectory 去“压缩一个文件”,常见做法是把它放进临时文件夹再压缩——这时若忘记设 includeBaseDirectory: false,ZIP 里就会多一层文件夹,比如 temp\myfile.txt 而非直接 myfile.txt,使用起来很不方便。
- 更干净的做法是不用
CreateFromDirectory,改用ZipArchive直接添加单文件(见上一节)。 - 如果坚持用
CreateFromDirectory,务必确保临时目录下只有目标文件,且调用时显式传includeBaseDirectory: false。 - 注意:该参数对空目录无效;若源目录为空,生成的 ZIP 也会为空(不含任何条目)。
解压时路径穿越漏洞必须手动过滤
必须警惕的是,ZipFile.ExtractToDirectory 和 ZipArchive.ExtractAll 默认不做路径校验。恶意 ZIP 若包含 ../../../etc/passwd 这类路径,可能覆写系统文件。这不是 .NET 的 bug,而是 ZIP 规范允许任意路径,防御责任在使用者身上。
- 提取前,对每个
ZipArchiveEntry.FullName调用Path.GetFullPath,并检查是否仍落在目标解压目录内。 - 推荐用
Path.IsPathRooted(entry.FullName)快速拦截绝对路径,再用entry.FullName.Contains("..")做初步筛查(注意跨平台路径分隔符)。 - 不要依赖
entry.Name,它只返回最后一段,无法反映完整路径结构。
真正麻烦的,往往不是压的过程,而是压完之后,别人能不能正常解压、文件名会不会乱码、有没有人故意塞个超长路径来搞事情——这些细节不写进代码里,上线后就是事故。