简介:这份资源面向.NET平台下需要处理ZIP压缩文件的C#开发者,重点讲解如何借助开源库ICSharpCode.SharpZipLib.dll完成解压缩操作,适合具备一定C#基础、希望快速落地文件压缩与解压功能的初中级程序员。压缩包共29个文件,以cs源码、dll动态库、exe可执行程序为主,另含sln解决方案、csproj工程文件、resx资源与pdb调试符号等,整体约342KB,结构完整,可直接在Visual Studio中打开参考。资源围绕ZipInputStream遍历条目、区分目录与文件、流式拷贝内容等核心用法展开,并给出UnzipFile方法的完整实现与调用示例,读者可据此掌握ZIP读写、目录创建与异常判断等关键环节,也可在此基础上进一步封装适配自身业务。目前已有882人学习下载,适合作为C#文件压缩解压功能的入门与查阅素材。
1. 从一次批量解压翻车说起:C# 里到底该用哪套 Zip 方案
上周帮一个做数据归档的朋友处理一批历史备份,三千多个 zip 包要按规则解压、重命名、再归到对应目录。他一开始用系统自带的解压工具手动点,点到第两百个就崩溃了。我接手后第一反应是写个 C# 小工具跑批处理,结果在选库这一步就卡了一下——.NET 自带的System.IO.Compression够用吗?还是得上ICSharpCode.SharpZipLib.dll?这两个东西在中文技术圈里被搜了十几年,关键词「zip 解压缩文件 ICSharpCode.SharpZipLib.dll」至今还有大量人在查,说明踩坑的人一直没断过。
这篇笔记就围绕这个场景展开:用 C# 做 zip 解压缩,什么时候用原生库、什么时候必须请出 SharpZipLib,以及那个 dll 到底怎么引、参数怎么设、哪些坑会让你半夜爬起来改代码。适合已经会写 C# 但没系统处理过压缩包的从业者,也适合被某个加密 zip 或中文文件名搞到怀疑人生的老手。下面按「选型 → 上手 → 避坑 → 进阶」的顺序推。
2. 选型先立住:原生库和 SharpZipLib 的边界在哪
2.1 两套方案的底层差异
.NET 从 Framework 4.5 开始内置了System.IO.Compression命名空间,核心是ZipArchive、ZipFile、DeflateStream这几个类。它底层调用的是系统自带的 zlib 实现,优点是零依赖、开箱即用,缺点是功能被刻意做窄了——不支持 zip 传统加密(ZipCrypto),不支持 AES 加密,对分卷压缩包基本没辙,早期版本对中文文件名还会出现乱码。
ICSharpCode.SharpZipLib.dll是另一条路线。它是一个独立的开源压缩库,最早从 Java 的 SharpZipLib 移植过来,后来在 .NET 社区维护了很多年。它把 zip 格式的细节暴露得比较全:ZipInputStream、ZipOutputStream、ZipFile、FastZip这些类覆盖了流式读写、随机访问、加密、分卷等场景。代价是你得手动管理这个 dll,而且它的 API 风格偏底层,参数不设对就容易出问题。
选型上我一般这么判断:如果只是解压自己生成的、无加密的、文件名全英文的 zip,原生库足够,代码短、依赖少。一旦遇到「别人给的包」「带密码」「文件名有中文」「包特别大不想全读进内存」这几类情况,直接上 SharpZipLib,别在原生库上硬扛。
2.2 引入 SharpZipLib 的三种方式
第一种是 NuGet,这也是现在最推荐的做法。在项目里执行:
# 在项目目录下执行,会自动写入 .csproj 的 PackageReference dotnet add package SharpZipLib装完之后不需要手动往 bin 目录拷 dll,还原时会自动拉下来。注意包名是SharpZipLib,不是ICSharpCode.SharpZipLib,后者是旧的手动引用时代的叫法,搜 NuGet 时别搞混。
第二种是手动引用 dll。有些老项目还在用 packages.config 或者干脆裸引用,那就把ICSharpCode.SharpZipLib.dll放到 lib 目录,在 csproj 里加:
<ItemGroup> <Reference Include="ICSharpCode.SharpZipLib"> <HintPath>lib\ICSharpCode.SharpZipLib.dll</HintPath> </Reference> </ItemGroup>这种方式要留意 dll 的版本和项目目标框架是否匹配,.NET Framework 4.x 和 .NET 6/8 用的程序集不通用,拿错了会在运行时抛FileLoadException。
第三种是源码内嵌,把 SharpZipLib 的源码直接拖进项目编译。适合需要改库行为或者目标环境不允许带外部 dll 的场景,但维护成本高,一般不建议。
提示:不管用哪种方式,先确认目标框架。.NET Framework 项目引 .NET Standard 版本的包通常没问题,反过来则可能缺 API。
2.3 一个最小可用的解压示例
先看原生库怎么写,作为对照:
using System.IO; using System.IO.Compression; // 解压到指定目录,overwriteFiles 为 true 时覆盖同名文件 ZipFile.ExtractToDirectory(@"D:\backup\data.zip", @"D:\output", overwriteFiles: true);这行代码能解决大部分简单场景,但它对加密包会直接抛InvalidDataException,对中文文件名在部分环境下会乱码。
再看 SharpZipLib 的等价写法:
using ICSharpCode.SharpZipLib.Zip; // 打开 zip 文件,参数为文件路径 using var zipFile = new ZipFile(@"D:\backup\data.zip"); // 遍历所有条目 foreach (ZipEntry entry in zipFile) { if (!entry.IsFile) continue; // 跳过目录项 // 计算解压后的目标路径 string targetPath = Path.Combine(@"D:\output", entry.Name); // 确保目标目录存在 Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); using var input = zipFile.GetInputStream(entry); using var output = File.Create(targetPath); input.CopyTo(output); }逻辑说明:ZipFile类提供随机访问能力,可以按条目读取;entry.IsFile用来过滤目录项,否则会尝试创建同名文件导致异常;GetInputStream返回的是解压后的流,直接拷贝到文件即可。参数上,ZipFile构造函数还可以传密码,后面会讲。
3. 把解压做扎实:流式处理、编码与密码
3.1 大包别全读进内存:ZipInputStream 流式解压
上面用ZipFile的方式会把中央目录读进来,对普通包没问题,但如果包有几个 G,或者你是在网络流上边下边解,就该换ZipInputStream。它的特点是顺序读取,内存占用稳定。
using ICSharpCode.SharpZipLib.Zip; // 从文件流创建,适合大文件 using var fs = File.OpenRead(@"D:\backup\bigdata.zip"); using var zipStream = new ZipInputStream(fs); // 设置密码,没有密码可省略 zipStream.Password = "your_password"; ZipEntry entry; while ((entry = zipStream.GetNextEntry()) != null) { if (!entry.IsFile) continue; string targetPath = Path.Combine(@"D:\output", entry.Name); Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); using var output = File.Create(targetPath); // 逐块拷贝,避免一次性分配大缓冲 byte[] buffer = new byte[8192]; int read; while ((read = zipStream.Read(buffer, 0, buffer.Length)) > 0) { output.Write(buffer, 0, read); } }逻辑说明:GetNextEntry每次返回一个条目并移动流指针,读完当前条目后自动指向下一个。Password属性必须在读取条目之前设置,否则加密条目会抛ZipException。缓冲区用 8KB 是常见折中,太大浪费内存,太小增加 IO 次数。
参数上有个细节:ZipInputStream不支持随机访问,所以不能像ZipFile那样先看条目列表再决定解哪个,只能顺序处理。如果你的逻辑需要「先筛选再解压」,还是得用ZipFile。
3.2 中文文件名乱码:编码问题的根因和修法
中文乱码是搜「zip 解压缩文件」的人最常撞的墙。根因在于 zip 格式早期没有规定文件名编码,Windows 资源管理器用本地代码页(GBK),而很多库默认用 UTF-8 或系统默认编码去读,两边对不上就花了。
SharpZipLib 里控制编码的是ZipStrings或者条目上的IsUnicodeText标志。常见修法有两种:
using ICSharpCode.SharpZipLib.Zip; // 方式一:全局指定代码页,936 是 GBK ZipStrings.CodePage = 936; // 方式二:在读取条目后判断,手动转码 using var zipFile = new ZipFile(@"D:\backup\data.zip"); foreach (ZipEntry entry in zipFile) { // 如果条目没标记 Unicode,按 GBK 解 string name = entry.IsUnicodeText ? entry.Name : System.Text.Encoding.GetEncoding(936).GetString( System.Text.Encoding.GetEncoding(936).GetBytes(entry.Name)); }逻辑说明:ZipStrings.CodePage是全局开关,设成 936 后所有条目按 GBK 解析,适合确定是 Windows 生成的包。方式二更灵活,但要注意entry.Name本身可能已经被错误解码过,转码不一定能救回来,所以优先在打开包之前设好全局编码。
注意:.NET Core 之后默认不带 GBK 编码,需要额外注册
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance),否则GetEncoding(936)会抛异常。
3.3 带密码的包:ZipCrypto 和 AES 的区别
SharpZipLib 支持两种加密:传统的 ZipCrypto 和 AES。ZipCrypto 兼容性好但强度弱,AES 强度高但需要对方也用支持 AES 的工具生成。解压时都通过Password属性传:
using var zipFile = new ZipFile(@"D:\backup\encrypted.zip"); zipFile.Password = "123456"; // 密码错误会在读取流时抛 ZipException foreach (ZipEntry entry in zipFile) { if (!entry.IsFile) continue; using var input = zipFile.GetInputStream(entry); // ... 后续拷贝逻辑同上 }逻辑说明:密码不是在构造时校验的,而是在GetInputStream或读取时才会验证,所以密码错不会立刻报,而是在读第一个加密条目时抛异常。AES 加密的条目在ZipEntry.AESKeySize上会有值,可以用它判断加密类型,做不同的错误提示。
参数上,如果包是 AES-256 而你的 SharpZipLib 版本太老,可能直接不认,表现为条目读出来是空的或者抛ZipException。这种情况先升级包版本,别急着改代码。
4. 避坑排查:那些让你加班到凌晨的细节
4.1 路径穿越:zip 里的../会写到目录外
现象:解压某个来源不明的包后,发现文件跑到了目标目录的上级甚至系统目录。原因:zip 条目名里可以包含../,如果直接Path.Combine拼接,就会跳出目标目录。解决:解压前规范化路径并校验前缀。
string root = Path.GetFullPath(@"D:\output"); string target = Path.GetFullPath(Path.Combine(root, entry.Name)); // 校验目标路径必须在 root 之下 if (!target.StartsWith(root, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException($"非法条目路径: {entry.Name}"); }4.2 目录项被当成文件:IsFile判断不能省
现象:解压时报「文件已存在」或创建出零字节的同名文件。原因:zip 里目录也是条目,entry.Name以/结尾,直接File.Create会失败或建出怪文件。解决:循环里先if (!entry.IsFile) continue;,目录用Directory.CreateDirectory单独建。
4.3 流没关导致文件被占用
现象:解压完想立刻删除或移动 zip,提示「文件正被另一进程使用」。原因:ZipFile、ZipInputStream、FileStream没释放。解决:全部用using包裹,或者显式Dispose。SharpZipLib 的ZipFile实现了IDisposable,别只关内部流。
4.4 密码错误没有明确异常
现象:密码填错,程序卡住或抛一个看不懂的ZipException。原因:前面说过,密码在读取时才校验,异常信息不直观。解决:捕获ZipException后统一提示「密码错误或包已损坏」,并记录条目名方便定位。
4.5 大文件解压内存暴涨
现象:解压几个 G 的包时内存飙到几个 G。原因:用了ZipFile加一次性ReadAllBytes。解决:换ZipInputStream流式处理,缓冲区控制在 8KB 到 64KB 之间,别用MemoryStream缓存整个条目。
5. 进阶技巧:批量解压的并发控制与完整性校验
批量解压最容易犯的错是无脑开多线程。zip 解压本身是 IO 密集型,磁盘就一块,开太多线程反而互相抢 IO,速度不升反降。我一般用Parallel.ForEach配一个受控的并发度,比如MaxDegreeOfParallelism = 4,具体值看磁盘类型,机械盘 2 到 4,固态可以到 8。
var zipFiles = Directory.GetFiles(@"D:\backup", "*.zip"); var options = new ParallelOptions { MaxDegreeOfParallelism = 4 }; Parallel.ForEach(zipFiles, options, zipPath => { try { string outDir = Path.Combine(@"D:\output", Path.GetFileNameWithoutExtension(zipPath)); Directory.CreateDirectory(outDir); using var zipFile = new ZipFile(zipPath); foreach (ZipEntry entry in zipFile) { if (!entry.IsFile) continue; string target = Path.GetFullPath(Path.Combine(outDir, entry.Name)); // 路径穿越校验 if (!target.StartsWith(Path.GetFullPath(outDir), StringComparison.OrdinalIgnoreCase)) continue; Directory.CreateDirectory(Path.GetDirectoryName(target)!); using var input = zipFile.GetInputStream(entry); using var output = File.Create(target); input.CopyTo(output); } } catch (Exception ex) { // 单个包失败不影响其他包,记录日志继续 Console.WriteLine($"解压失败 {zipPath}: {ex.Message}"); } });逻辑说明:Parallel.ForEach的MaxDegreeOfParallelism控制同时处理的包数量,不是线程数上限,实际线程由线程池调度。每个包独立 try-catch,避免一个坏包中断整批。路径校验放在循环内,防止个别条目越界。
完整性校验方面,SharpZipLib 的ZipFile提供TestArchive方法,可以在解压前先跑一遍:
using var zipFile = new ZipFile(zipPath); // true 表示所有条目 CRC 校验通过 bool ok = zipFile.TestArchive(true, TestStrategy.FindFirstError, null); if (!ok) { Console.WriteLine($"包损坏: {zipPath}"); return; }参数说明:第一个参数testData为 true 时会实际读取数据做 CRC 校验,比只检查目录结构更可靠但更慢;TestStrategy.FindFirstError表示遇到第一个错误就停,适合快速筛查;第三个参数是错误回调,可以传 null 或自定义记录。
最后说个习惯。从那以后我每次写解压逻辑,都会强制走一遍「路径校验 → 编码设置 → 流式读取 → 异常兜底」这四步,哪怕需求再简单也不省。因为 zip 这东西看着简单,真出问题往往不是代码写错,而是包本身带了你想不到的边界情况。希望帮到你。
本文还有配套的精品资源,点击获取