什么是一个好的Java库来压缩/解压文件?

282

我看了JDK自带的默认Zip库和Apache压缩库,但对它们不满意有以下3个原因:

  1. 它们体积过大且API设计不好。我必须要自己写50行样板代码来处理字节数组的输出、压缩输入、文件输出流以及关闭相关流和捕获异常并移动字节缓冲区?为什么不能有一个简单的API,像这样 Zipper.unzip(InputStream zipFile, File targetDirectory, String password = null)Zipper.zip(File targetDirectory, String password = null) 只是工作就好了呢?

  2. 似乎压缩解压会破坏文件元数据且密码处理有问题。

  3. 此外,我尝试过的所有库都比我在UNIX上得到的命令行压缩工具慢2-3倍?

对于我来说(2)和(3)只是小问题,但我真的想要一个经过良好测试且只需要一行命令的接口的优秀库。


15
关于#1的原因是不是每个人都只是将文件解压到一个目录中。如果您总是使用相同的模式,为什么不编写一个实用程序类来包装其中一个类并执行所需的操作,然后只使用 - Edward Thomson
22
因为使用库比编写代码、测试代码和维护代码更容易。 - Zak
16
你的论点无效。看看Python的zip API:http://docs.python.org/3/library/zipfile。你只需要一行代码就能压缩或解压文件。API应该很好地处理常见情况,我想不出除了压缩或解压之外还有任何zip API的用例。 - pathikrit
8
将文件压缩或解压缩是压缩或解压缩流的特例。如果您的 API 不允许我将流直接写入其中,而是强制我先将流写入文件,然后再将其提供给您的 API,那么您的 API 就有问题了。@wrick - Edward Thomson
61
@EdwardThomson - 好的,那么让这个库支持文件和流。让每个人都需要实现自己的Zip工具是浪费时间 - 包括我、你、提问者以及所有会偶然发现这个问题的谷歌搜索者。就像有DRY(不要重复自己)一样,还有DROP - 不要重复别人的代码。 - ArtOfWarfare
显示剩余2条评论
9个回答

341

我知道这有点晚了,而且已经有很多答案了,但是zip4j是我使用过的最好的压缩库之一。它简单易用(没有样板代码)并且可以轻松处理密码保护文件。

import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.core.ZipFile;


public static void unzip(){
    String source = "some/compressed/file.zip";
    String destination = "some/destination/folder";
    String password = "password";

    try {
         ZipFile zipFile = new ZipFile(source);
         if (zipFile.isEncrypted()) {
            zipFile.setPassword(password);
         }
         zipFile.extractAll(destination);
    } catch (ZipException e) {
        e.printStackTrace();
    }
}

这个 Maven 依赖是:

<dependency>
    <groupId>net.lingala.zip4j</groupId>
    <artifactId>zip4j</artifactId>
    <version>1.3.2</version>
</dependency>

1
当您解压资源文件夹中的文件时,也可能会出现问题。您可以使用以下代码获取zip文件:code new File(getClass().getResource(zipFileName).getPath()); 但是,该文件将无法解压缩,并导致EOFException或MALFORMED异常。这是因为在使用maven时,必须关闭maven资源插件中的过滤。 <configuration> <nonFilteredFileExtensions> <nonFilteredFileExtension>zip</nonFilteredFileExtension> </nonFilteredFileExtensions>... - Евгений Коптюбенко
对于版本2.11.3及以上,请使用以下代码导入net.lingala.zip4j.ZipFile参考链接 - RikuPotato
无法从InputStream解压缩,只能从File解压缩。这是一个问题,例如在使用资源时。 - Daniel Hári

98

在Java 8中,使用Apache Commons-IOIOUtils,你可以这样做:

try (java.util.zip.ZipFile zipFile = new ZipFile(file)) {
  Enumeration<? extends ZipEntry> entries = zipFile.entries();
  while (entries.hasMoreElements()) {
    ZipEntry entry = entries.nextElement();
    File entryDestination = new File(outputDir,  entry.getName());
    if (entry.isDirectory()) {
        entryDestination.mkdirs();
    } else {
        entryDestination.getParentFile().mkdirs();
        try (InputStream in = zipFile.getInputStream(entry);
             OutputStream out = new FileOutputStream(entryDestination)) {
            IOUtils.copy(in, out);
        }
    }
  }
}

