如何使用InputStream从ZIP文件中读取文件?

52

我需要从一个ZIP压缩包中获取文件内容(只有一个文件,我知道它的名字),并使用SFTP协议。我唯一拥有的是ZIP的InputStream。大多数示例都展示了如何使用以下语句获取内容:


ZipFile zipFile = new ZipFile("location");

但是,正如我所说,我在本地机器上没有ZIP文件,也不想下载它。使用InputStream是否足以进行读取?

更新:这就是我的做法:

import java.util.zip.ZipInputStream;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;

public class SFTP {


    public static void main(String[] args) {

        String SFTPHOST = "host";
        int SFTPPORT = 3232;
        String SFTPUSER = "user";
        String SFTPPASS = "mypass";
        String SFTPWORKINGDIR = "/dir/work";
        Session session = null;
        Channel channel = null;
        ChannelSftp channelSftp = null;
        try {
            JSch jsch = new JSch();
            session = jsch.getSession(SFTPUSER, SFTPHOST, SFTPPORT);
            session.setPassword(SFTPPASS);
            java.util.Properties config = new java.util.Properties();
            config.put("StrictHostKeyChecking", "no");
            session.setConfig(config);
            session.connect();
            channel = session.openChannel("sftp");
            channel.connect();
            channelSftp = (ChannelSftp) channel;
            channelSftp.cd(SFTPWORKINGDIR);
            ZipInputStream stream = new ZipInputStream(channelSftp.get("file.zip"));
            ZipEntry entry = zipStream.getNextEntry();
            System.out.println(entry.getName); //Yes, I got its name, now I need to get content
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            session.disconnect();
            channelSftp.disconnect();
            channel.disconnect();
        }


    }
}

如果我只需要读取zip文件中的txt文件内容,那么我真的需要编写一个新的zip文件吗? - Tony
没有理由不起作用,你只需要获取所有的ZIPEntries并将它们从流中保存。 - Kenneth Clark
7个回答

43

以下是一个简单的示例,说明如何提取ZIP文件,您需要检查文件是否为目录。但这是最简单的。

您错过的步骤是读取输入流并将内容写入缓冲区,然后将其写入输出流。

// Expands the zip file passed as argument 1, into the
// directory provided in argument 2
public static void main(String args[]) throws Exception
{
    if(args.length != 2)
    {
        System.err.println("zipreader zipfile outputdir");
        return;
    }

    // create a buffer to improve copy performance later.
    byte[] buffer = new byte[2048];

    // open the zip file stream
    InputStream theFile = new FileInputStream(args[0]);
    ZipInputStream stream = new ZipInputStream(theFile);
    String outdir = args[1];

    try
    {

        // now iterate through each item in the stream. The get next
        // entry call will return a ZipEntry for each file in the
        // stream
        ZipEntry entry;
        while((entry = stream.getNextEntry())!=null)
        {
            String s = String.format("Entry: %s len %d added %TD",
                            entry.getName(), entry.getSize(),
                            new Date(entry.getTime()));
            System.out.println(s);

            // Once we get the entry from the stream, the stream is
            // positioned read to read the raw data, and we keep
            // reading until read returns 0 or less.
            String outpath = outdir + "/" + entry.getName();
            FileOutputStream output = null;
            try
            {
                output = new FileOutputStream(outpath);
                int len = 0;
                while ((len = stream.read(buffer)) > 0)
                {
                    output.write(buffer, 0, len);
                }
            }
            finally
            {
                // we must always close the output file
                if(output!=null) output.close();
            }
        }
    }
    finally
    {
        // we must always close the zip file.
        stream.close();
    }
}

代码片段来自以下网站:

http://www.thecoderscorner.com/team-blog/java-and-jvm/12-reading-a-zip-file-from-java-using-zipinputstream#.U4RAxYamixR


35

嗯,我已经做过这个了:

 zipStream = new ZipInputStream(channelSftp.get("Port_Increment_201405261400_2251.zip"));
 zipStream.getNextEntry();

 sc = new Scanner(zipStream);
 while (sc.hasNextLine()) {
     System.out.println(sc.nextLine());
 }

它帮助我阅读ZIP文件的内容而无需将内容写入另一个文件。


1
显然,文件内容仍然被下载。您只需要不将其写入(临时)文件即可。 - Martin Prikryl
2
我认为@KennethClark的解决方案更好。它适用于文本和二进制文件,而你的只适用于文本文件,个人认为。请注意,虽然他将提取的内容存储到文件中,但这只是将内容复制到另一个流的示例。它不一定是文件流,也可以是内存流,或者根本不需要是流。 - Martin Prikryl
顺便提一下,存档文件中的文本文件大小约为1 MB(111589行文本)。而且读取(使用 while (sc.hasNextLine()) 语句而不带有 sysout)需要38秒。这正常吗? - Tony
尝试一下 @KennethClark 的解决方案。我可以想象 Scanner 可能会很慢。 - Martin Prikryl

20

ZipInputStream是一个InputStream,在每次调用getNextEntry()后会提供每个条目的内容。需要特别注意不要关闭从中读取内容的流,因为它与ZIP流相同。

