在Java中将ZIP文件解压缩到内存中

30

我正在下载包含XML的压缩文件,并希望在处理它们之前避免将zip文件写入磁盘,因为需要低延迟。然而,java.util.zip 对我来说不够用。没有办法说“这里是一个zip文件的字节数组,请使用它”,而不将其转换为流,而ZipInputStream 不可靠,因为它会扫描条目头(请参见下面讨论原因为什么不可靠)。

我尚未能够访问我将要处理的zip文件,因此不知道是否能通过ZipInputStream 处理它们,并且我需要找到适用于任何有效ZIP文件的解决方案,因为一旦进入生产,如果失败的话,代价将非常高。

假设ZipInputStream不起作用,在没有条目头的情况下,我该怎么解决这个问题? 我使用维基百科的定义,其中包括有关如何正确解压缩zip文件的评论(如下所引用),作为标准。

编辑

Apache Commons Zip库对使用Stream(包括他们的解决方案和Java的解决方案)存在的一些问题进行了良好的阐述。我还要补充一点,来自维基百科和个人经验,条目头中的大小和crc字段可能没有填写(我有一些这些字段为-1的文件)。感谢centic提供此链接。
此外,让我引用一下维基百科上的内容:
正确读取zip存档的工具必须扫描各种字段、zip中央目录的签名。它们不得扫描条目,因为只有目录指定文件块从哪里开始。扫描可能会导致误报,因为该格式不禁止在块之间或包含这些签名的未压缩流中存在其他数据。
请注意,ZipInputStream扫描条目而不是中央目录,这就是它的问题所在。
最后编辑。
如果有人感兴趣,此脚本可用于生成一个有效的ZIP文件,并且无法由现有的ZIP文件中的ZipInputStream读取。因此,作为这个已经关闭的问题的最终编辑,我需要一个可以读取由该脚本生成的文件的库。

9
实际上,我还没有遇到过ZipInputStream无法读取的压缩文件。也许这种情况会发生,但我认为这可能是很少见的。我唯一注意到的真正问题是,对单个ZipInputStream实例的不正确同步访问可能会触发本地代码中的并发异常,进而迅速导致整个JVM崩溃。需要注意的是,Java使用这些相同的类来从JAR文件中加载类,因此在适当使用时,它们应该是相当健壮的。 - aroth
4
你在哪里看到表明输入数据是可选的格式?请注意,一些工具可以处理文件并不意味着该文件是有效的。 - Jon Skeet
2
@DanielC.Sobral:我会编辑我的回答来解决这个问题。听起来你在这里提出了不可能的要求。 - Jon Skeet
4
这个问题绝对值得重新开启讨论。它是一个完全有效的问题。 - Richard J. Ross III
2
显然,自Apache Commons Compress 1.5版本起发生了变化。我现在能够读取以前无法读取的文件。从1.5版本开始,ZipArchiveInputStream将尝试读取存档直到包括“中央目录结束”记录为止。 - Jasper Krijgsman
显示剩余27条评论
4个回答

24

编辑:另一个建议...

从Apache Commons实现中查看ZipFile,看起来很容易为您的项目有效地分叉。创建一个包装器,包装您的字节数组,其中有所有所需的RandomAccessFile API的部件(我认为没有很多)。您已经表明您更喜欢ZipFile的接口,那就选择它吧。

我们不了解您的项目是否会引起任何法律问题,即使您提供了详细信息,我也怀疑这里没有人能够给出好的法律建议,但我认为只需要一两个小时就可以使这个解决方案运作起来,并且我相信您会对此有合理的信心。


编辑:这可能是一个稍微更有成效的答案...

如果您担心条目不连续,但又不想自己处理所有压缩方面的问题,您可以考虑一种选项,其中您有效地重写数据。创建一个新的ByteArrayOutputStream,并在结尾处读取中央目录。对于中央目录中的每个条目,在输出流中以您认为ZipInputStream将满意的格式写出一个条目(标头+数据)。然后编写一个新的中央目录 - 如果您希望替换有效,则可能需要从头开始进行此操作,但如果您使用的是您知道实际上不会读取中央目录的代码,则可以提供原始代码。只要它以正确的签名开头,那就足够好了:)

