在 .NET 4.5 中轻松创建 Zip 文件
我们将使用更新的 System.IO.Compression 命名空间来轻松创建、更新和提取 zip 文件。
引言
NET 4.5 中的一个新变化是 System.IO.Compression 命名空间的巨大改进。现在,我们可以以一种非常简单的方式执行 zip 和 unzip 操作。我将演示如何执行这些操作以及如何避免使用其中一些方法时的陷阱。最后,我将介绍如何扩展当前功能,以弥补我们将讨论的一些更简单的不足之处。
入门
为了使用 Compression 命名空间的新功能,您需要安装 .NET 4.5。我构建的示例将在 Visual Studio 11 的 beta 版本中展示,所以如果您下载本文附加的解决方案文件,您也需要使用 beta 版本。此外,您还需要添加对 System.IO.Compression 和 System.IO.Compression.ZipArchive 的引用。仅仅添加 System.IO.Compression 作为 using 语句是不够的,您还需要将其添加为引用。为了简化起见,我还为这两个命名空间以及 System.IO 添加了 using 语句。完成这些步骤后,我们就可以开始编写代码了。每当您遇到下面编号的示例时,您都可以查看本文附加的源代码中相同的示例。代码将在其上方的注释中带有相应的示例编号。
压缩文件和文件夹
要压缩文件夹的内容(包括子文件夹),只需调用 ZipFile 的 CreateFromDirectory 方法。您需要首先传入要压缩的根文件夹,然后是将被创建的 zip 文件的完整名称(包括相对或绝对路径)。这是一个示例调用(这是示例 1)
ZipFile.CreateFromDirectory(@“C:\Temp\Logs”, @“C:\Temp\LogFiles.zip”);
让我们稍微分解一下。首先,如果您不知道,@ 符号告诉编译器后面的字符串是字符串字面量(无需翻译特殊字符 - 只需显示引号之间的内容)。这使我们每个路径段只需要输入一个斜杠。接下来,请注意这里的简洁性。我们只向方法提供两样信息:要压缩什么以及将其放在哪里。最后,请注意没有提到压缩。默认情况下,该方法假定 Optimal 压缩(其他选项是 Fastest 和 None)。另一个默认设置是我们不会将根目录包含在 zip 文件中。我们会包含根目录下的所有文件夹和文件,只是不包含目录本身。
CreateFromDirectory 方法的唯一其他重载包括两个附加选项:压缩和包括根目录。这些都相当容易理解。下面是一个示例,可以帮助您直观地理解(这是示例 2)
ZipFile.CreateFromDirectory(@“C:\Temp\Logs”, @“C:\Temp\LogFiles.zip”, CompressionLevel.Optimal, true);
此示例与上面相同,只是它将根目录包含在 zip 文件中。如果您仔细考虑,您可能会注意到我们当前示例的一个缺点:我们没有检查输出 zip 文件是否存在。如果您同时运行这两个示例,系统将在第二个文件处抛出错误,因为输出文件已经存在(因为它们命名相同)。一种解决方案是在创建文件之前检查其是否存在。我看到的问题是,这会要求您每次使用此类时都编写相同的代码。对我来说,这似乎不是非常 DRY。我将在下面展示如何扩展此功能以改进此方法的工作方式。
解压文件
此功能的基本实现与压缩文件一样简单。要将 zip 文件解压到目录,只需执行以下示例(这是示例 3)
ZipFile.ExractToDirectory(@“C:\Temp\LogFiles.zip”, @“C:\Temp\Logs”);
这会将 LogFiles.zip 文件解压到 Logs 目录。这里我们立即看到了另一个问题:如果某些文件已存在怎么办?简而言之,提取将失败并抛出错误。糟糕。这似乎不太有用。实际上,对于日常使用来说,这还可以。大多数情况下,您会将文件解压到新的根文件夹。但是,如果您尝试从 zip 恢复备份,您可能会遇到一个严重的问题。然而,这意味着这不是适用于所有情况的正确方法。如果您需要有条件地恢复文件(覆盖如果较新,如果不存在等),您需要读取 zip 文件并对每个文件执行操作。我们将在下一步中看到如何做到这一点。
打开 Zip 文件
有时您需要打开一个 zip 文件并读取其内容。这里的代码会稍微复杂一些,但这仅仅是因为我们需要遍历 zip 文件中包含的文件。下面是一个示例(这是示例 4)
//This stores the path where the file should be unzipped to,
//including any subfolders that the file was originally in.
string fileUnzipFullPath;
//This is the full name of the destination file including
//the path
string fileUnzipFullName;
//Opens the zip file up to be read
using (ZipArchive archive = ZipFile.OpenRead(zipName))
{
//Loops through each file in the zip file
foreach (ZipArchiveEntry file in archive.Entries)
{
//Outputs relevant file information to the console
Console.WriteLine("File Name: {0}", file.Name);
Console.WriteLine("File Size: {0} bytes", file.Length);
Console.WriteLine("Compression Ratio: {0}", ((double)file.CompressedLength / file.Length).ToString("0.0%"));
//Identifies the destination file name and path
fileUnzipFullName = Path.Combine(dirToUnzipTo, file.FullName);
//Extracts the files to the output folder in a safer manner
if (!System.IO.File.Exists(fileUnzipFullName))
{
//Calculates what the new full path for the unzipped file should be
fileUnzipFullPath = Path.GetDirectoryName(fileUnzipFullName);
//Creates the directory (if it doesn't exist) for the new path
Directory.CreateDirectory(fileUnzipFullPath);
//Extracts the file to (potentially new) path
file.ExtractToFile(fileUnzipFullName);
}
}
}
这里一个很酷的事情是 ZipArchive 实现了 IDisposable,这允许我们使用 using 语句。这确保了我们在完成类后正确地将其处置掉。OpenRead 方法与 Read 模式下的 Open 方法相同。我们将在下一步介绍 Open 方法。
此代码以只读模式打开 zip 文件(它不能修改 zip 文件内容,但可以提取它们)。然后它遍历每个文件并为我们提供文件的属性。我添加了一个有趣的输出,标题为“压缩比”,我计算了压缩大小和实际大小之间的差异。然后我显示了压缩大小占总大小的百分比。最后,我提取了文件并将其与原始路径一起放入指定的根目录。这涉及几个与 Compression 不直接相关的额外步骤。我构建了文件的新路径(不带文件名),然后创建该目录(如果已存在则什么也不做),然后将文件提取到新位置。现有的 ExtractToFile 方法不会在路径不存在时创建路径(它只会抛出错误),并且如果文件已存在,它也会抛出错误。
这是 Compression 命名空间的另一个我希望更健壮的领域。但是,我在下面写了几个方法,它们将扩展 Compression 命名空间的功能,使其更容易使用。但是,首先,让我们回顾一下 Compression 命名空间的其余部分。
手动创建 Zip 文件
上面我向您展示了如何通过传递根文件夹和 zip 文件名来创建 zip 文件。但是,有时您想专门选择要包含在存档中的文件。上面我们已经看到如何遍历现有存档。我们在这里做同样的事情,只是我们将添加文件而不是查看它们。下面是一个示例(这是示例 5)
//Creates a new, blank zip file to work with - the file will be //finalized when the using statement completes using (ZipArchive newFile = ZipFile.Open(zipName, ZipArchiveMode.Create)) { //Here are two hard-coded files that we will be adding to the zip //file. If you don't have these files in your system, this will //fail. Either create them or change the file names. newFile.CreateEntryFromFile(@"C:\Temp\File1.txt", "File1.txt"); newFile.CreateEntryFromFile(@"C:\Temp\File2.txt", "File2.txt", CompressionLevel.Fastest); }
基本上,我们正在创建这个文件,向其中添加两个新文件,然后 zip 文件将隐式提交(在 using 语句结束时)。请注意,当我添加第二个 txt 文件时,我为该特定文件设置了 CompressionLevel。这是每个文件的选择。请注意,我们使用的是 Open 方法,并将模式设置为 Create。模式选项有 Create、Open 和 Update。我上面提到过,当您使用 Open 模式时,它与 OpenRead 方法相同。这里我们涵盖了 Create 模式。最后一种模式是 Update,我们将在下一步进行介绍。
向现有 Zip 文件添加或删除文件
最后我们要介绍的 Open 模式是 Update 模式。这是对现有 zip 文件进行读写操作的模式。您与文件的交互方式与以上两种方法没有区别。例如,如果我们想将两个新文件添加到我们刚刚创建的 ManualBackup.zip 文件中,我们会这样做(这是示例 6)
//Opens the existing file like we opened the new file (just changed
//the ZipArchiveMode to Update
using (ZipArchive modFile = ZipFile.Open(zipName, ZipArchiveMode.Update))
{
//Here are two hard-coded files that we will be adding to the zip
//file. If you don't have these files in your system, this will
//fail. Either create them or change the file names. Also, note
//that their names are changed when they are put into the zip file.
modFile.CreateEntryFromFile(@"C:\Temp\File1.txt", "File10.txt");
modFile.CreateEntryFromFile(@"C:\Temp\File2.txt", "File20.txt", CompressionLevel.Fastest);
//We could also add the code from Example 4 here to read
//the contents of the open zip file as well.
}
请注意,我重命名了我添加的文件,以便它们有所不同。
扩展基本功能
现在我们已经学会了如何使用 Compression 命名空间处理 zip 文件,让我们来看看如何使这个命名空间的功能变得更好。上面我们指出了现有系统中的一些不足之处,主要围绕如何处理意外情况(文件已存在、文件夹不存在等)。如果我们想正确地处理存档,我们确实需要每次添加大量代码来处理所有可能的情况。为了避免您每次想处理存档时都重新创建这些代码,我添加了一个 Helper 命名空间,它为 Microsoft 提供的标准方法增加了功能。
我为 Compression 命名空间的标准方法集添加了三个新方法。我将解释每个方法的作用,然后将代码放在其下方。如果您想要完整的项目(这是一个易于在您的项目中使用 DLL),只需下载本文的代码即可。我附上了完整的库以及如何使用 Compression 命名空间和我的改进的 Compression 方法的示例。
改进的 ExtractToDirectory 方法
此方法改进了 ExtractToDirectory 方法。它不是盲目地提取文件并寄希望于没有与我们正在提取的文件匹配的现有文件,而是遍历每个文件并将其与目标进行比较,以查看它是否存在。如果存在,它将按照我们的要求处理该特定文件(Overwrite.Always、Overwrite.IfNewer 或 Overwrite.Never)。它还会在尝试写入文件之前创建目标路径(如果它不存在)。这两个新功能确保我们单独处理存档中的每个文件,并且我们不会因为存档中某个文件的问题而导致整个提取失败。
要调用此方法,请使用以下行(这是示例 7)
Compression.ImprovedExtractToDirectory(zipName, dirToUnzipTo, Compression.Overwrite.IfNewer);
请注意这一行多么简单。事实上,它的格式与示例 3 相同,这意味着您可以省略最后一个参数,它仍然可以工作(默认的 Overwrite 是 IfNewer)。此方法的代码如下
public static void ImprovedExtractToDirectory(string sourceArchiveFileName,
string destinationDirectoryName,
Overwrite overwriteMethod = Overwrite.IfNewer)
{
//Opens the zip file up to be read
using (ZipArchive archive = ZipFile.OpenRead(sourceArchiveFileName))
{
//Loops through each file in the zip file
foreach (ZipArchiveEntry file in archive.Entries)
{
ImprovedExtractToFile(file, destinationDirectoryName, overwriteMethod);
}
}
}
改进的 ExtractToFile 方法
当您打开一个存档并遍历其中包含的文件时,您需要一种合理的方式来安全地提取文件。在 打开 Zip 文件 部分(示例 4)中,我们添加了一些辅助代码,以确保我们只在文件不存在时才提取它们。但是,对于如此标准的操作,我们有太多的代码行和太多的逻辑。因此,我创建了一个名为 ImprovedExtractToFile 的方法。您传入 ZipArchiveEntry 引用(文件)、根目标目录(我的方法将根据根目录和我们要提取的文件的相对路径计算完整目录),以及当文件存在于目标位置时我们希望执行的操作。作为如何调用此方法的示例,我复制了示例 4 并用我的新调用替换了辅助代码(这是示例 8)
//Opens the zip file up to be read
using (ZipArchive archive = ZipFile.OpenRead(zipName))
{
//Loops through each file in the zip file
foreach (ZipArchiveEntry file in archive.Entries)
{
//Outputs relevant file information to the console
Console.WriteLine("File Name: {0}", file.Name);
Console.WriteLine("File Size: {0} bytes", file.Length);
Console.WriteLine("Compression Ratio: {0}", ((double)file.CompressedLength / file.Length).ToString("0.0%"));
//This is the new call
Compression.ImprovedExtractToFile(file, dirToUnzipTo, Compression.Overwrite.Always);
}
}
请注意两个示例的相对大小。这个更简洁,并且我们不需要在其他地方重复的逻辑更少。但是,我没有抽象出压缩比的代码。如果您发现自己需要多次执行此操作,可以将其添加到库中作为一个方法。同样重要的是要注意,我在我的 ImprovedExtractToDirectory 方法中使用了 ImprovedExtractToFile 方法。这样我们就不必在不需要的地方重复逻辑。此方法的代码如下
public static void ImprovedExtractToFile(ZipArchiveEntry file,
string destinationPath,
Overwrite overwriteMethod = Overwrite.IfNewer)
{
//Gets the complete path for the destination file, including any
//relative paths that were in the zip file
string destinationFileName = Path.Combine(destinationPath, file.FullName);
//Gets just the new path, minus the file name so we can create the
//directory if it does not exist
string destinationFilePath = Path.GetDirectoryName(destinationFileName);
//Creates the directory (if it doesn't exist) for the new path
Directory.CreateDirectory(destinationFilePath);
//Determines what to do with the file based upon the
//method of overwriting chosen
switch (overwriteMethod)
{
case Overwrite.Always:
//Just put the file in and overwrite anything that is found
file.ExtractToFile(destinationFileName, true);
break;
case Overwrite.IfNewer:
//Checks to see if the file exists, and if so, if it should
//be overwritten
if (!File.Exists(destinationFileName) || File.GetLastWriteTime(destinationFileName) < file.LastWriteTime)
{
//Either the file didn't exist or this file is newer, so
//we will extract it and overwrite any existing file
file.ExtractToFile(destinationFileName, true);
}
break;
case Overwrite.Never:
//Put the file in if it is new but ignores the
//file if it already exists
if (!File.Exists(destinationFileName))
{
file.ExtractToFile(destinationFileName);
}
break;
default:
break;
}
}
AddToArchive 方法
我创建了这个新方法,用于将一组文件放入 zip 文件。如果 zip 文件已存在,我们可以将文件合并到 zip 文件中,可以覆盖 zip 文件,可以抛出错误,或者可以静默失败(您应该避免最后一种选择 - 它有一些有效的用途,但不如您想象的那么常见)。如果我们最终将文件合并到现有存档中,我们可以检查是否每个文件都要放入。我们可以覆盖所有匹配的文件,我们只能在要放入的文件较新时覆盖匹配的文件,或者如果我们已经有一个匹配项,我们可以忽略要放入的文件。
对于曾经是一个相当简单的任务来说,这有很多选项。但是,此方法处理了示例 5 的所有内容,以及示例 6 的大部分内容(我们不遍历存档中的现有文件 - 这可能是另一个辅助方法,但我暂时决定不这样做)。下面是我们可以用于示例 5 和示例 6 将文件添加到存档的改进调用(这是示例 9)
//This creates our list of files to be added
List<string> filesToArchive = new List<string>();
//Here we are adding two hard-coded files to our list
filesToArchive.Add(@"C:\Temp\File1.txt");
filesToArchive.Add(@"C:\Temp\File2.txt");
Compression.AddToArchive(zipName,
filesToArchive,
Compression.ArchiveAction.Replace,
Compression.Overwrite.IfNewer,
CompressionLevel.Optimal);
请注意,我们选择在 zip 文件已存在时进行 Replace,并在我们要插入的文件较新时覆盖内部的任何匹配文件。我们还为存档中的每个文件设置了 Optimal 压缩。这最后三个参数可以根据您的需求进行更改。它们也可以省略。我指定的这些值是默认值。此方法的代码如下
public static void AddToArchive(string archiveFullName,
List<string> files,
ArchiveAction action = ArchiveAction.Replace,
Overwrite fileOverwrite = Overwrite.IfNewer,
CompressionLevel compression = CompressionLevel.Optimal)
{
//Identifies the mode we will be using - the default is Create
ZipArchiveMode mode = ZipArchiveMode.Create;
//Determines if the zip file even exists
bool archiveExists = File.Exists(archiveFullName);
//Figures out what to do based upon our specified overwrite method
switch (action)
{
case ArchiveAction.Merge:
//Sets the mode to update if the file exists, otherwise
//the default of Create is fine
if (archiveExists)
{
mode = ZipArchiveMode.Update;
}
break;
case ArchiveAction.Replace:
//Deletes the file if it exists. Either way, the default
//mode of Create is fine
if (archiveExists)
{
File.Delete(archiveFullName);
}
break;
case ArchiveAction.Error:
//Throws an error if the file exists
if (archiveExists)
{
throw new IOException(String.Format("The zip file {0} already exists.", archiveFullName));
}
break;
case ArchiveAction.Ignore:
//Closes the method silently and does nothing
if (archiveExists)
{
return;
}
break;
default:
break;
}
//Opens the zip file in the mode we specified
using (ZipArchive zipFile = ZipFile.Open(archiveFullName, mode))
{
//This is a bit of a hack and should be refactored - I am
//doing a similar foreach loop for both modes, but for Create
//I am doing very little work while Update gets a lot of
//code. This also does not handle any other mode (of
//which there currently wouldn't be one since we don't
//use Read here).
if (mode == ZipArchiveMode.Create)
{
foreach (string file in files)
{
//Adds the file to the archive
zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression);
}
}
else
{
foreach (string file in files)
{
var fileInZip = (from f in zipFile.Entries
where f.Name == Path.GetFileName(file)
select f).FirstOrDefault();
switch (fileOverwrite)
{
case Overwrite.Always:
//Deletes the file if it is found
if (fileInZip != null)
{
fileInZip.Delete();
}
//Adds the file to the archive
zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression);
break;
case Overwrite.IfNewer:
//This is a bit trickier - we only delete the file if it is
//newer, but if it is newer or if the file isn't already in
//the zip file, we will write it to the zip file
if (fileInZip != null)
{
//Deletes the file only if it is older than our file.
//Note that the file will be ignored if the existing file
//in the archive is newer.
if (fileInZip.LastWriteTime < File.GetLastWriteTime(file))
{
fileInZip.Delete();
//Adds the file to the archive
zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression);
}
}
else
{
//The file wasn't already in the zip file so add it to the archive
zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression);
}
break;
case Overwrite.Never:
//Don't do anything - this is a decision that you need to
//consider, however, since this will mean that no file will
//be writte. You could write a second copy to the zip with
//the same name (not sure that is wise, however).
break;
default:
break;
}
}
}
}
}
压缩比较
在评论中有人提出,如果能看到这些 zip 方法与其他常用的 zip 方法进行比较会很有益。我认为这是一个很棒的主意,所以我开始调查。为了创建一个测试数据集,我创建了 10,000 个文本文件,每个文件包含 10,000 行文本。文本文件易于创建,也易于压缩。这为我提供了一个统一的基础。然后我将我的代码与 Windows 的“发送到 Zip”和 7zip 进行了测试。以下是结果
压缩方法 | 压缩级别 | 耗时 | 最终大小 |
7zip(右键单击并发送到 zip 文件) | 正常 | 14 分钟 19 秒 | 66,698kb |
代码(Debug 模式) | Optimal | 10 分钟 13 秒 | 62,938kb |
代码(Release 模式) | Optimal | 9 分钟 36 秒 | 62,938kb |
Windows Zip(右键单击并发送到 zip 文件) | 正常 | 8 分钟 31 秒 | 62,938kb |
代码(Release 模式) | Fastest | 8 分钟 5 秒 | 121,600kb |
7zip(通过 UI 压缩文件) | Fastest | 8 分钟 0 秒 | 66,698kb |
请注意,我的机器相当慢。我相信您的系统可以运行得更快。但是,这里要看的是不是实际的时间,而是方法之间的比较。看起来 Optimal 压缩与 Windows zip 相同(并且可能使用相同的库)。实际压缩比 7zip 稍好,但速度较慢。第一个 7zip 条目让我有点困惑。它似乎在做与最后一个条目相同的任务,但它耗费了六分钟以上。我重复了测试以进行验证。我还验证了我使用的是最新版本的 7zip。
我认为这些测试结果表明,在 .NET 4.5 中使用 Compression 命名空间对于所有类型的项目都是一个非常有价值的选择。它与一个强大的竞争对手匹敌,而且事实上您不需要依赖第三方库即可使用它,在我看来,这使得它是存档文件的明显选择。
结论
NET 4.5 中的 System.IO.Compression 命名空间为我们提供了一种简单的方式来处理 zip 文件。我们可以创建存档、更新存档和提取存档。这与我们以前的做法相比是一个巨大的飞跃。虽然在实现方面存在一些不足之处,但我们已经看到了如何轻松地克服它们。我希望您发现这篇文章有帮助。一如既往,我非常感谢您的建设性反馈。
历史
- 2012年5月10日 - 初始版本
- 2012年5月11日 - 添加了压缩比较部分