使用C#和System.IO.Packaging程序化地从Zip归档文件中提取文件

49
我有一堆ZIP文件,急需进行分层重新组织和提取。目前,我能做的是创建目录结构并将zip文件移动到正确位置。我缺少的是从ZIP存档中提取文件的部分。
我已经看过关于ZipArchive类的MSDN文章,并且理解得还不错。我也看过VBScript ways to extract。这不是一个复杂的类,所以提取东西应该很简单。实际上,它“大多数”起作用。为了参考,我在下面包含了我的当前代码。
 using (ZipPackage package = (ZipPackage)Package.Open(@"..\..\test.zip", FileMode.Open, FileAccess.Read))
 {
    PackagePartCollection packageParts = package.GetParts();
    foreach (PackageRelationship relation in packageParts)
    {
       //Do Stuff but never gets here since packageParts is empty.
    }
 }

问题似乎出现在GetParts(或者说任何Get开头的函数)中。看起来这个包在打开时是空的。深入调试后,调试器显示私有成员_zipArchive实际上有部分内容。它们的名称和其他所有东西都是正确的。为什么GetParts函数无法检索它们呢?我已经尝试将打开的内容转换为ZipArchive,但没有帮助。Grrr.

1
我在MS Connect上发布了一个请求,希望添加对通用ZIP存档的支持。您也可以在https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=477393进行投票。 - Joannes Vermorel
6个回答

47

如果你要处理ZIP文件,你可能需要使用第三方库来帮助你。

例如,最近更新的DotNetZip库可以帮助你。当前版本为v1.8。下面是一个创建zip文件的示例:

using (ZipFile zip = new ZipFile())
{
  zip.AddFile("c:\\photos\\personal\\7440-N49th.png");
  zip.AddFile("c:\\Desktop\\2005_Annual_Report.pdf");
  zip.AddFile("ReadMe.txt");

  zip.Save("Archive.zip");
}

下面是一个更新已有 zip 文件的例子;你不需要先解压文件就可以完成更新:

using (ZipFile zip = ZipFile.Read("ExistingArchive.zip"))
{
  // 1. remove an entry, given the name
  zip.RemoveEntry("README.txt");

  // 2. Update an existing entry, with content from the filesystem
  zip.UpdateItem("Portfolio.doc");

  // 3. modify the filename of an existing entry 
  // (rename it and move it to a sub directory)
  ZipEntry e = zip["Table1.jpg"];
  e.FileName = "images/Figure1.jpg";

  // 4. insert or modify the comment on the zip archive
  zip.Comment = "This zip archive was updated " + System.DateTime.ToString("G"); 

  // 5. finally, save the modified archive
  zip.Save();
}

这里有一个提取条目的示例:

using (ZipFile zip = ZipFile.Read("ExistingZipFile.zip"))
{
  foreach (ZipEntry e in zip)
  {
    e.Extract(TargetDirectory, true);  // true => overwrite existing files
  }
}

DotNetZip支持文件名中的多字节字符,ZIP加密,AES加密,流,Unicode,自解压缩档案。此外,还支持ZIP64,用于长度大于0xFFFFFFFF的文件或包含超过65535个条目的存档。

免费,开源。

获取地址:codeplex直接从windows.net下载 - CodePlex已停止服务并进行了归档。


1
Cheeso,我同意你的观点,但是我无法构建我从CodePlex下载的代码。请告诉我如何构建。如果我构建主解决方案,它会抛出很多错误。我不知道该怎么构建。 - Naruto
4
为什么要建它?这里有一个二进制文件。下载这个DLL。 - Cheeso
1
为什么要推荐第三方库,System.IO.Packaging 命名空间不足以满足需求吗?还是你最后一段详细说明了内置的 .NET 框架 Zip 功能不包括哪些内容? - Josh M.
1
啊,我明白了 - 内置的打包/压缩工具只能与“开放式打包约定”一起使用,正如卢克在另一个答案中指出的那样。谢谢。 - Josh M.
这可能是2020年最好的方法:https://learn.microsoft.com/zh-cn/dotnet/standard/io/how-to-compress-and-extract-files - Robert Harvey

46

来自 MSDN

在这个示例中,使用了 Package 类(而不是 ZipPackage)。在两者都有过工作经验的情况下,我只看到过当 zip 文件存在损坏时才会出现问题。这种损坏不一定会引发 Windows 解压程序或 WinZip 的错误,但可能会影响 Packaging 组件的处理能力。

希望对你有所帮助,也许可以为你提供解决问题的另一种方法。

using System;
using System.IO;
using System.IO.Packaging;
using System.Text;

class ExtractPackagedImages
{
    static void Main(string[] paths)
    {
        foreach (string path in paths)
        {
            using (Package package = Package.Open(
                path, FileMode.Open, FileAccess.Read))
            {
                DirectoryInfo dir = Directory.CreateDirectory(path + " Images");
                foreach (PackagePart part in package.GetParts())
                {
                    if (part.ContentType.ToLowerInvariant().StartsWith("image/"))
                    {
                        string target = Path.Combine(
                            dir.FullName, CreateFilenameFromUri(part.Uri));
                        using (Stream source = part.GetStream(
                            FileMode.Open, FileAccess.Read))
                        using (Stream destination = File.OpenWrite(target))
                        {
                            byte[] buffer = new byte[0x1000];
                            int read;
                            while ((read = source.Read(buffer, 0, buffer.Length)) > 0)
                            {
                                destination.Write(buffer, 0, read);
                            }
                        }
                        Console.WriteLine("Extracted {0}", target);
                    }
                }
            }
        }
        Console.WriteLine("Done");
    }

