【问题标题】:How to check if InputStream is Gzipped?如何检查 InputStream 是否已压缩?
【发布时间】:2011-06-16 15:36:35
【问题描述】:

有什么方法可以检查 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 具有 32 位 CRC,我觉得这很令人惊讶。一个损坏的流至少应该在最后抛出一个异常。
  • 我想知道 OP 是否意味着在 IOException 发生后从流中读取的值无效......这是有道理的,因为 GZIPInputStream 构造函数会消耗流中的一些字节.
  • 值在 IOException 发生后损坏。 InputStream 来自 HttpURLConnection#getInputStream()
  • 所以一般的解决方案是创建一个 BufferedInputStream 包装原始输入流,然后调用“mark”来标记流的开始。然后围绕它包装一个 GZipInputStream 。如果没有异常,返回 GZipInputStream。如果发生异常,调用“reset”并返回BufferedInputStream。

标签: java http gzip inputstream httpurlconnection


【解决方案1】:

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

【讨论】:

  • 好吧,GZip != Zip 所以这个想法是对的,但是你想包装 GZipInputStream,而不是 ZipInputStream。
  • 没错,我会修复答案。
  • 如果条目的大小超出缓冲区大小?
  • GZIPInputStream 没有 ZipEntry 这样的东西。 GZ 流仅包含一个文件(至少,通过 Java API)。
  • 我尝试了类似的方法,但无法正常工作。我正在从 GZipInputStream 中读取 protobuf,所以我不确定是 protobuf 读取代码还是 GZip 代码,但后来标记被重置,所以我无法将流设置回开头。
【解决方案2】:

不完全是您的要求,但如果您使用 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();
}

【讨论】:

  • 已经有一段时间了,但 IIRC HttpClient 已经(或至少可以)自动解码它。
  • @BalusC 真的吗?谢谢。这是用 httpClient 3 写的,如果有的话我就错过了。
【解决方案3】:

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 spec 中明确指定。


更新:根据您压缩流源的方式:这种比率检查非常……疯狂。摆脱它。相同的长度并不一定意味着字节相同。让它总是返回 gzip 压缩的流,这样您就可以总是期待一个 gzip 压缩的流并且只需应用 GZIPInputStream 而无需进行讨厌的检查。

【讨论】:

  • 那么对方本质上是在滥用HTTP协议或者根本就不是HTTP服务。如果响应被压缩,请与服务管理员联系,以了解他们的方式。编辑:等等,你的意思是有一个代理请求的 servlet 并且你的输入来自它的响应?然后需要修复该 servlet,它也复制所有必需的 HTTP 标头。
  • 上次我检查你是否被允许通过 HTTP 传输任何类型的内容,包括 gzip,所以这并不是真正的滥用。
  • @biziclop:滥用不是关于使用 gzip 内容编码(哎呀,我什至在我最初的回答中包含了关于这个的 HTTP 规范链接),而是关于不发送强制性的 HTTP 标头(其中因此意味着 OP 违反了 HTTP 规范)。
  • 听起来您正在尝试压缩二进制内容而不是文本内容。这是真的?为什么你会尝试压缩二进制内容?在普通的HTTP服务器/客户端中,gzip一般只应用于Content-Type,以text/开头,如text/plaintext/htmltext/css等。
  • @BalusC "当存在时,它的值表示已经对实体主体应用了哪些额外的内容编码,因此必须应用哪些解码机制才能获得内容引用的媒体类型-Type header field" 这显然意味着如果我想传输 gzip 压缩的内容,我不应该(实际上我不能)设置 content-encoding 字段。只是为了说清楚:不是在 gzip 中传输编码的某些内容,而是恰好是 gzip 格式的文件。
【解决方案4】:

这不是万无一失的,但它可能是最简单的,并且不依赖任何外部数据。像所有体面的格式一样,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 file format specification

更新:我刚刚发现GZipInputStream 中还有一个名为GZIP_MAGIC 的常量包含这个值,所以如果你真的想要,你可以使用它的低两个字节。

【讨论】:

  • 我相信你需要为 PushBackInputStream 使用 2-arg 构造函数,因为默认情况下它只允许你推回 1 个字节(并且 pb.unread(signature) 推回 2 个字节)。例如new PushBackInputStream(input, 2)
  • 不错的方法,但是当流为空或只有一个字节时会出现错误。您需要检查读取的字节数,然后只写回读取的字节数。只有在成功读取两个字节时才应进行签名检查。
  • 因此应该是int nread = pb.read( signature ); if (nread > 0) pb.unread( signature, 0, nread );
  • 有没有办法在读取两个字节后重置原始流?我需要处理原始流,而不是新的 GZIPInputStream,因为似乎创建一个新的 GZIPInputStream 对象会创建一个大 10kb 的新流
  • @McLovin 你不能重置原始流(除非它支持标记/重置操作,这是不保证的),你可以重置的是你包装原始流的 pushbackinputstream。跨度>
【解决方案5】:

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

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

【讨论】:

  • 我可以确认这是可行的 - 遗憾的是,在我生命中的某个时候,我不得不使用这种方法检查流 ;-)
  • 为了其他不使用 Java 的人的利益:GZIPInputStream.GZIP_MAGIC = 35615See it yourself
【解决方案6】:

我发现这个useful example 提供了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));
}

【讨论】:

    【解决方案7】:

    这是读取可以压缩的文件的方法:

    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();
            }
        }
    }
    

    【讨论】:

      【解决方案8】:

      此函数在 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
      }
      

      【讨论】:

        【解决方案9】:

        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() );
            }
        

        【讨论】:

          【解决方案10】:

          基于@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;
              }
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2022-12-06
            • 2013-10-15
            • 2011-08-28
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2022-06-28
            相关资源
            最近更新 更多