public void readZipStream(InputStream in) throws IOException {
    ZipInputStream zipIn = new ZipInputStream(in);
    ZipEntry entry;
    while ((entry = zipIn.getNextEntry()) != null) {
        System.out.println(entry.getName());
        readContents(zipIn);
        zipIn.closeEntry();
    }
}

private void readContents(InputStream contentsIn) throws IOException {
    byte contents[] = new byte[4096];
    int direct;
    while ((direct = contentsIn.read(contents, 0, contents.length)) >= 0) {
        System.out.println("Read " + direct + "bytes content.");
    }
}

当将阅读内容委托给其他逻辑时,有必要使用 FilterInputStreamZipInputStream 包装起来,以便仅关闭条目而不是整个流:

TBD
public void readZipStream(InputStream in) throws IOException {
    ZipInputStream zipIn = new ZipInputStream(in);
    ZipEntry entry;
    while ((entry = zipIn.getNextEntry()) != null) {
        System.out.println(entry.getName());

        readContents(new FilterInputStream(zipIn) {
            @Override
            public void close() throws IOException {
                zipIn.closeEntry();
            }
        });
    }
}

1
包装FilterInputStream非常有帮助。 - Ng Zhong Qin

5

OP接近了答案,只需要读取字节。调用getNextEntry函数会将流定位到条目数据的开头(文档)。如果这是我们想要的条目(或唯一的条目),那么InputStream就处于正确的位置。我们只需要读取该条目的解压缩字节即可。

byte[] bytes = new byte[(int) entry.getSize()];
int i = 0;
while (i < bytes.length) {
    // .read doesn't always fill the buffer we give it.
    // Keep calling it until we get all the bytes for this entry.
    i += zipStream.read(bytes, i, bytes.length - i);
}

所以,如果这些字节确实是文本,那么我们可以将这些字节解码为字符串。我只是假设使用utf8编码。
new String(bytes, "utf8")

旁注:我个人使用Apache Commons-IO的IOUtils来减少这种较低级别的内容。ZipInputStream.read的文档似乎暗示read将停止在当前zip条目的末尾。如果是这样,那么使用IOUtils读取当前文本条目只需要一行。

String text = IOUtils.toString(zipStream)

我可以通过实验确认,读取操作会在下一个条目处停止。另外请注意,未指定字符集的IOUtils.toString已被弃用。https://commons.apache.org/proper/commons-io/javadocs/api-release/index.html?org/apache/commons/io/input/package-summary.html - julaine

1
将压缩文件(zip)解压到指定目录并保留文件结构。 注意:此代码依赖于“org.apache.commons.io.IOUtils”,但您可以使用自己的自定义“read-stream”代码替换它。
public static void unzipDirectory(File archiveFile, File destinationDir) throws IOException
{
  Path destPath = destinationDir.toPath();
  try (ZipInputStream zis = new ZipInputStream(new FileInputStream(archiveFile)))
  {
    ZipEntry zipEntry;
    while ((zipEntry = zis.getNextEntry()) != null)
    {
      Path resolvedPath = destPath.resolve(zipEntry.getName()).normalize();
      if (!resolvedPath.startsWith(destPath))
      {
        throw new IOException("The requested zip-entry '" + zipEntry.getName() + "' does not belong to the requested destination");
      }
      if (zipEntry.isDirectory())
      {
        Files.createDirectories(resolvedPath);
      } else
      {
        if(!Files.isDirectory(resolvedPath.getParent()))
        {
          Files.createDirectories(resolvedPath.getParent());
        }
        try (FileOutputStream outStream = new FileOutputStream(resolvedPath.toFile()))
        {
          IOUtils.copy(zis, outStream);
        }
      }
    }
  }
}

0

这里提供了一种更通用的解决方案,可以使用BiConsumer处理zip输入流。这几乎是与haui使用的相同解决方案。

private void readZip(InputStream is, BiConsumer<ZipEntry,InputStream> consumer) throws IOException {
    try (ZipInputStream zipFile = new ZipInputStream(is);) {
        ZipEntry entry;
        while((entry = zipFile.getNextEntry()) != null){
            consumer.accept(entry, new FilterInputStream(zipFile) {
                @Override
                public void close() throws IOException {
                    zipFile.closeEntry();
                }
            });
        }
    }
}

你可以通过简单调用来使用它

readZip(<some inputstream>, (entry, is) -> {
    /* don't forget to close this stream after processing. */
    is.read() // ... <- to read each entry
});

0
如果您的ZIP内容只包含一个文件(例如,HTTP响应的压缩内容),您可以使用Kotlin读取文本内容,方法如下:

@Throws(IOException::class)
fun InputStream.readZippedContent() = ZipInputStream(this).use { stream ->
     stream.nextEntry?.let { stream.bufferedReader().readText() } ?: String()
}

这个扩展函数可以解压缩 ZIP 文件的第一个条目,并将内容作为纯文本读取。

用法:

val inputStream: InputStream = ... // your zipped InputStream
val textContent = inputStream.readZippedContent()

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