完成后,将ByteArrayOutputStream转换为新的byte[],将其包装在ByteArrayInputStream中,然后将其传递给ZipInputStreamZipArchiveInputStream

根据您的目的,您可能甚至不需要做那么多工作 - 您可以通过创建一个“迷你”zip文件,每次从目录中读取一个条目来提取每个文件。
这确实涉及了解zip文件格式,但并不完全需要 - 只需了解其框架即可。这不是像完全使用现有API那样快速简便的修复方法,但也不应该花费太长时间。它不能保证能够读取所有无效文件(怎么可能呢?),但它将保护您免受您特别担心的“条目之间的数据”问题的影响。希望这至少是一个有用的想法...

没有一种方式可以说“这是一个zip文件的字节数组,请使用它”

是有的:

byte[] data = ...;
ByteArrayInputStream byteStream = new ByteArrayInputStream(data);
ZipInputStream zipStream = new ZipInputStream(byteStream);

这就涉及到ZipInputStream是否能够处理您提供的所有zip文件的问题,但我不会那么快地放弃它。
当然,还有其他可用的API。例如,您可以查看Apache Commons Compress。尽管ZipFile需要一个文件,但ZipArchiveInputStream不需要 - 所以您可以再次使用ByteArrayInputStream。编辑:看起来ZipArchiveStream也不从中央目录中读取。我希望它会使用markSupported事先进行检查,但似乎并没有...
编辑:在问题的评论中,我问过您在哪里读到zip文件不必包含条目数据。您引用了维基百科:
"正确读取zip档案的工具必须扫描各个字段,即zip中央目录的签名。它们不能扫描条目,因为只有目录指定文件块的起始位置。扫描可能会导致误报,因为该格式不禁止在块之间或包含这些签名的未压缩流中存在其他数据。"
“这并不意味着条目数据是可选的。它是说可能有额外的数据存在于尴尬的地方,而不是条目可能完全不存在。它基本上是说不能假设条目是连续的。我可以很乐意承认ZipInputStream可能没有读取文件末尾处的中央目录,但找到处理不存在条目数据的代码与找到处理中央目录的代码并不相同。”
“你随后写道:我可能进一步补充说,无论zip文件是否有效都不是我的关注点。我的关注点是使用它。”
“...这表明你想要处理无效zip文件的代码。再加上这个:我还没有访问我将要处理的zip文件,所以我不知道是否能通过流处理它们。”
这意味着你需要编写处理zip文件的代码,即使它们以无法预测的方式无效。如果我给你1000个随机字节,根本不是zip文件,你会怎么做呢?
基本上,在能够确定问题更紧密之前,甚至说一个特定的库是否是有效的解决方案都是不可行的。从各个地方收集一组zip文件是合理的,这些文件可能无效,但是这些无效方式是被充分理解的,可以说“我必须能够支持所有这些文件”。后来,如果发现这样做不够好,您可能需要做一些工作。但是,能够支持任何东西,无论多么损坏,都不是一个有效的要求。

我没有看到你的编辑。老实说,我不知道如何更紧密地定位问题。我想要一个能够正确解压所有有效ZIP文件的东西——而ZIS则不能。是的,如果它最终被证明是无效的ZIP文件,我仍然必须处理它,但如果我事先不给自己设限,那么我将处于更好的位置。 - Daniel C. Sobral
@DanielC.Sobral:对的,所以根据你解释的一种方式,ZipInputStream无法处理在块之间具有额外数据的有效文件,而我的编辑建议您处理它,您认为还有其他类型的有效文件不适用于我提出的建议吗? - Jon Skeet
不,你提出的方法应该是可行的。然而,我想问一下是否有可以为我完成这个任务(它就在那里,第三段),原因有两个:我没有太多时间去做这件事,而且我怀疑自己亲手完成可能会让致命错误进入生产环境的机会比ZIS无法处理文件的机会更高。如果TrueZIP能够正常工作,那么它是一个更好的答案。在问题关闭后,有人在Twitter上建议使用Common VFS + ZipFile,这也是一个相当不错的主意。 - Daniel C. Sobral
@DanielC.Sobral:我提出了另一个可能更简单的选项-请参见编辑(在顶部)。 TrueZIP的一个缺点是它需要Java 7-您是否正在使用Java 7?如果对您有用,请使用它-我尝试阅读文档并很快就迷失了。 - Jon Skeet

