C#实现文件或数据的压缩和解压缩之Zip(C#原生代码、.NET自带的压缩和解压,不使用第三方库)

1,614 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第29天,点击查看活动详情

本篇主要介绍使用C#自带的压缩类实现对Zip格式的文件或数据的解/压缩。

通过引用.NET自带的System.IO.Compression.FileSystem.dllSystem.IO.Compression.dll(项目引用右键->添加引用->搜索System.IO.Compression.FileSystem添加),实现原生C#压缩、解压缩文件,读取压缩文档,并进行简单的ZipHelper、GZipHelper封装(后续)。

新建项目ZipUnZip,添加引用,using System.IO.Compression;

目前所知7z压缩文件格式,需要引用第三方库,原生.NET未提供相关方法

压缩解压缩zip文件

压缩、解压缩的使用

System.IO.Compression命名空间下,ZipFile静态类提供压缩、解压缩文件夹的方法:

  • ZipFile.CreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName) 从源目录创建压缩文件到目标地址(目标压缩文件名)
  • ZipFile.ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName) 将源zip文件解压缩到目标目录中

默认不提供直接从一个文件创建压缩文档。

压缩时目标文档存在将会报错,因此可以提前判断,是否覆盖还是不压缩;解压缩时同样,如果源压缩文档不存在,或解压目标路径中存在同名文件,也都会报错。

  • 压缩
// 压缩
var testPath = "test"; // 程序所在目录下的`test/`目录下有a.txt\b.txt\c.txt文件
var zipFile = "test.zip";

ZipFile.CreateFromDirectory(testPath, zipFile);

不能直接从文件创建压缩包,只能从文件夹创建压缩文件,否则报错:System.IO.IOException:目录名称无效

var aTxt = @"test\a.txt";
var aTxtZip = "aTxt.zip";
// 报错:System.IO.IOException:目录名称无效。
ZipFile.CreateFromDirectory(aTxt, aTxtZip);
  • 解压缩

解压缩刚创建的zipFile文件

// 解压缩
var extraPath = "unZipDir";            

ZipFile.ExtractToDirectory(zipFile, extraPath);

读取压缩文档、解压缩指定文件

System.IO.Compression.dll提供了可以访问zip压缩文档的方法OpenRead/Open,获取到ZipArchive对象,可以读取zip压缩文件的内容,通ZipArchiveEntry对象的ExtractToFile方法解压其中的某个文件项。

如下,通过判断压缩档每项的文件名后置".txt",解压缩提取指定类型的文件。

string zipFile = @"test.zip";
string extractPath = @"extractSomeFile";

