.NET 压缩/解压文件

科技   2024-10-16 07:50   北京  

 .NET 压缩/解压文件

本文经原作者授权以原创方式二次分享,欢迎转载、分享。

原文作者:唐宋元明清

原文地址:https://www.cnblogs.com/kybs0/p/18398891

本文为大家介绍下.NET解压/压缩zip文件。虽然解压缩不是啥核心技术,但压缩性能以及进度处理还是需要关注下,针对使用较多的zip开源组件验证,给大家提供个技术选型参考

之前在《.NET WebSocket高并发通信阻塞问题 - 唐宋元明清2188 - 博客园 (cnblogs.com)》[1]讲过,团队遇到Zip文件解压进度频率过高问题,也在这里顺带讲下解决方法

目前了解到的常用技术方案有System.IO.CompressionSharpZipLib以及DotNetZip,下面我们分别介绍下使用以及性能

System.IO.Compression

如果你需要处理简单的ZIP压缩和解压任务,且不需要高级特性,建议使用System.IO.Compression。作为.NET标准库的一部分,不需要额外安装第三方库,而且会随着.NET平台的更新而更新

看下代码实现:

/// <summary>
    /// 解压Zip文件
    /// </summary>
    /// <param name="filePath">zip文件路径</param>
    /// <param name="outputFolder">解压目录</param>
    /// <returns></returns>
    public static void Decompress(string filePath, string outputFolder)
    {
        ZipFile.ExtractToDirectory(filePath, outputFolder);
    }

    /// <summary>
    /// 压缩成Zip文件
    /// </summary>
    /// <param name="sourceFolder">文件目录</param>
    /// <param name="zipFile">zip文件路径</param>
    /// <param name="includeFolder">是否包含文件父目录(即sourceFolder本身)</param>
    /// <returns></returns>
    public static void Compress(string sourceFolder, string zipFile, bool includeFolder = true)
    {
        ZipFile.CreateFromDirectory(sourceFolder, zipFile, CompressionLevel.Fastest, includeFolder);
    }

优点很明显,API简洁易懂,适用于简单的文件压缩和解压操作。当然提供的功能比较基础,缺乏一些高级特性,比如分卷压缩和加密,也提供不了操作详细进度

我们来测试下解压缩性能,找个zip文件,“智微工厂生产需要的固件及安装包.zip”文件大小847M,里面是如下结构有文件以及文件夹:

解压耗时:8484ms。再将解压后的文件夹压缩,耗时:28672ms。性能整体上还是不错的,特别是解压很优秀 所以呢,比较简单的业务场景可以直接用这个方案。大家可以将这个方案放在公司通用基础技术组件里

SharpZipLib

支持多种压缩格式(如ZIPTARGZIPBZIP2等),并提供了高级功能如加密、分卷压缩等。icsharpcode/SharpZipLib: #ziplib is a Zip, GZip, Tar and BZip2 library written entirely in C# for the .NET platform. (github.com)[2]

API设计可用性高,满足更多复杂定制化需求。社区里好多小伙伴在使用,开发历史久远、组件稳定性较高

引用下NugetSharpZipLib后,解压zip文件

获取压缩包压缩后的文件的大小,这里Size是压缩前大小,还有一个属性CompressedSize压缩后大小:

public static long GetZipFileTotalSize(string zipPath)
        {
            long totalSize = 0;
            using FileStream fileStream = File.OpenRead(zipPath);
            using ZipInputStream zipStream = new ZipInputStream(fileStream);
            while (zipStream.GetNextEntry() is { } zipEntry)
            {
                totalSize += zipEntry.Size;
            }

            return totalSize;
        }

解压Zip文件:

/// <summary>
      /// 解压Zip文件
      /// </summary>
      /// <param name="zipFile">zip文件路径</param>
      /// <param name="outputFolder">解压目录</param>
      /// <param name="cancellationToken">取消操作</param>
      /// <param name="progressChanged">解压进度回调</param>
      /// <returns></returns>
      public static async Task UnZipAsync(string zipFile, string outputFolder,
          CancellationToken cancellationToken = default, Action<ZipProgress> progressChanged = null
)

      {
          if (!File.Exists(zipFile))
          {
              throw new InvalidOperationException($"file not exist,{zipFile}");
          }
          var decompressLength = GetZipFileTotalSize(zipFile);
          using FileStream fileStream = File.OpenRead(zipFile);
          await Task.Run(() =>
          {
              using ZipInputStream zipStream = new ZipInputStream(fileStream);
              long completedSize = 0;
              while (zipStream.GetNextEntry() is { } zipEntry)
              {
                  if (cancellationToken != default && cancellationToken.IsCancellationRequested)
                  {
                      cancellationToken.ThrowIfCancellationRequested();
                  }

                  if (zipEntry.IsDirectory)
                  {
                      string folder = Path.Combine(outputFolder, zipEntry.Name);
                      EnsureFolder(folder);
                  }
                  else if (zipEntry.IsFile)
                  {
                      var operatingSize = completedSize;
                      var zipEntryName = zipEntry.Name;
                      string fullEntryPath = Path.Combine(outputFolder, zipEntryName);
                      string dirPath = Path.GetDirectoryName(fullEntryPath);
                      EnsureFolder(dirPath);
                      //解压后的数据
                      long singleFileSize = WriteUnzipDataToFile(zipStream, fullEntryPath, partialFileSize =>
                      {
                          if (progressChanged == null)
                          {
                              return;
                          }
                          long currentSize = operatingSize + partialFileSize;
                          progressChanged.Invoke(new ZipProgress(currentSize, decompressLength, zipEntryName));
                      });
                      completedSize += singleFileSize;
                  }
              }
          }, cancellationToken);
      }

解压进度能反馈详细的文件写入进度值。另外,这里有个文件夹判断处理,也是支持空文件夹的

Zip压缩,获取所有的文件夹/子文件夹、所有的文件,添加到ZipFile里保存:

/// <summary>
      /// 压缩文件
      /// </summary>
      /// <param name="toZipDirectory">待压缩的文件夹</param>
      /// <param name="destZipPath">Zip文件的保存路径</param>
      /// <returns></returns>
      public static bool Zip(string toZipDirectory, string destZipPath)
      {
          if (string.IsNullOrEmpty(destZipPath))
          {
              throw new ArgumentNullException(nameof(destZipPath));
          }
          if (!destZipPath.ToUpper().EndsWith(".ZIP"))
          {
              throw new ArgumentException("保存路径不是ZIP后缀"nameof(destZipPath));
          }
          if (!Directory.Exists(toZipDirectory))
          {
              throw new ArgumentException("待压缩的文件夹不存在"nameof(toZipDirectory));
          }

          var dirs = Directory.GetDirectories(toZipDirectory, "*", SearchOption.AllDirectories)
              .Select(dir => PathUtils.GetRelativePath(toZipDirectory, dir));
          var files = Directory.GetFiles(toZipDirectory, "*", SearchOption.AllDirectories).ToArray();
          var destFiles = files.Select(file => PathUtils.GetRelativePath(toZipDirectory, file)).ToArray();
          if (File.Exists(destZipPath))
          {
              File.Delete(destZipPath);
          }
          using (ZipFile zipFile = ZipFile.Create(destZipPath))
          {
              zipFile.BeginUpdate();
              foreach (var dir in dirs)
              {
                  zipFile.AddDirectory(dir);
              }
              for (int i = 0; i < files.Length; i++)
              {
                  zipFile.Add(files[i], destFiles[i]);
              }
              zipFile.CommitUpdate();
          }
          return true;
      }

值得一提的是,如有需要指定Zip压缩文件内的文件名以及文件路径,可以在文件时输入对应的压缩后路径定义,注意是指压缩包内的相对路径:

/// <summary>指定的文件压缩到对应的压缩文件中</summary>
      /// <param name="files">待压缩的文件路径列表(绝对路径)</param>
      /// <param name="destFiles">文件路径对应的压缩后路径列表,即压缩后压缩包内的文件路径</param>
      /// <param name="destZipPath">Zip文件的保存路径</param>
      public static bool Zip(List<string> files, List<string> destFiles, string destZipPath)
      {
          if (files.Count != destFiles.Count)
          {
              throw new ArgumentException($"{nameof(files)}{nameof(destFiles)}文件列表数量不一致");
          }
          if (string.IsNullOrEmpty(destZipPath))
              throw new ArgumentNullException(nameof(destZipPath));
          using (ZipFile zipFile = ZipFile.Create(destZipPath))
          {
              zipFile.BeginUpdate();
              for (int i = 0; i < files.Count; i++)
              {
                  zipFile.Add(files[i], destFiles[i]);
              }
              zipFile.CommitUpdate();
          }
          return true;
      }

SharpZipLib虽然功能丰富,但大家看上面的demo代码,接口搞的有点复杂、学习曲线较高 同样我们按上面测试操作,解压缩同一zip文件,解压耗时20719ms,压缩耗时102109ms。。。

DotNetZip

再看看DotNetZip,这个相对SharpZipLibAPI设计的更友好、容易上手。官网是haf/DotNetZip.Semverd(github.com)[3],它停止维护了。。。作者推荐大家去使用System.IO.Compression!好吧先忽略这个,尽管已不再积极维护,但稳定性、性能真的好,下面给大家列下使用demo和性能测试

Zip文件解压:

/// <summary>
   /// 解压Zip文件
   /// </summary>
   /// <param name="zipFile">zip文件路径</param>
   /// <param name="outputFolder">解压目录</param>
   /// <param name="password">密码</param>
   /// <param name="progressChanged">解压进度回调</param>
   /// <returns></returns>
   public static void UnZip(string zipFile, string outputFolder, string password, Action<ZipProgress> progressChanged)
   {
       if (!File.Exists(zipFile)) throw new InvalidOperationException($"file not exist,{zipFile}");
       //获取文件解压后的大小
       var totalZipSize = GetZipFileSize(zipFile);
       long completedSize = 0L;
       using (var zip = ZipFile.Read(zipFile))
       {
           zip.Password = password;
           zip.ExtractProgress += (s, e) =>
           {
               if (e.EventType == ZipProgressEventType.Extracting_EntryBytesWritten)
               {
                   var fileName = e.CurrentEntry.FileName;
                   if (e.BytesTransferred < e.TotalBytesToTransfer)
                   {
                       //单个文件解压中的进度
                       var operatingSize = completedSize + e.BytesTransferred;
                       progressChanged?.Invoke(new ZipProgress(operatingSize, totalZipSize, fileName));
                   }
                   else
                   {
                       //单个文件解压完全的进度
                       completedSize += e.TotalBytesToTransfer;
                       progressChanged?.Invoke(new ZipProgress(completedSize, totalZipSize, fileName));
                   }
               }
           };
           zip.ExtractAll(outputFolder);
       }
   }

这里获取压缩后文件大小,与上面SharpZipLibzipEntry.Size对应,取的是zipEntry.UncompressedSize

非常人性的提供了ExtractProgress事件进度,我们取的是Extracting_EntryBytesWritten类型,可以拿到细节进度。具体进度的处理看上方代码

因为反馈的是详细字节写入进度,所以间隔很短。。。1ms都能给你爆几次进度,尤其是大文件:

所以需要限制下回调Action触发,可以加个计时器限制单个文件的进度回调,如100ms内最多触发一次,下面是优化后的代码:

