如何检查InputStream是否为Gzipped?

60

有没有办法检查InputStream是否已经被gzip压缩过了?以下是代码:

public static InputStream decompressStream(InputStream input) {
    try {
        GZIPInputStream gs = new GZIPInputStream(input);
        return gs;
    } catch (IOException e) {
        logger.info("Input stream not in the GZIP format, using standard format");
        return input;
    }
}

我尝试过这种方法,但它并没有像预期的那样工作-从流中读取的值无效。 编辑: 添加了我用于压缩数据的方法:

public static byte[] compress(byte[] content) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try {
        GZIPOutputStream gs = new GZIPOutputStream(baos);
        gs.write(content);
        gs.close();
    } catch (IOException e) {
        logger.error("Fatal error occured while compressing data");
        throw new RuntimeException(e);
    }
    double ratio = (1.0f * content.length / baos.size());
    if (ratio > 1) {
        logger.info("Compression ratio equals " + ratio);
        return baos.toByteArray();
    }
    logger.info("Compression not needed");
    return content;

}

InputStream 是从哪里来的?是从 URLConnection#getInputStream() 来的吗?在像 HTTP 这样稍微正式的协议中,最终用户应该已经以某种方式被告知内容是经过 gzip 压缩的。 - BalusC
考虑到GZIP具有32位CRC,我觉得这很令人惊讶。至少在结尾处,损坏的流应该抛出异常。 - Peter Lawrey
我在想 OP 是否意味着在 IOException 发生后从流中读取的值无效... 这是有道理的,因为 GZIPInputStream 构造函数已经消耗了流中的一些字节。 - Eric Giguere
发生IOException后,值被损坏。 InputStream来自HttpURLConnection#getInputStream()。 - voo
没错,这是因为GZipInputStream从原始输入流中读取字节。因此,您需要像下面的答案中所示缓冲输入流。 - Eric Giguere
1
因此,一般的解决方案是创建一个包装原始输入流的BufferedInputStream,然后调用“mark”来标记流的开头。然后在其周围包装一个GZipInputStream。如果没有异常发生,则返回GZipInputStream。如果发生异常,请调用“reset”并返回BufferedInputStream。 - Eric Giguere
10个回答

80

虽然并非万无一失,但这可能是最简单的方法,并且不依赖于任何外部数据。与所有良好格式一样,GZip也以一个魔数开头,可以快速检查而无需读取整个流。

public static InputStream decompressStream(InputStream input) {
     PushbackInputStream pb = new PushbackInputStream( input, 2 ); //we need a pushbackstream to look ahead
     byte [] signature = new byte[2];
     int len = pb.read( signature ); //read the signature
     pb.unread( signature, 0, len ); //push back the signature to the stream
     if( signature[ 0 ] == (byte) 0x1f && signature[ 1 ] == (byte) 0x8b ) //check if matches standard gzip magic number
       return new GZIPInputStream( pb );
     else 
       return pb;
}