这仍然是一些样板代码,但只有一个非外来依赖:Commons-IO

在Java 11及更高版本中,可能有更好的选择,请参阅ZhekaKozlov的评论。


1
@VitalySazanovich,您正在提到Java 7 ZipEntry。 - Randy
4
为什么不使用IOUtils.closeQuietly(out)来关闭输出流? - Juan Mendez
2
@JuanMendez 因为如果关闭时有错误,您就无法确定文件是否已完全正确保存。但是除了常规的 close() 之外,也不会有任何损害。 - vadipp
4
这个解决方案容易受到ZipSlip漏洞的攻击(zip4j也受到影响)。 - Marcono1234
4
在Java 9及以上版本中,您不再需要使用IOUtils。只需编写“zipFile.getInputStream(entry).transferTo(outputStream)”即可。 - ZhekaKozlov
显示剩余2条评论

47

仅使用JDK提取zip文件及其所有子文件夹:

private void extractFolder(String zipFile,String extractFolder) 
{
    try
    {
        int BUFFER = 2048;
        File file = new File(zipFile);

        ZipFile zip = new ZipFile(file);
        String newPath = extractFolder;

        new File(newPath).mkdir();
        Enumeration zipFileEntries = zip.entries();

        // Process each entry
        while (zipFileEntries.hasMoreElements())
        {
            // grab a zip file entry
            ZipEntry entry = (ZipEntry) zipFileEntries.nextElement();
            String currentEntry = entry.getName();

            File destFile = new File(newPath, currentEntry);
            //destFile = new File(newPath, destFile.getName());
            File destinationParent = destFile.getParentFile();

            // create the parent directory structure if needed
            destinationParent.mkdirs();

            if (!entry.isDirectory())
            {
                BufferedInputStream is = new BufferedInputStream(zip
                .getInputStream(entry));
                int currentByte;
                // establish buffer for writing file
                byte data[] = new byte[BUFFER];

                // write the current file to disk
                FileOutputStream fos = new FileOutputStream(destFile);
                BufferedOutputStream dest = new BufferedOutputStream(fos,
                BUFFER);

                // read and write until last byte is encountered
                while ((currentByte = is.read(data, 0, BUFFER)) != -1) {
                    dest.write(data, 0, currentByte);
                }
                dest.flush();
                dest.close();
                is.close();
            }


        }
    }
    catch (Exception e) 
    {
        Log("ERROR: "+e.getMessage());
    }

}

压缩文件及其所有子文件夹:

 private void addFolderToZip(File folder, ZipOutputStream zip, String baseName) throws IOException {
    File[] files = folder.listFiles();
    for (File file : files) {
        if (file.isDirectory()) {
            addFolderToZip(file, zip, baseName);
        } else {
            String name = file.getAbsolutePath().substring(baseName.length());
            ZipEntry zipEntry = new ZipEntry(name);
            zip.putNextEntry(zipEntry);
            IOUtils.copy(new FileInputStream(file), zip);
            zip.closeEntry();
        }
    }
}

9
至少应将关闭调用放在“finally”块内,异常处理不佳。-> 我想这也是发帖者要求使用库的原因之一。 - user500592
1
这段代码不会保留文件属性和权限...如果你使用类似这样的代码来解压可运行应用程序,要准备好处理有关文件权限的奇怪错误。这让我头痛了一个星期。 - Renato
代码存在Zip Slip漏洞。 - undefined

25
另一个您可以查看的选项是从Maven中心可用的zt-zip,以及位于https://github.com/zeroturnaround/zt-zip的项目页面。
它具有标准的打包和解压功能(在流和文件系统上),还有许多辅助方法来测试存档中的文件或添加/删除条目。

这是最好的,因为它可以处理InputStream。 - Daniel Hári

22

使用zip4j完整实现文件夹/文件的压缩和解压


此依赖项添加到您的构建管理器中。或者,从此处下载最新的JAR文件,并将其添加到项目构建路径中。下面的class可以压缩和提取任何具有或不具有密码保护的文件或文件夹-

import java.io.File;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.util.Zip4jConstants;
import net.lingala.zip4j.core.ZipFile;  