using (ZipArchive archive = ZipFile.OpenRead(zipFile))
{
    foreach (ZipArchiveEntry entry in archive.Entries)
    {
        if (entry.Name.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
        {
            entry.ExtractToFile(Path.Combine(extractPath, entry.FullName));
        }
    }
}

将指定文件添加到压缩包、向压缩包中某个文件写入内容

ZipArchive对象支持更新模式,从而向压缩包添加新文件(创建新项)。 ZipArchiveEntry 提供流对象的访问形式,从而实现向压缩档内的某个文件写入或添加新内容

var zipFile = "test.zip";
using (FileStream zipToOpen = new FileStream(zipFile, FileMode.Open))
{
    using (ZipArchive archive = new ZipArchive(zipToOpen, ZipArchiveMode.Update))
    {
        ZipArchiveEntry readmeEntry = archive.CreateEntry("Readme.txt");
        using (StreamWriter writer = new StreamWriter(readmeEntry.Open()))
        {
            writer.WriteLine("有关该压缩包的信息");
            writer.WriteLine("========================");
        }
    }
}

压缩解压缩的帮助类封装ZipHelper

关于源文件和目标文件存在或不存在的处理,最好不要直接使用覆盖,而是一一确认【尤其压缩包中有多个文件与目标文件夹中的文件同名时】

/// <summary>
/// ZipHelper类,zip文件的压缩解压缩(UTF-8编码,如果需要指定其他编码,请使用ZipFile的原生方法)
/// </summary>
public static class ZipHelper
{
    /// <summary>
    /// Zip文件压缩,生成同名的.zip文件
    /// </summary>
    /// <param name="sourceDirOrFileName">源文件或目录</param>
    /// <param name="isWrite">若zip文件存在是否覆盖</param>
    /// <param name="compressionLevel">指定压缩级别,压缩速度还是大小</param>
    /// <returns></returns>
    public static void Compress(string sourceDirOrFileName,bool isWrite=false, CompressionLevel? compressionLevel = null)
    {
        Compress(sourceDirOrFileName, Path.Combine(File.Exists(sourceDirOrFileName) ? Path.GetDirectoryName(sourceDirOrFileName): Directory.GetParent(sourceDirOrFileName).FullName, $"{Path.GetFileNameWithoutExtension(sourceDirOrFileName)}.zip"), isWrite, compressionLevel);
    }
    /// <summary>
    /// Zip文件压缩,生成指定文件名的.zip文件
    /// </summary>
    /// <param name="sourceDirOrFileName">源文件或目录</param>
    /// <param name="destinationZipFileName">压缩包文件名(含.zip后缀的文件名)</param>
    /// <param name="isWrite">若zip文件存在是否覆盖</param>
    /// <param name="compressionLevel">指定压缩级别,压缩速度还是大小</param>
    /// <returns></returns>
    public static void Compress(string sourceDirOrFileName, string destinationZipFileName, bool isWrite = false, CompressionLevel? compressionLevel = null)
    {
        var sourceDirName = sourceDirOrFileName;
        var isDeleteTempDir = false;
        try
        {
            if (File.Exists(sourceDirOrFileName))
            {
                // 处理文件 创建随机文件夹并
                var randomDir = Path.Combine( Path.GetDirectoryName(sourceDirOrFileName), Path.GetRandomFileName());
                while (Directory.Exists(randomDir))
                {
                    randomDir = Path.Combine(Path.GetDirectoryName(sourceDirOrFileName), Path.GetRandomFileName());
                }
                var di = Directory.CreateDirectory(randomDir);
                di.Attributes = FileAttributes.Directory | FileAttributes.Hidden;
                File.Copy(sourceDirOrFileName, Path.Combine(randomDir, Path.GetFileName(sourceDirOrFileName)));
                sourceDirName = randomDir;
                isDeleteTempDir = true;
            }
            var isCompress = true;
            if (File.Exists(destinationZipFileName))
            {
                if (isWrite)//覆盖并压缩
                {
                    File.Delete(destinationZipFileName);
                }
                else
                {
                    isCompress = false;
                }
            }
            if (isCompress)
            {
                if (compressionLevel.HasValue)
                {
                    ZipFile.CreateFromDirectory(sourceDirName, destinationZipFileName, compressionLevel.Value,false);
                }
                else
                {
                    ZipFile.CreateFromDirectory(sourceDirName, destinationZipFileName);
                }
            }
        }
        finally
        {

            if (isDeleteTempDir) // 删除创建的随机文件夹
            {
                if (Directory.Exists(sourceDirName))  Directory.Delete(sourceDirName, true);
            }
        }
    }
    /// <summary>
    /// 向zip压缩档中添加文件或文件夹。暂未实现,基本思路是:遍历读取要添加的文件,同时在压缩档对象ZipArchive创建对应文件项,通过读取原文件流,写入新创建的ZipArchiveEntry。
    /// 更正确的处理应该是,使用 ZipFileExtensions 的 ZipArchive 扩展方法CreateEntryFromFile(),直接从文件创建
    /// </summary>
    /// <param name="sourceDirOrFileName"></param>
    /// <param name="destinationZipFileName"></param>
    /// <param name="compressionLevel"></param>
    [Obsolete("未实现",true)]
    public static void CompressAddFile(string sourceDirOrFileName, string destinationZipFileName, CompressionLevel? compressionLevel = null){}
    /// <summary>
    /// 解压缩zip文件到当前文件夹
    /// </summary>
    /// <param name="sourceZipFileName">源zip文件</param>
    /// <param name="isWrite">解压目标文件存在时是否覆盖,推荐false</param>
    /// <returns></returns>
    public static void Decompress(string sourceZipFileName,bool isWrite=false)
    {
         Decompress(sourceZipFileName,Path.Combine(Path.GetDirectoryName(sourceZipFileName),Path.GetFileNameWithoutExtension(sourceZipFileName)), isWrite);
    }
    /// <summary>
    /// 解压缩zip文件
    /// </summary>
    /// <param name="sourceZipFileName">源zip文件</param>
    /// <param name="destinationDirName">目标目录</param>
    /// <param name="isWrite">解压目标文件存在时是否覆盖,推荐false</param>
    /// <returns></returns>
    public static void Decompress(string sourceZipFileName, string destinationDirName, bool isWrite = false)
    {
        try
        {
            ZipFile.ExtractToDirectory(sourceZipFileName, destinationDirName);
        }
        catch (IOException ex)
        {
            var isThrow = true;
            if (isWrite)
            {
                // 判断目标文件是否存在
                using (ZipArchive archive = ZipFile.OpenRead(sourceZipFileName)) // 需要添加System.IO.Compression(.dll)引用
                {
                    foreach (ZipArchiveEntry entry in archive.Entries)
                    {
                        if (File.Exists(Path.Combine(destinationDirName, entry.FullName)))
                        {
                            File.Delete(Path.Combine(destinationDirName, entry.FullName));
                            isThrow = false;
                        }
                    }
                }
                if (!isThrow) ZipFile.ExtractToDirectory(sourceZipFileName, destinationDirName);
            }
            if (isThrow) throw ex;
        }
    }
    /// <summary>
    /// 解压缩zip中指定模式的文件
    /// </summary>
    /// <param name="sourceZipFileName">源zip文件</param>
    /// <param name="destinationDirName">目标目录</param>
    /// <param name="zipEntryRegPattern">要解压缩的zip内文件名和路径符合的正则模式,比如,".txt$"解压缩txt后缀的文件</param>
    /// <param name="isWrite">解压目标文件存在时是否覆盖,推荐false</param>
    /// <returns></returns>
    public static void Decompress(string sourceZipFileName, string destinationDirName,string zipEntryRegPattern, bool isWrite = false)
    {
            var existsFiles = new List<string>();
            // 判断目标文件是否存在
            using (ZipArchive archive = ZipFile.OpenRead(sourceZipFileName)) // 需要添加System.IO.Compression(.dll)引用
            {
                foreach (ZipArchiveEntry entry in archive.Entries)
                {
                    var m = Regex.Match(entry.FullName, zipEntryRegPattern);
                    if (m.Success)
                    {
                        if (File.Exists(Path.Combine(destinationDirName, entry.FullName)))
                        {
                                existsFiles.Add(entry.FullName);
                        }
                    }
                }
                if (existsFiles.Count>0)
                {
                    if (!isWrite)
                    {
                        throw new Exception($"解压目录中存在同名文件,请确认后再解压缩。【{string.Join(";", existsFiles)}】");
                    }
                }
                foreach (ZipArchiveEntry entry in archive.Entries)
                {
                    var m = Regex.Match(entry.FullName, zipEntryRegPattern);
                    if (m.Success)
                    {
                        entry.ExtractToFile(Path.Combine(destinationDirName, entry.FullName), true);
                    }
                }
            }
    }
}

ZipFileExtensions 类中提供的扩展方法

ZipFileExtensions 类提供对ZipArchive和ZipArchiveEntry的扩展。

比如:

  • 直接从文件创建ZipArchiveEntry压缩归档项的ZipArchive扩展方法CreateEntryFromFile(ZipArchive, String, String)CreateEntryFromFile(ZipArchive, String, String, CompressionLevel)

  • 解压某一个归档项到文件夹的ZipArchive扩展方法ExtractToDirectory(ZipArchive, String)ExtractToDirectory(ZipArchive, String, Boolean)

  • 上面已经使用到的ZipArchiveEntry提取到文件的扩展方法ExtractToFile(ZipArchiveEntry, String)、``ExtractToFile(ZipArchiveEntry, String, Boolean)`

使用System.IO.Packaging实现对多个文件打包压缩

关于打包

参考

.NET 自带的压缩和解压

推荐