(魔数来源:GZip文件格式规范

更新:我刚刚发现,在GZipInputStream中也有一个名为GZIP_MAGIC的常量,其中包含此值,因此如果你确实想要,可以使用其低两个字节。


2
我认为你需要使用PushBackInputStream的2个参数构造函数,因为默认情况下它只允许您推回1个字节(而pb.unread(signature)会推回2个字节)。例如:new PushBackInputStream(input, 2) - overthink
4
不错的方法,但是当流为空或只有一个字节时存在错误。您需要检查读取的字节数,并仅写回那些被读取的字节。如果成功读取了两个字节,则应该仅在此时进行签名检查。 - Alexander Torstling
1
因此,应该是 int nread = pb.read( signature ); if (nread > 0) pb.unread( signature, 0, nread ); - 18446744073709551615
1
@McLovin 你无法重置原始流(除非它支持标记/重置操作,这并不保证),你只能重置包装原始流的PushbackInputStream。 - biziclop
1
使用GZIP_MAGIC和Guava:如果(len == 2 && GZIPInputStream.GZIP_MAGIC == Ints.fromBytes((byte) 0, (byte) 0, signature[1], signature[0]))。 - blacelle
显示剩余6条评论

40

InputStream 来自于 HttpURLConnection#getInputStream() 方法

在这种情况下,您需要检查 HTTP 响应头的 Content-Encoding 是否等于 gzip。

URLConnection connection = url.openConnection();
InputStream input = connection.getInputStream();

if ("gzip".equals(connection.getContentEncoding())) {
    input = new GZIPInputStream(input);
}

// ...

所有这些都在HTTP规范中明确说明。


更新: 根据您压缩流源的方式:这个比率检查非常...疯狂。抛弃它。相同长度并不一定意味着字节相同。让它始终返回gzip流,以便您始终可以期望gzip流,并且只需应用GZIPInputStream而不必进行恶心的检查。


1
另一方面,实质上是滥用HTTP协议,或者根本不是HTTP服务。如果响应已经gzip压缩,请联系服务管理员了解如何处理。编辑:等等,您的意思是有一个servlet代理请求,并且您的输入来自其响应吗?那么该servlet需要修复以便它也复制所有必需的HTTP头。 - BalusC
1
上次我检查时,你可以通过HTTP传输任何类型的内容,包括gzip,所以这并不是滥用。 - biziclop
1
@biziclop:这种滥用并不是关于使用gzip内容编码(我甚至在我的初始答案中包含了HTTP规范链接),而是关于没有发送必需的HTTP标头(这意味着OP正在违反HTTP规范)。 - BalusC
1
听起来你正在尝试压缩二进制内容而不是文本内容。这是真的吗?为什么你要尝试压缩二进制内容呢?在正常的HTTP服务器/客户端中,gzip通常只应用于以text/开头的Content-Type,如text/plaintext/htmltext/css等。 - BalusC
1
@BalusC:“当存在时,它的值表示对实体主体应用了哪些附加内容编码,因此必须应用哪些解码机制才能获取由Content-Type头字段引用的媒体类型。”这意味着如果我想传输gzip压缩内容,则不应该(确实不允许)设置content-encoding字段。仅为明确起见:不是一些在gzip中进行内容传输,而是一个碰巧是gzip格式的文件。 - biziclop
显示剩余3条评论

27
我发现了这个有用的例子,它提供了一个干净的 isCompressed() 实现:

这里 是链接。

/*
 * Determines if a byte array is compressed. The java.util.zip GZip
 * implementation does not expose the GZip header so it is difficult to determine
 * if a string is compressed.
 * 
 * @param bytes an array of bytes
 * @return true if the array is compressed or false otherwise
 * @throws java.io.IOException if the byte array couldn't be read
 */
 public boolean isCompressed(byte[] bytes)
 {
      if ((bytes == null) || (bytes.length < 2))
      {
           return false;
      }
      else
      {
            return ((bytes[0] == (byte) (GZIPInputStream.GZIP_MAGIC)) && (bytes[1] == (byte) (GZIPInputStream.GZIP_MAGIC >> 8)));
      }
 }

我已经成功测试过了:

@Test
public void testIsCompressed() {
    assertFalse(util.isCompressed(originalBytes));
    assertTrue(util.isCompressed(compressed));
}

11

我认为这是检查字节数组是否为gzip格式的最简单方法,它不依赖于任何HTTP实体或mime类型支持。

public static boolean isGzipStream(byte[] bytes) {
      int head = ((int) bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00);
      return (GZIPInputStream.GZIP_MAGIC == head);
}

我可以确认这个方法是可行的 - 不幸的是,在我的生活中曾经有过使用这种方法来检查流的经历;-) - Konrad 'ktoso' Malawski
为了那些不使用Java的人的好处:GZIPInputStream.GZIP_MAGIC = 35615点击查看 - oleksii

5

在@biziclop的回答基础上,这个版本使用了GZIP_MAGIC头,并且对于空或单字节数据流也是安全的。

