翼度科技»论坛 编程开发 .net 查看内容

.NET 压缩/解压文件

8

主题

8

帖子

24

积分

新手上路

Rank: 1

积分
24
本文为大家介绍下.NET解压/压缩zip文件。虽然解压缩不是啥核心技术,但压缩性能以及进度处理还是需要关注下,针对使用较多的zip开源组件验证,给大家提供个技术选型参考
之前在《.NET WebSocket高并发通信阻塞问题 - 唐宋元明清2188 - 博客园 (cnblogs.com)》讲过,团队遇到Zip文件解压进度频率过高问题,也在这里顺带讲下解决方法
目前了解到的常用技术方案有System.IO.Compression、SharpZipLib以及DotNetZip,下面我们分别介绍下使用以及性能
System.IO.Compression

如果你需要处理简单的ZIP压缩和解压任务,且不需要高级特性,建议使用System.IO.Compression。作为.NET标准库的一部分,不需要额外安装第三方库,而且会随着.NET平台的更新而更新
看下代码实现:
  1. 1     /// <summary>
  2. 2     /// 解压Zip文件
  3. 3     /// </summary>
  4. 4     /// <param name="filePath">zip文件路径</param>
  5. 5     /// <param name="outputFolder">解压目录</param>
  6. 6     /// <returns></returns>
  7. 7     public static void Decompress(string filePath, string outputFolder)
  8. 8     {
  9. 9         ZipFile.ExtractToDirectory(filePath, outputFolder);
  10. 10     }
  11. 11
  12. 12     /// <summary>
  13. 13     /// 压缩成Zip文件
  14. 14     /// </summary>
  15. 15     /// <param name="sourceFolder">文件目录</param>
  16. 16     /// <param name="zipFile">zip文件路径</param>
  17. 17     /// <param name="includeFolder">是否包含文件父目录(即sourceFolder本身)</param>
  18. 18     /// <returns></returns>
  19. 19     public static void Compress(string sourceFolder, string zipFile, bool includeFolder = true)
  20. 20     {
  21. 21         ZipFile.CreateFromDirectory(sourceFolder, zipFile, CompressionLevel.Fastest, includeFolder);
  22. 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. 1         public static long GetZipFileTotalSize(string zipPath)
  2. 2         {
  3. 3             long totalSize = 0;
  4. 4             using FileStream fileStream = File.OpenRead(zipPath);
  5. 5             using ZipInputStream zipStream = new ZipInputStream(fileStream);
  6. 6             while (zipStream.GetNextEntry() is { } zipEntry)
  7. 7             {
  8. 8                 totalSize += zipEntry.Size;
  9. 9             }
  10. 10
  11. 11             return totalSize;
  12. 12         }
复制代码
解压Zip文件:
  1. 1       /// <summary>
  2. 2       /// 解压Zip文件
  3. 3       /// </summary>
  4. 4       /// <param name="zipFile">zip文件路径</param>
  5. 5       /// <param name="outputFolder">解压目录</param>
  6. 6       /// <param name="cancellationToken">取消操作</param>
  7. 7       /// <param name="progressChanged">解压进度回调</param>
  8. 8       /// <returns></returns>
  9. 9       public static async Task UnZipAsync(string zipFile, string outputFolder,
  10. 10           CancellationToken cancellationToken = default, Action<ZipProgress> progressChanged = null)
  11. 11       {
  12. 12           if (!File.Exists(zipFile))
  13. 13           {
  14. 14               throw new InvalidOperationException($"file not exist,{zipFile}");
  15. 15           }
  16. 16           var decompressLength = GetZipFileTotalSize(zipFile);
  17. 17           using FileStream fileStream = File.OpenRead(zipFile);
  18. 18           await Task.Run(() =>
  19. 19           {
  20. 20               using ZipInputStream zipStream = new ZipInputStream(fileStream);
  21. 21               long completedSize = 0;
  22. 22               while (zipStream.GetNextEntry() is { } zipEntry)
  23. 23               {
  24. 24                   if (cancellationToken != default && cancellationToken.IsCancellationRequested)
  25. 25                   {
  26. 26                       cancellationToken.ThrowIfCancellationRequested();
  27. 27                   }
  28. 28
  29. 29                   if (zipEntry.IsDirectory)
  30. 30                   {
  31. 31                       string folder = Path.Combine(outputFolder, zipEntry.Name);
  32. 32                       EnsureFolder(folder);
  33. 33                   }
  34. 34                   else if (zipEntry.IsFile)
  35. 35                   {
  36. 36                       var operatingSize = completedSize;
  37. 37                       var zipEntryName = zipEntry.Name;
  38. 38                       string fullEntryPath = Path.Combine(outputFolder, zipEntryName);
  39. 39                       string dirPath = Path.GetDirectoryName(fullEntryPath);
  40. 40                       EnsureFolder(dirPath);
  41. 41                       //解压后的数据
  42. 42                       long singleFileSize = WriteUnzipDataToFile(zipStream, fullEntryPath, partialFileSize =>
  43. 43                       {
  44. 44                           if (progressChanged == null)
  45. 45                           {
  46. 46                               return;
  47. 47                           }
  48. 48                           long currentSize = operatingSize + partialFileSize;
  49. 49                           progressChanged.Invoke(new ZipProgress(currentSize, decompressLength, zipEntryName));
  50. 50                       });
  51. 51                       completedSize += singleFileSize;
  52. 52                   }
  53. 53               }
  54. 54           }, cancellationToken);
  55. 55       }
复制代码
解压进度能反馈详细的文件写入进度值。另外,这里有个文件夹判断处理,也是支持空文件夹的
Zip压缩,获取所有的文件夹/子文件夹、所有的文件,添加到ZipFile里保存:
  1. 1       /// <summary>
  2. 2       /// 压缩文件
  3. 3       /// </summary>
  4. 4       /// <param name="toZipDirectory">待压缩的文件夹</param>
  5. 5       /// <param name="destZipPath">Zip文件的保存路径</param>
  6. 6       /// <returns></returns>
  7. 7       public static bool Zip(string toZipDirectory, string destZipPath)
  8. 8       {
  9. 9           if (string.IsNullOrEmpty(destZipPath))
  10. 10           {
  11. 11               throw new ArgumentNullException(nameof(destZipPath));
  12. 12           }
  13. 13           if (!destZipPath.ToUpper().EndsWith(".ZIP"))
  14. 14           {
  15. 15               throw new ArgumentException("保存路径不是ZIP后缀", nameof(destZipPath));
  16. 16           }
  17. 17           if (!Directory.Exists(toZipDirectory))
  18. 18           {
  19. 19               throw new ArgumentException("待压缩的文件夹不存在", nameof(toZipDirectory));
  20. 20           }
  21. 21
  22. 22           var dirs = Directory.GetDirectories(toZipDirectory, "*", SearchOption.AllDirectories)
  23. 23               .Select(dir => PathUtils.GetRelativePath(toZipDirectory, dir));
  24. 24           var files = Directory.GetFiles(toZipDirectory, "*", SearchOption.AllDirectories).ToArray();
  25. 25           var destFiles = files.Select(file => PathUtils.GetRelativePath(toZipDirectory, file)).ToArray();
  26. 26           if (File.Exists(destZipPath))
  27. 27           {
  28. 28               File.Delete(destZipPath);
  29. 29           }
  30. 30           using (ZipFile zipFile = ZipFile.Create(destZipPath))
  31. 31           {
  32. 32               zipFile.BeginUpdate();
  33. 33               foreach (var dir in dirs)
  34. 34               {
  35. 35                   zipFile.AddDirectory(dir);
  36. 36               }
  37. 37               for (int i = 0; i < files.Length; i++)
  38. 38               {
  39. 39                   zipFile.Add(files[i], destFiles[i]);
  40. 40               }
  41. 41               zipFile.CommitUpdate();
  42. 42           }
  43. 43           return true;
  44. 44       }
复制代码
值得一提的是,如有需要指定Zip压缩文件内的文件名以及文件路径,可以在文件时输入对应的压缩后路径定义,注意是指压缩包内的相对路径:
  1. 1       /// <summary>指定的文件压缩到对应的压缩文件中</summary>
  2. 2       /// <param name="files">待压缩的文件路径列表(绝对路径)</param>
  3. 3       /// <param name="destFiles">文件路径对应的压缩后路径列表,即压缩后压缩包内的文件路径</param>
  4. 4       /// <param name="destZipPath">Zip文件的保存路径</param>
  5. 5       public static bool Zip(List<string> files, List<string> destFiles, string destZipPath)
  6. 6       {
  7. 7           if (files.Count != destFiles.Count)
  8. 8           {
  9. 9               throw new ArgumentException($"{nameof(files)}与{nameof(destFiles)}文件列表数量不一致");
  10. 10           }
  11. 11           if (string.IsNullOrEmpty(destZipPath))
  12. 12               throw new ArgumentNullException(nameof(destZipPath));
  13. 13           using (ZipFile zipFile = ZipFile.Create(destZipPath))
  14. 14           {
  15. 15               zipFile.BeginUpdate();
  16. 16               for (int i = 0; i < files.Count; i++)
  17. 17               {
  18. 18                   zipFile.Add(files[i], destFiles[i]);
  19. 19               }
  20. 20               zipFile.CommitUpdate();
  21. 21           }
  22. 22           return true;
  23. 23       }
复制代码
SharpZipLib虽然功能丰富,但大家看上面的demo代码,接口搞的有点复杂、学习曲线较高
同样我们按上面测试操作,解压缩同一zip文件,解压耗时20719ms,压缩耗时102109ms。。。
DotNetZip

再看看DotNetZip,这个相对SharpZipLib,API设计的更友好、容易上手。官网是haf/DotNetZip.Semverd(github.com),它停止维护了。。。作者推荐大家去使用System.IO.Compression!好吧先忽略这个,尽管已不再积极维护,但稳定性、性能真的好,下面给大家列下使用demo和性能测试
 Zip文件解压:
  1. 1     /// <summary>
  2. 2     /// 解压Zip文件
  3. 3     /// </summary>
  4. 4     /// <param name="zipFile">zip文件路径</param>
  5. 5     /// <param name="outputFolder">解压目录</param>
  6. 6     /// <param name="password">密码</param>
  7. 7     /// <param name="progressChanged">解压进度回调</param>
  8. 8     /// <returns></returns>
  9. 9     public static void UnZip(string zipFile, string outputFolder, string password, Action<ZipProgress> progressChanged)
  10. 10     {
  11. 11         if (!File.Exists(zipFile)) throw new InvalidOperationException($"file not exist,{zipFile}");
  12. 12         //获取文件解压后的大小
  13. 13         var totalZipSize = GetZipFileSize(zipFile);
  14. 14         long completedSize = 0L;
  15. 15         using (var zip = ZipFile.Read(zipFile))
  16. 16         {
  17. 17             zip.Password = password;
  18. 18             zip.ExtractProgress += (s, e) =>
  19. 19             {
  20. 20                 if (e.EventType == ZipProgressEventType.Extracting_EntryBytesWritten)
  21. 21                 {
  22. 22                     var fileName = e.CurrentEntry.FileName;
  23. 23                     if (e.BytesTransferred < e.TotalBytesToTransfer)
  24. 24                     {
  25. 25                         //单个文件解压中的进度
  26. 26                         var operatingSize = completedSize + e.BytesTransferred;
  27. 27                         progressChanged?.Invoke(new ZipProgress(operatingSize, totalZipSize, fileName));
  28. 28                     }
  29. 29                     else
  30. 30                     {
  31. 31                         //单个文件解压完全的进度
  32. 32                         completedSize += e.TotalBytesToTransfer;
  33. 33                         progressChanged?.Invoke(new ZipProgress(completedSize, totalZipSize, fileName));
  34. 34                     }
  35. 35                 }
  36. 36             };
  37. 37             zip.ExtractAll(outputFolder);
  38. 38         }
  39. 39     }
复制代码
这里获取压缩后文件大小,与上面SharpZipLib的zipEntry.Size对应,取的是zipEntry.UncompressedSize
非常人性的提供了ExtractProgress事件进度,我们取的是Extracting_EntryBytesWritten类型,可以拿到细节进度。具体进度的处理看上方代码
因为反馈的是详细字节写入进度,所以间隔很短。。。1ms都能给你爆几次进度,尤其是大文件:

所以需要限制下回调Action触发,可以加个计时器限制单个文件的进度回调,如100ms内最多触发一次,下面是优化后的代码:
  1. 1     /// <summary>
  2. 2     /// 解压Zip文件
  3. 3     /// </summary>
  4. 4     /// <param name="zipFile">zip文件路径</param>
  5. 5     /// <param name="outputFolder">解压目录</param>
  6. 6     /// <param name="password">密码</param>
  7. 7     /// <param name="progressChanged">解压进度回调</param>
  8. 8     /// <returns></returns>
  9. 9     public static void UnZip(string zipFile, string outputFolder, string password,
  10. 10         Action<ZipProgress> progressChanged)
  11. 11     {
  12. 12         if (!File.Exists(zipFile)) throw new InvalidOperationException($"file not exist,{zipFile}");
  13. 13         //获取文件解压后的大小
  14. 14         var totalZipSize = GetZipFileSize(zipFile);
  15. 15         long completedSize = 0L;
  16. 16         using (var zip = ZipFile.Read(zipFile))
  17. 17         {
  18. 18             zip.Password = password;
  19. 19             var lastProgressTick = Environment.TickCount;
  20. 20             zip.ExtractProgress += (s, e) =>
  21. 21             {
  22. 22                 if (e.EventType == ZipProgressEventType.Extracting_EntryBytesWritten)
  23. 23                 {
  24. 24                     var fileName = e.CurrentEntry.FileName;
  25. 25                     if (e.BytesTransferred < e.TotalBytesToTransfer)
  26. 26                     {
  27. 27                         // 单个文件解压变化,限制间隔时间触发解压事件
  28. 28                         if (Environment.TickCount - lastProgressTick < ProgressEventTick)
  29. 29                         {
  30. 30                             return;
  31. 31                         }
  32. 32                         lastProgressTick = Environment.TickCount;
  33. 33                         //单个文件解压中的进度
  34. 34                         var operatingSize = completedSize + e.BytesTransferred;
  35. 35                         progressChanged?.Invoke(new ZipProgress(operatingSize, totalZipSize, fileName));
  36. 36                     }
  37. 37                     else
  38. 38                     {
  39. 39                         //重置计时器
  40. 40                         lastProgressTick = Environment.TickCount;
  41. 41                         //单个文件解压完全的进度
  42. 42                         completedSize += e.TotalBytesToTransfer;
  43. 43                         progressChanged?.Invoke(new ZipProgress(completedSize, totalZipSize, fileName));
  44. 44                     }
  45. 45                 }
  46. 46             };
  47. 47             zip.ExtractAll(outputFolder);
  48. 48         }
  49. 49     }
复制代码

解压进度就正常了很多,限制间隔只会优化单个文件解压过程中的进度,单个文件解压完成时最后还是有进度回调的。
再看看Zip压缩:
  1. 1     public static void Zip(string sourceFolder, string destZipFile, string password,
  2. 2         Action<ZipProgress> zipProgressAction)
  3. 3     {
  4. 4         if (string.IsNullOrEmpty(destZipFile)) throw new ArgumentNullException(nameof(destZipFile));
  5. 5         if (!destZipFile.ToUpper().EndsWith(".ZIP")) throw new ArgumentException("保存路径不是Zip文件", destZipFile);
  6. 6         if (File.Exists(destZipFile)) File.Delete(destZipFile);
  7. 7
  8. 8         using (var zipFile = new ZipFile())
  9. 9         {
  10. 10             // 设置压缩进度事件处理程序
  11. 11             zipFile.SaveProgress += (sender, e) =>
  12. 12             {
  13. 13                 if (e.EventType == ZipProgressEventType.Saving_AfterWriteEntry)
  14. 14                     zipProgressAction?.Invoke(new ZipProgress(e.EntriesSaved, e.EntriesTotal, e.CurrentEntry.FileName));
  15. 15             };
  16. 16             zipFile.AddDirectory(sourceFolder);
  17. 17             zipFile.Password = password;
  18. 18             zipFile.Save(destZipFile);
  19. 19         }
  20. 20     }
复制代码
如果不考虑加密、压缩进度,DotNetZip压缩zip文件只需要几行代码,所以是相当的易学易用、入手快
还是同一个847M的zip文件,测试下解压缩性能,解压11907ms,压缩耗时16282ms,用数据说话性能强不强
用表格把这三个方案的对比列下:

所以如果你需要处理简单的ZIP压缩和解压任务,且不需要高级特性,建议使用System.IO.Compression
需要考虑解压缩性能比如公司的大文件OTA功能,需要减少业务的处理时间,推荐使用DotNetZip。DotNetZip也能提供高级特性,进度显示等。至于停止维护的状况可以忽然,有BUG大家可以在公司内或者github维护下这个组件代码
 
出处:http://www.cnblogs.com/kybs0/本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。
来源:https://www.cnblogs.com/kybs0/p/18398891
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具