2
"TrueZIP库提供了一种成熟的zip实现方案。它还包括文件系统抽象,甚至可以用于HTTP。例如:"
Path path = new TPath(new URI("http://acme.com/download/everything.zip/entry.xml"));
try (InputStream in = Files.newInputStream(path)) {
    // Read archive entry contents here.
    ...
}

因此,如果您只对特定条目感兴趣,它将仅下载这些条目,节省带宽和时间。而且您不必编写下载代码。
另请参见 http://truezip.java.net/faq.html#http

遗憾的是,虽然这可能是一个有效的答案(我稍后会查看那个常见问题解答),但它并没有真正帮助到我,因为我的所有I/O都是异步的。所以,除非它提供了一个异步I/O接口来替换我的接口,否则我不能使用它。不过,如果它能够起作用,我仍然会接受这个答案。 - Daniel C. Sobral

2
我建议使用Apache库commons-compress,可以参考http://commons.apache.org/compress/。它支持通过流读取Zip文件,并提供详尽的文档,请参考http://commons.apache.org/compress/zip.html获取详细信息。该文档还介绍了Zip格式固有的一些限制。以下是示例代码:
ZipArchiveInputStream zip =
    new ZipArchiveInputStream(inputStream);
try {
    ZipArchiveEntry entry = zip.getNextZipEntry();
    while(entry != null) {
        assertEquals("README", entry.getName());
        ...
        entry = zip.getNextZipEntry();
    }
} finally {
    zip.close();
}

感谢您提供的Apache Commons链接,因为它正确地表达了使用“流”作为Zip文件的问题。这不是Zip格式本身的固有限制,而是使用流处理Zip格式的限制,这正是我需要克服的限制。 - Daniel C. Sobral
我认为问题实际上是由于zip格式的定义方式造成的,即只将一些信息存储在文件末尾,使得在没有加载整个文件的情况下准确处理复杂的zip文件变得不可能。Apache Compress采用了妥协方案,即提供了流式接口,但牺牲了一些在zip文件中很少使用的功能。因此,如果您知道zip文件的来源,可以确定这样的zip文件不会出现,并且使用Apache commons也没问题。 - centic
我可以先加载整个文件;但是如果我事先知道ZIP文件的来源,我就不能做到 -- 如果我能的话,我不会在这里问这个问题,也不会为它提供赏金。 - Daniel C. Sobral

2
这个问题听起来类似于如何在内存中创建目录?伪文件系统/虚拟目录。基本上,我的建议是使用一个更通用的解决方案-内存中的虚拟文件系统(我不是指像Linux的ramfs/tmpfs这样的操作系统级别)。
一个例子是使用Java 7 NIO API,它现在提供了通过FileSystemProvider实现文件系统的SPI。看起来ShrinkWrap文件系统实现了这个SPI。
一个更容易访问的选择是使用Apache Commons VFS的RAM文件系统:它只需要Java 5。如果你需要与Java 5和6兼容,这可能是你最好的选择。

我最初了解Java中内存文件系统是从这篇文章中,除了提出了像Commons VFS和JBoss Microcontainer这样的解决方案外,还给出了NetBeans IDE的一个很好的使用示例。

虽然内存虚拟文件系统是避免操作系统级文件系统(带有相关性能优势)的一个不错的通用解决方案,但它可能会受到其他缺点的影响,而更专业的解决方案可能可以解决这些缺点。例如,我不确定在同时从多个线程中使用该文件系统时,会发生什么情况。只要您不访问同一文件,它可能会正常工作,或者您可能需要创建单独的文件系统(这可能会对资源使用造成限制)。


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