public static InputStream maybeDecompress(InputStream input) {
    final PushbackInputStream pb = new PushbackInputStream(input, 2);

    int header = pb.read();
    if(header == -1) {
        return pb;
    }

    int b = pb.read();
    if(b == -1) {
        pb.unread(header);
        return pb;
    }

    pb.unread(new byte[]{(byte)header, (byte)b});

    header = (b << 8) | header;

    if(header == GZIPInputStream.GZIP_MAGIC) {
        return new GZIPInputStream(pb);
    } else {
        return pb;
    }
}

4

这个函数在Java中工作得非常好:

public static boolean isGZipped(File f) {   
    val raf = new RandomAccessFile(file, "r")
    return GZIPInputStream.GZIP_MAGIC == (raf.read() & 0xff | ((raf.read() << 8) & 0xff00))
}

在 Scala 中:
def isGZip(file:File): Boolean = {
   int gzip = 0
   RandomAccessFile raf = new RandomAccessFile(f, "r")
   gzip = raf.read() & 0xff | ((raf.read() << 8) & 0xff00)
   raf.close()
   return gzip == GZIPInputStream.GZIP_MAGIC
}

1

虽然不完全符合您的要求,但如果您正在使用HttpClient,则可能是一种替代方法:

private static InputStream getInputStream(HttpEntity entity) throws IOException {
  Header encoding = entity.getContentEncoding(); 
  if (encoding != null) {
     if (encoding.getValue().equals("gzip") || encoding.getValue().equals("zip") ||      encoding.getValue().equals("application/x-gzip-compressed")) {
        return new GZIPInputStream(entity.getContent());
     }
  }
  return entity.getContent();
}

已经有一段时间了,但我记得 HttpClient 已经(或者至少可以)自动解码它。 - BalusC
@BalusC 真的吗?谢谢。这是使用httpClient 3编写的,如果在其中则我错过了它。 - Richard H

1

SimpleMagic是一个用于解析内容类型的Java库:

<!-- pom.xml -->
    <dependency>
        <groupId>com.j256.simplemagic</groupId>
        <artifactId>simplemagic</artifactId>
        <version>1.8</version>
    </dependency>

import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.j256.simplemagic.ContentType;
// ...

public class SimpleMagicSmokeTest {

    private final static Logger log = LoggerFactory.getLogger(SimpleMagicSmokeTest.class);

    @Test
    public void smokeTestSimpleMagic() throws IOException {
        ContentInfoUtil util = new ContentInfoUtil();
        InputStream possibleGzipInputStream = getGzipInputStream();
        ContentInfo info = util.findMatch(possibleGzipInputStream);

        log.info( info.toString() );
        assertEquals( ContentType.GZIP, info.getContentType() );
    }

1

将原始流包装在BufferedInputStream中,然后将其包装在GZipInputStream中。 接下来尝试提取ZipEntry。如果成功,则是zip文件。然后您可以在检查后使用BufferedInputStream中的“mark”和“reset”返回流的初始位置。


好的,GZip!= Zip,所以想法是正确的,但您想要包装GZipInputStream,而不是ZipInputStream。 - Eric Giguere
没错,我会修正答案。 - Amir Afghani
如果条目的大小超出了缓冲区的大小怎么办? - Lawrence Dol
在GZIPInputStream中不存在ZipEntry这样的东西。通过Java API,GZ流只包含一个文件。 - GreenGiant
我尝试过类似的东西,但无法使其工作。我正在从GZipInputStream中读取protobufs,因此我不确定是protobuf读取代码还是GZip代码出了问题,但标记在之后被重置,所以我无法将流设置回开头。 - kybernetikos

0

以下是如何读取一个可能被压缩的文件:

private void read(final File file)
        throws IOException {
    InputStream stream = null;
    try (final InputStream inputStream = new FileInputStream(file);
            final BufferedInputStream bInputStream = new BufferedInputStream(inputStream);) {
        bInputStream.mark(1024);
        try {
            stream = new GZIPInputStream(bInputStream);
        } catch (final ZipException e) {
            // not gzipped OR not supported zip format
            bInputStream.reset();
            stream = bInputStream;
        }
        // USE STREAM HERE
    } finally {
        if (stream != null) {
            stream.close();
        }
    }
}

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