.NET 壓縮/解壓檔案

唐宋元明清2188發表於2024-09-06

本文為大家介紹下.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維護下這個元件程式碼

相關文章