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