public class Compressor {
    public static void zip (String targetPath, String destinationFilePath, String password) {
        try {
            ZipParameters parameters = new ZipParameters();
            parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE);
            parameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL);

            if (password.length() > 0) {
                parameters.setEncryptFiles(true);
                parameters.setEncryptionMethod(Zip4jConstants.ENC_METHOD_AES);
                parameters.setAesKeyStrength(Zip4jConstants.AES_STRENGTH_256);
                parameters.setPassword(password);
            }
                
            ZipFile zipFile = new ZipFile(destinationFilePath);
                
            File targetFile = new File(targetPath);
            if (targetFile.isFile()) {
                zipFile.addFile(targetFile, parameters);
            } else if (targetFile.isDirectory()) {
                zipFile.addFolder(targetFile, parameters);
            } else {
                //neither file nor directory
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
        
    public static void unzip(String targetZipFilePath, String destinationFolderPath, String password) {
        try {
            ZipFile zipFile = new ZipFile(targetZipFilePath);
            if (zipFile.isEncrypted()) {
                zipFile.setPassword(password);
            }
            zipFile.extractAll(destinationFolderPath);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**/ /// for test
    public static void main(String[] args) {
        
        String targetPath = "target\\file\\or\\folder\\path";
        String zipFilePath = "zip\\file\\Path"; 
        String unzippedFolderPath = "destination\\folder\\path";
        String password = "your_password"; // keep it EMPTY<""> for applying no password protection
            
        Compressor.zip(targetPath, zipFilePath, password);
        Compressor.unzip(zipFilePath, unzippedFolderPath, password);
    }/**/
}

有关更详细的使用说明,请参见此处


3
一个很好的答案和库。使用这个库提取1868个文件只需要大约15秒,而使用ZipInputStream则需要20多分钟(出于某种原因)。 - Jonty800
1
@Jonty800,如果性能差异如此之大,您可能应该重新审视您的实现。如果您不缓冲流并且每个字节都直接从设备读取/写入,那么您将获得这样的性能差异。我刚刚提取了17588个文件,总大小为1.8 GB,zip4j花费了64秒,而缓冲标准库实现只花费了39秒。话虽如此,一个天真的BufferedOutputStream实现需要大约5分钟。 - Felix S

8
一个非常好的项目是 TrueZip

TrueZIP 是一个基于 Java 的插件框架,用于虚拟文件系统(VFS),它提供了对存档文件的透明访问,就像它们只是普通目录一样。

例如(来自网站):
File file = new TFile("archive.tar.gz/README.TXT");
OutputStream out = new TFileOutputStream(file);
try {
   // Write archive entry contents here.
   ...
} finally {
   out.close();
}

这个库看起来很不错 - 但是如何简单地解压缩一个zip文件并不明显,需要给定一个zipinputstream /文件/路径。 - pathikrit
1
TrueZIP似乎不能很好地处理从流中读取的操作。 - Teo Klestrup Röijezon
5
你能做的大部分与Java 7中可以做的是相同的吗?(看看ZipFileSystemProvider)。 - peterh
1
@peterh:标准JDK ZipFileSystemProvider 是一个不错的选择。只有少数人将其视为评论。 - iuzuz

4
另一个选择是JZlib。以我的经验,它比zip4J更少涉及“文件中心”,因此如果需要处理内存块而不是文件,则可能需要查看它。

0

你看过http://commons.apache.org/vfs/了吗?它声称可以为你简化很多事情。但我从未在项目中使用过它。

除了JDK或Apache Compression,我也不知道其他Java本地压缩库。

我记得有一次我们从Apache Ant中剥离了一些功能-他们内置了许多压缩/解压缩工具。

使用VFS的示例代码如下:

File zipFile = ...;
File outputDir = ...;
FileSystemManager fsm = VFS.getManager();
URI zip = zipFile.toURI();
FileObject packFileObject = fsm.resolveFile(packLocation.toString());
FileObject to = fsm.toFileObject(destDir);
FileObject zipFS;
try {
    zipFS = fsm.createFileSystem(packFileObject);
    fsm.toFileObject(outputDir).copyFrom(zipFS, new AllFileSelector());
} finally {
    zipFS.close();
}

1
看起来VFS中对zip文件的支持还是相当有限的:http://commons.apache.org/vfs/filesystems.html - T.J. Crowder

0

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