    private static string CreateFilenameFromUri(Uri uri)
    {
        char [] invalidChars = Path.GetInvalidFileNameChars();
        StringBuilder sb = new StringBuilder(uri.OriginalString.Length);
        foreach (char c in uri.OriginalString)
        {
            sb.Append(Array.IndexOf(invalidChars, c) < 0 ? c : '_');
        }
        return sb.ToString();
    }
}

24
看着那段代码,我简直要吐了。PackagePartCollection?PartRelationship?PackagePart?Part URIs?ToLowerInvariant?我只是想要一个ZIP文件... - Cheeso
2
是的,这似乎是OpenPackage开发人员忘记的部分。使用OpenPackage更多地涉及与虚拟组件一起工作,而不是物理表示。 - jro
16
这是唯一一个真正回答了“我如何使用X来完成Y”的问题的答案,它包含了代码和所有必要信息,不会走题并展示如何使用Z来完成Y,并且它得票最少?拜托各位了。 - a7drew
1
根据文档,Package.Openpackage.GetParts 默认使用 ZipPackage 实现,该实现需要 Luke、joshuam 和 sharptooth 提到的“打开包约定”标准。换句话说,如果您要处理办公文档,则非常适用,但对于大多数用户压缩的文件则无用。 - Trisped
https://learn.microsoft.com/en-us/dotnet/standard/io/how-to-compress-and-extract-files - Robert Harvey

31

来自"ZipPackage Class" (MSDN):

尽管通过 ZipPackage 类将 Packages 存储为 Zip 文件*,但并非所有 Zip 文件都是 ZipPackage。ZipPackage 有特殊要求,例如符合 URI 的文件(部分)名称以及定义了 Package 中包含的所有文件的 MIME 类型的“[Content_Types].xml”文件。无法使用 ZipPackage 类打开不符合 Open Packaging Conventions 标准的任意 Zip 文件。

更多细节请参见 ECMA International “Open Packaging Conventions” 标准的第 9.2 节“映射到 ZIP 存档”,http://www.ecma-international.org/publications/files/ECMA-ST/Office%20Open%20XML%20Part%202%20(DOCX).zip(342Kb)或http://www.ecma-international.org/publications/files/ECMA-ST/Office%20Open%20XML%20Part%202%20(PDF).zip(1.3Mb)

*您可以将“.zip”简单地添加到任何基于 ZipPackage 的文件(.docx、.xlsx、.pptx 等)的扩展名中,以在您喜欢的 Zip 实用程序中打开它。


13
我遇到了完全相同的问题!为了让GetParts()方法返回内容,我不得不将[Content_Types].xml文件添加到归档根目录中,并为每个包含的文件扩展名添加一个“Default”节点。一旦我这样做了(只使用Windows资源管理器),我的代码就能够读取并提取存档的内容。
有关[Content_Types].xml文件的更多信息可以在此处找到:http://msdn.microsoft.com/en-us/magazine/cc163372.aspx - 在该文章的第13个图下面有一个示例文件。
var zipFilePath = "c:\\myfile.zip"; 
var tempFolderPath = "c:\\unzipped"; 

using (Package package = ZipPackage.Open(zipFilePath, FileMode.Open, FileAccess.Read)) 
{ 
    foreach (PackagePart part in package.GetParts()) 
    { 
        var target = Path.GetFullPath(Path.Combine(tempFolderPath, part.Uri.OriginalString.TrimStart('/'))); 
        var targetDir = target.Remove(target.LastIndexOf('\\')); 

        if (!Directory.Exists(targetDir)) 
            Directory.CreateDirectory(targetDir); 

        using (Stream source = part.GetStream(FileMode.Open, FileAccess.Read)) 
        { 
            FileStream targetFile = File.OpenWrite(target);
            source.CopyTo(targetFile);
            targetFile.Close();
        } 
    } 
} 
注意:此代码在.NET 4.0中使用了Stream.CopyTo方法。

1
感谢您按照问题的要求回答问题! - shytikov

6

我同意Cheeso的观点。System.IO.Packaging在处理通用ZIP文件时很麻烦,因为它是为Office Open XML文档设计的。建议使用DotNetZipSharpZipLib


2
这基本上是此答案的另一种表述:
原来System.IO.Packaging.ZipPackage不支持PKZIP,因此当您打开一个“通用”的ZIP文件时,没有返回“部件”。该类仅支持某些特定类型的ZIP文件(请参见MSDN描述底部的注释),其中包括Windows Azure服务包SDK 1.6之前使用的文件类型。因此,如果您解压缩服务包,然后再使用Info-ZIP打包程序重新打包它,它将变得无效。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接