/// <summary>
    /// 解压Zip文件
    /// </summary>
    /// <param name="zipFile">zip文件路径</param>
    /// <param name="outputFolder">解压目录</param>
    /// <param name="password">密码</param>
    /// <param name="progressChanged">解压进度回调</param>
    /// <returns></returns>
    public static void UnZip(string zipFile, string outputFolder, string password,
        Action<ZipProgress> progressChanged
)

    {
        if (!File.Exists(zipFile)) throw new InvalidOperationException($"file not exist,{zipFile}");
        //获取文件解压后的大小
        var totalZipSize = GetZipFileSize(zipFile);
        long completedSize = 0L;
        using (var zip = ZipFile.Read(zipFile))
        {
            zip.Password = password;
            var lastProgressTick = Environment.TickCount;
            zip.ExtractProgress += (s, e) =>
            {
                if (e.EventType == ZipProgressEventType.Extracting_EntryBytesWritten)
                {
                    var fileName = e.CurrentEntry.FileName;
                    if (e.BytesTransferred < e.TotalBytesToTransfer)
                    {
                        // 单个文件解压变化,限制间隔时间触发解压事件
                        if (Environment.TickCount - lastProgressTick < ProgressEventTick)
                        {
                            return;
                        }
                        lastProgressTick = Environment.TickCount;
                        //单个文件解压中的进度
                        var operatingSize = completedSize + e.BytesTransferred;
                        progressChanged?.Invoke(new ZipProgress(operatingSize, totalZipSize, fileName));
                    }
                    else
                    {
                        //重置计时器
                        lastProgressTick = Environment.TickCount;
                        //单个文件解压完全的进度
                        completedSize += e.TotalBytesToTransfer;
                        progressChanged?.Invoke(new ZipProgress(completedSize, totalZipSize, fileName));
                    }
                }
            };
            zip.ExtractAll(outputFolder);
        }
    }

解压进度就正常了很多,限制间隔只会优化单个文件解压过程中的进度,单个文件解压完成时最后还是有进度回调的。

再看看Zip压缩:

public static void Zip(string sourceFolder, string destZipFile, string password,
        Action<ZipProgress> zipProgressAction
)

    {
        if (string.IsNullOrEmpty(destZipFile)) throw new ArgumentNullException(nameof(destZipFile));
        if (!destZipFile.ToUpper().EndsWith(".ZIP")) throw new ArgumentException("保存路径不是Zip文件", destZipFile);
        if (File.Exists(destZipFile)) File.Delete(destZipFile);

        using (var zipFile = new ZipFile())
        {
            // 设置压缩进度事件处理程序
            zipFile.SaveProgress += (sender, e) =>
            {
                if (e.EventType == ZipProgressEventType.Saving_AfterWriteEntry)
                    zipProgressAction?.Invoke(new ZipProgress(e.EntriesSaved, e.EntriesTotal, e.CurrentEntry.FileName));
            };
            zipFile.AddDirectory(sourceFolder);
            zipFile.Password = password;
            zipFile.Save(destZipFile);
        }
    }

如果不考虑加密、压缩进度,DotNetZip压缩zip文件只需要几行代码,所以是相当的易学易用入手快

还是同一个847Mzip文件,测试下解压缩性能,解压11907ms,压缩耗时16282ms,用数据说话性能强不强

用表格把这三个方案的对比列下:

所以如果你需要处理简单的ZIP压缩和解压任务,且不需要高级特性,建议使用System.IO.Compression

需要考虑解压缩性能比如公司的大文件OTA功能,需要减少业务的处理时间,推荐使用DotNetZipDotNetZip也能提供高级特性,进度显示等。至于停止维护的状况可以忽然,有BUG大家可以在公司内或者github维护下这个组件代码

参考资料
[1]

《.NET WebSocket高并发通信阻塞问题 - 唐宋元明清2188 - 博客园 (cnblogs.com)》: https://www.cnblogs.com/kybs0/p/18395504

[2]

icsharpcode/SharpZipLib: #ziplib is a Zip, GZip, Tar and BZip2 library written entirely in C# for the .NET platform. (github.com): https://github.com/icsharpcode/SharpZipLib

[3]

haf/DotNetZip.Semverd(github.com): https://github.com/haf/DotNetZip.Semverd


WPF开发者
「WPF开发者」现役微软MVP,专注 WPF 和 Avalonia 技术分享与传播。
 最新文章