.NET 压缩/解压文件
本文为大家介绍下.NET解压/压缩zip文件。虽然解压缩不是啥核心技术,但压缩性能以及进度处理还是需要关注下,针对使用较多的zip开源组件验证,给大家提供个技术选型参考
之前在《.NET WebSocket高并发通信阻塞问题 - 唐宋元明清2188 - 博客园 (cnblogs.com)》讲过,团队遇到Zip文件解压进度频率过高问题,也在这里顺带讲下解决方法
目前了解到的常用技术方案有System.IO.Compression、SharpZipLib以及DotNetZip,下面我们分别介绍下使用以及性能
System.IO.Compression
如果你需要处理简单的ZIP压缩和解压任务,且不需要高级特性,建议使用System.IO.Compression。作为.NET标准库的一部分,不需要额外安装第三方库,而且会随着.NET平台的更新而更新
看下代码实现:
1 /// <summary> 2 /// 解压Zip文件 3 /// </summary> 4 /// <param name="filePath">zip文件路径</param> 5 /// <param name="outputFolder">解压目录</param> 6 /// <returns></returns> 7 public static void Decompress(string filePath, string outputFolder) 8 { 9 ZipFile.ExtractToDirectory(filePath, outputFolder); 10 } 11 12 /// <summary> 13 /// 压缩成Zip文件 14 /// </summary> 15 /// <param name="sourceFolder">文件目录</param> 16 /// <param name="zipFile">zip文件路径</param> 17 /// <param name="includeFolder">是否包含文件父目录(即sourceFolder本身)</param> 18 /// <returns></returns> 19 public static void Compress(string sourceFolder, string zipFile, bool includeFolder = true) 20 { 21 ZipFile.CreateFromDirectory(sourceFolder, zipFile, CompressionLevel.Fastest, includeFolder); 22 }
优点很明显,API简洁易懂,适用于简单的文件压缩和解压操作。当然提供的功能比较基础,缺乏一些高级特性,比如分卷压缩和加密,也提供不了操作详细进度
我们来测试下解压缩性能,找个zip文件,“智微工厂生产需要的固件及安装包.zip”文件大小847M,里面是如下结构有文件以及文件夹:
解压耗时:8484ms。再将解压后的文件夹压缩,耗时:28672ms。性能整体上还是不错的,特别是解压很优秀所以呢,比较简单的业务场景可以直接用这个方案。大家可以将这个方案放在公司通用基础技术组件里
SharpZipLib
支持多种压缩格式(如ZIP、TAR、GZIP、BZIP2等),并提供了高级功能如加密、分卷压缩等。icsharpcode/SharpZipLib: #ziplib is a Zip, GZip, Tar and BZip2 library written entirely in C# for the .NET platform. (github.com)
API设计可用性高,满足更多复杂定制化需求。社区里好多小伙伴在使用,开发历史久远、组件稳定性较高
引用下Nuget包SharpZipLib后,解压zip文件
获取压缩包压缩后的文件的大小,这里Size是压缩前大小,还有一个属性CompressedSize压缩后大小:
1 public static long GetZipFileTotalSize(string zipPath) 2 { 3 long totalSize = 0; 4 using FileStream fileStream = File.OpenRead(zipPath); 5 using ZipInputStream zipStream = new ZipInputStream(fileStream); 6 while (zipStream.GetNextEntry() is { } zipEntry) 7 { 8 totalSize += zipEntry.Size; 9 } 10 11 return totalSize; 12 }
解压Zip文件:
1 /// <summary> 2 /// 解压Zip文件 3 /// </summary> 4 /// <param name="zipFile">zip文件路径</param> 5 /// <param name="outputFolder">解压目录</param> 6 /// <param name="cancellationToken">取消操作</param> 7 /// <param name="progressChanged">解压进度回调</param> 8 /// <returns></returns> 9 public static async Task UnZipAsync(string zipFile, string outputFolder, 10 CancellationToken cancellationToken = default, Action<ZipProgress> progressChanged = null) 11 { 12 if (!File.Exists(zipFile)) 13 { 14 throw new InvalidOperationException($"file not exist,{zipFile}"); 15 } 16 var decompressLength = GetZipFileTotalSize(zipFile); 17 using FileStream fileStream = File.OpenRead(zipFile); 18 await Task.Run(() => 19 { 20 using ZipInputStream zipStream = new ZipInputStream(fileStream); 21 long completedSize = 0; 22 while (zipStream.GetNextEntry() is { } zipEntry) 23 { 24 if (cancellationToken != default && cancellationToken.IsCancellationRequested) 25 { 26 cancellationToken.ThrowIfCancellationRequested(); 27 } 28 29 if (zipEntry.IsDirectory) 30 { 31 string folder = Path.Combine(outputFolder, zipEntry.Name); 32 EnsureFolder(folder); 33 } 34 else if (zipEntry.IsFile) 35 { 36 var operatingSize = completedSize; 37 var zipEntryName = zipEntry.Name; 38 string fullEntryPath = Path.Combine(outputFolder, zipEntryName); 39 string dirPath = Path.GetDirectoryName(fullEntryPath); 40 EnsureFolder(dirPath); 41 //解压后的数据 42 long singleFileSize = WriteUnzipDataToFile(zipStream, fullEntryPath, partialFileSize => 43 { 44 if (progressChanged == null) 45 { 46 return; 47 } 48 long currentSize = operatingSize + partialFileSize; 49 progressChanged.Invoke(new ZipProgress(currentSize, decompressLength, zipEntryName)); 50 }); 51 completedSize += singleFileSize; 52 } 53 } 54 }, cancellationToken); 55 }
解压进度能反馈详细的文件写入进度值。另外,这里有个文件夹判断处理,也是支持空文件夹的
Zip压缩,获取所有的文件夹/子文件夹、所有的文件,添加到ZipFile里保存:
1 /// <summary> 2 /// 压缩文件 3 /// </summary> 4 /// <param name="toZipDirectory">待压缩的文件夹</param> 5 /// <param name="destZipPath">Zip文件的保存路径</param> 6 /// <returns></returns> 7 public static bool Zip(string toZipDirectory, string destZipPath) 8 { 9 if (string.IsNullOrEmpty(destZipPath)) 10 { 11 throw new ArgumentNullException(nameof(destZipPath)); 12 } 13 if (!destZipPath.ToUpper().EndsWith(".ZIP")) 14 { 15 throw new ArgumentException("保存路径不是ZIP后缀", nameof(destZipPath)); 16 } 17 if (!Directory.Exists(toZipDirectory)) 18 { 19 throw new ArgumentException("待压缩的文件夹不存在", nameof(toZipDirectory)); 20 } 21 22 var dirs = Directory.GetDirectories(toZipDirectory, "*", SearchOption.AllDirectories) 23 .Select(dir => PathUtils.GetRelativePath(toZipDirectory, dir)); 24 var files = Directory.GetFiles(toZipDirectory, "*", SearchOption.AllDirectories).ToArray(); 25 var destFiles = files.Select(file => PathUtils.GetRelativePath(toZipDirectory, file)).ToArray(); 26 if (File.Exists(destZipPath)) 27 { 28 File.Delete(destZipPath); 29 } 30 using (ZipFile zipFile = ZipFile.Create(destZipPath)) 31 { 32 zipFile.BeginUpdate(); 33 foreach (var dir in dirs) 34 { 35 zipFile.AddDirectory(dir); 36 } 37 for (int i = 0; i < files.Length; i++) 38 { 39 zipFile.Add(files[i], destFiles[i]); 40 } 41 zipFile.CommitUpdate(); 42 } 43 return true; 44 }
值得一提的是,如有需要指定Zip压缩文件内的文件名以及文件路径,可以在文件时输入对应的压缩后路径定义,注意是指压缩包内的相对路径:
1 /// <summary>指定的文件压缩到对应的压缩文件中</summary> 2 /// <param name="files">待压缩的文件路径列表(绝对路径)</param> 3 /// <param name="destFiles">文件路径对应的压缩后路径列表,即压缩后压缩包内的文件路径</param> 4 /// <param name="destZipPath">Zip文件的保存路径</param> 5 public static bool Zip(List<string> files, List<string> destFiles, string destZipPath) 6 { 7 if (files.Count != destFiles.Count) 8 { 9 throw new ArgumentException($"{nameof(files)}与{nameof(destFiles)}文件列表数量不一致"); 10 } 11 if (string.IsNullOrEmpty(destZipPath)) 12 throw new ArgumentNullException(nameof(destZipPath)); 13 using (ZipFile zipFile = ZipFile.Create(destZipPath)) 14 { 15 zipFile.BeginUpdate(); 16 for (int i = 0; i < files.Count; i++) 17 { 18 zipFile.Add(files[i], destFiles[i]); 19 } 20 zipFile.CommitUpdate(); 21 } 22 return true; 23 }
SharpZipLib虽然功能丰富,但大家看上面的demo代码,接口搞的有点复杂、学习曲线较高
同样我们按上面测试操作,解压缩同一zip文件,解压耗时20719ms,压缩耗时102109ms。。。
DotNetZip
再看看DotNetZip,这个相对SharpZipLib,API设计的更友好、容易上手。官网是haf/DotNetZip.Semverd(github.com),它停止维护了。。。作者推荐大家去使用System.IO.Compression!好吧先忽略这个,尽管已不再积极维护,但稳定性、性能真的好,下面给大家列下使用demo和性能测试
Zip文件解压:
1 /// <summary> 2 /// 解压Zip文件 3 /// </summary> 4 /// <param name="zipFile">zip文件路径</param> 5 /// <param name="outputFolder">解压目录</param> 6 /// <param name="password">密码</param> 7 /// <param name="progressChanged">解压进度回调</param> 8 /// <returns></returns> 9 public static void UnZip(string zipFile, string outputFolder, string password, Action<ZipProgress> progressChanged) 10 { 11 if (!File.Exists(zipFile)) throw new InvalidOperationException($"file not exist,{zipFile}"); 12 //获取文件解压后的大小 13 var totalZipSize = GetZipFileSize(zipFile); 14 long completedSize = 0L; 15 using (var zip = ZipFile.Read(zipFile)) 16 { 17 zip.Password = password; 18 zip.ExtractProgress += (s, e) => 19 { 20 if (e.EventType == ZipProgressEventType.Extracting_EntryBytesWritten) 21 { 22 var fileName = e.CurrentEntry.FileName; 23 if (e.BytesTransferred < e.TotalBytesToTransfer) 24 { 25 //单个文件解压中的进度 26 var operatingSize = completedSize + e.BytesTransferred; 27 progressChanged?.Invoke(new ZipProgress(operatingSize, totalZipSize, fileName)); 28 } 29 else 30 { 31 //单个文件解压完全的进度 32 completedSize += e.TotalBytesToTransfer; 33 progressChanged?.Invoke(new ZipProgress(completedSize, totalZipSize, fileName)); 34 } 35 } 36 }; 37 zip.ExtractAll(outputFolder); 38 } 39 }
这里获取压缩后文件大小,与上面SharpZipLib的zipEntry.Size对应,取的是zipEntry.UncompressedSize
非常人性的提供了ExtractProgress事件进度,我们取的是Extracting_EntryBytesWritten类型,可以拿到细节进度。具体进度的处理看上方代码
因为反馈的是详细字节写入进度,所以间隔很短。。。1ms都能给你爆几次进度,尤其是大文件:
所以需要限制下回调Action触发,可以加个计时器限制单个文件的进度回调,如100ms内最多触发一次,下面是优化后的代码:
1 /// <summary> 2 /// 解压Zip文件 3 /// </summary> 4 /// <param name="zipFile">zip文件路径</param> 5 /// <param name="outputFolder">解压目录</param> 6 /// <param name="password">密码</param> 7 /// <param name="progressChanged">解压进度回调</param> 8 /// <returns></returns> 9 public static void UnZip(string zipFile, string outputFolder, string password, 10 Action<ZipProgress> progressChanged) 11 { 12 if (!File.Exists(zipFile)) throw new InvalidOperationException($"file not exist,{zipFile}"); 13 //获取文件解压后的大小 14 var totalZipSize = GetZipFileSize(zipFile); 15 long completedSize = 0L; 16 using (var zip = ZipFile.Read(zipFile)) 17 { 18 zip.Password = password; 19 var lastProgressTick = Environment.TickCount; 20 zip.ExtractProgress += (s, e) => 21 { 22 if (e.EventType == ZipProgressEventType.Extracting_EntryBytesWritten) 23 { 24 var fileName = e.CurrentEntry.FileName; 25 if (e.BytesTransferred < e.TotalBytesToTransfer) 26 { 27 // 单个文件解压变化,限制间隔时间触发解压事件 28 if (Environment.TickCount - lastProgressTick < ProgressEventTick) 29 { 30 return; 31 } 32 lastProgressTick = Environment.TickCount; 33 //单个文件解压中的进度 34 var operatingSize = completedSize + e.BytesTransferred; 35 progressChanged?.Invoke(new ZipProgress(operatingSize, totalZipSize, fileName)); 36 } 37 else 38 { 39 //重置计时器 40 lastProgressTick = Environment.TickCount; 41 //单个文件解压完全的进度 42 completedSize += e.TotalBytesToTransfer; 43 progressChanged?.Invoke(new ZipProgress(completedSize, totalZipSize, fileName)); 44 } 45 } 46 }; 47 zip.ExtractAll(outputFolder); 48 } 49 }
解压进度就正常了很多,限制间隔只会优化单个文件解压过程中的进度,单个文件解压完成时最后还是有进度回调的。
再看看Zip压缩:
1 public static void Zip(string sourceFolder, string destZipFile, string password, 2 Action<ZipProgress> zipProgressAction) 3 { 4 if (string.IsNullOrEmpty(destZipFile)) throw new ArgumentNullException(nameof(destZipFile)); 5 if (!destZipFile.ToUpper().EndsWith(".ZIP")) throw new ArgumentException("保存路径不是Zip文件", destZipFile); 6 if (File.Exists(destZipFile)) File.Delete(destZipFile); 7 8 using (var zipFile = new ZipFile()) 9 { 10 // 设置压缩进度事件处理程序 11 zipFile.SaveProgress += (sender, e) => 12 { 13 if (e.EventType == ZipProgressEventType.Saving_AfterWriteEntry) 14 zipProgressAction?.Invoke(new ZipProgress(e.EntriesSaved, e.EntriesTotal, e.CurrentEntry.FileName)); 15 }; 16 zipFile.AddDirectory(sourceFolder); 17 zipFile.Password = password; 18 zipFile.Save(destZipFile); 19 } 20 }
如果不考虑加密、压缩进度,DotNetZip压缩zip文件只需要几行代码,所以是相当的易学易用、入手快
还是同一个847M的zip文件,测试下解压缩性能,解压11907ms,压缩耗时16282ms,用数据说话性能强不强
用表格把这三个方案的对比列下:
所以如果你需要处理简单的ZIP压缩和解压任务,且不需要高级特性,建议使用System.IO.Compression
需要考虑解压缩性能比如公司的大文件OTA功能,需要减少业务的处理时间,推荐使用DotNetZip。DotNetZip也能提供高级特性,进度显示等。至于停止维护的状况可以忽然,有BUG大家可以在公司内或者github维护下这个组件代码