Java GZip 在压缩文件和再次解压时有细微差别

Java GZip makes small differences when compressing file and decompressing it again

经过一周的努力,我设计了一个二进制文件格式,并为它做了一个Java reader。这只是一个实验,效果很好,除非我使用 GZip 压缩功能。

我把我的二进制类型称为 MBDF(最小二进制数据库格式),它可以存储 8 种不同的类型:

我用这个数据作为测试数据:

COMPOUND {
    float1: FLOAT_32 3.3
    bool2: BOOLEAN true
    float2: FLOAT_64 3.3
    int1: INTEGER 3
    compound1: COMPOUND {
        xml: STRING "two length compound"
        int: INTEGER 23
    }
    string1: STRING "Hello world!"
    string2: STRING "3"
    arr1: ARRAY [
        STRING "Hello world!"
        INTEGER 3
        STRING "3"
        FLOAT_32 3.29
        FLOAT_64 249.2992
        BOOLEAN true
        COMPOUND {
            str: STRING "one length compound"
        }
        BOOLEAN false
        NULL null
    ]
    bool1: BOOLEAN false
    null1: NULL null
}

化合物中的 xml 键很重要!!

我用这个 java 代码从中创建了一个文件:

MBDFFile.writeMBDFToFile(
    "/Users/<anonymous>/Documents/Java/MBDF/resources/file.mbdf", 
    b.makeMBDF(false)
);

这里,变量b是一个MBDFBinary对象,包含上面给出的所有数据。它使用 makeMBDF 函数生成 ISO 8859-1 编码的字符串,如果给定的布尔值是 true,它会使用 GZip 压缩字符串。然后,写入时,在文件的开头添加一个额外的信息字符,包含有关如何读回它的信息。

然后,写入文件后,我将其读回java并解析它

MBDF mbdf = MBDFFile.readMBDFFromFile("/Users/<anonymous>/Documents/Java/MBDF/resources/file.mbdf");
System.out.println(mbdf.getBinaryObject().parse());

这会打印出上面提到的信息。

然后我尝试使用压缩:

MBDFFile.writeMBDFToFile(
    "/Users/<anonymous>/Documents/Java/MBDF/resources/file.mbdf", 
    b.makeMBDF(true)
);

我读回它的方式与读回未压缩文件的方式完全相同,应该可以。它打印此信息:

COMPOUND {
    float1: FLOAT_32 3.3
    bool2: BOOLEAN true
    float2: FLOAT_64 3.3
    int1: INTEGER 3
    compound1: COMPOUND {
        xUT: STRING 'two length compound'
        int: INTEGER 23
    }
    string1: STRING 'Hello world!'
    string2: STRING '3'
    arr1: ARRAY [
        STRING 'Hello world!'
        INTEGER 3
        STRING '3'
        FLOAT_32 3.29
        FLOAT_64 249.2992
        BOOLEAN true
        COMPOUND {
            str: STRING 'one length compound'
        }
        BOOLEAN false
        NULL null
    ]
    bool1: BOOLEAN false
    null1: NULL null
}

对比初始信息,名字xml不知为何变成了xUT...

经过一些研究,我发现压缩前后的二进制数据差别不大。 110011等模式变为101010.

当我将名称 xml 加长时,例如 xmldm,出于某种原因它只是被解析为 xmldm。 我目前看到问题只发生在三个字符的名称上。

直接压缩和解压生成的字符串(不保存到文件并读取)确实有效,所以可能是文件编码导致的错误。

据我所知,字符串输出为 ISO 8859-1 格式,但我无法获得正确的文件编码。读一个文件的时候,就按该读的来读,所有的字符都读成ISO 8859-1字符。

我有一些可能是原因,我实际上不知道如何测试它们:

但是哪一个是正确的,如果其中 none 个是正确的,那么这个错误的真正原因是什么?

我现在想不通。

MBDF文件class,读取和存储文件:

/* MBDFFile.java */
package com.redgalaxy.mbdf;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class MBDFFile {
    public static MBDF readMBDFFromFile(String filename) throws IOException {

//        FileInputStream is = new FileInputStream(filename);
//        InputStreamReader isr = new InputStreamReader(is, "ISO-8859-1");
//        BufferedReader br = new BufferedReader(isr);
//
//        StringBuilder builder = new StringBuilder();
//
//        String currentLine;
//
//        while ((currentLine = br.readLine()) != null) {
//            builder.append(currentLine);
//            builder.append("\n");
//        }
//
//        builder.deleteCharAt(builder.length() - 1);
//
//
//        br.close();

        Path path = Paths.get(filename);
        byte[] data = Files.readAllBytes(path);

        return new MBDF(new String(data, "ISO-8859-1"));
    }

    private static void writeToFile(String filename, byte[] txt) throws IOException {
//        BufferedWriter writer = new BufferedWriter(new FileWriter(filename));
////        FileWriter writer = new FileWriter(filename);
//        writer.write(txt.getBytes("ISO-8859-1"));
//        writer.close();
//        PrintWriter pw = new PrintWriter(filename, "ISO-8859-1");
        FileOutputStream stream = new FileOutputStream(filename);
        stream.write(txt);
        stream.close();
    }

    public static void writeMBDFToFile(String filename, MBDF info) throws IOException {
        writeToFile(filename, info.pack().getBytes("ISO-8859-1"));
    }
}

pack 函数以 ISO 8859-1 格式生成文件的最终字符串。

对于所有其他代码,请参阅我的 MBDF Github repository

我评论了我尝试过的代码,试图展示我的尝试。

我的作品space: - Macbook Air '11(High Sierra) - IntellIJ 社区 2017.3 - JDK 1.8

我希望这是足够的信息,这实际上是弄清楚我在做什么以及到底什么不起作用的唯一方法。


编辑:MBDF.java

/* MBDF.java */
package com.redgalaxy.mbdf;

import java.io.IOException;
import java.io.UnsupportedEncodingException;

public class MBDF {

    private String data;
    private InfoTag tag;

    public MBDF(String data) {
        this.tag = new InfoTag((byte) data.charAt(0));
        this.data = data.substring(1);
    }

    public MBDF(String data, InfoTag tag) {
        this.tag = tag;
        this.data = data;
    }

    public MBDFBinary getBinaryObject() throws IOException {
        String uncompressed = data;
        if (tag.isCompressed) {
            uncompressed = GZipUtils.decompress(data);
        }
        Binary binary = getBinaryFrom8Bit(uncompressed);
        return new MBDFBinary(binary.subBit(0, binary.getLen() - tag.trailing));
    }

    public static Binary getBinaryFrom8Bit(String s8bit) {
        try {
            byte[] bytes = s8bit.getBytes("ISO-8859-1");
            return new Binary(bytes, bytes.length * 8);
        } catch( UnsupportedEncodingException ignored ) {
            // This is not gonna happen because encoding 'ISO-8859-1' is always supported.
            return new Binary(new byte[0], 0);
        }
    }

    public static String get8BitFromBinary(Binary binary) {
        try {
            return new String(binary.getByteArray(), "ISO-8859-1");
        } catch( UnsupportedEncodingException ignored ) {
            // This is not gonna happen because encoding 'ISO-8859-1' is always supported.
            return "";
        }
    }

    /*
     * Adds leading zeroes to the binary string, so that the final amount of bits is 16
     */
    private static String addLeadingZeroes(String bin, boolean is16) {
        int len = bin.length();
        long amount = (long) (is16 ? 16 : 8) - len;

        // Create zeroes and append binary string
        StringBuilder zeroes = new StringBuilder();
        for( int i = 0; i < amount; i ++ ) {
            zeroes.append(0);
        }
        zeroes.append(bin);

        return zeroes.toString();
    }

    public String pack(){
        return tag.getFilePrefixChar() + data;
    }

    public String getData() {
        return data;
    }

    public InfoTag getTag() {
        return tag;
    }

}

此 class 包含 pack() 方法。 data 已在此处压缩(如果应该的话)。

对于其他 classes,请查看 Github 存储库,我不想让我的问题太长。

自己解决了!

好像是读写系统。当我导出文件时,我使用 ISO-8859-1 table 制作了一个字符串,将字节转换为字符。我将该字符串写入一个文本文件,该文件是 UTF-8。最大的问题是我使用 FileWriter 个实例来编写它,这些实例用于文本文件。

阅读使用了逆系统。完整的文件作为字符串读入内存(内存消耗!!),然后被解码。

我不知道文件是二进制数据,它们的特定格式形成文本数据。 ISO-8859-1 和 UTF-8 是其中的一些格式。我在使用 UTF-8 时遇到了问题,因为它将一些字符拆分为两个字节,我无法处理...

我的解决方案是使用流。 Java中存在FileInputStreamFileOutputStream,可以用来读写二进制文件。我没有使用流,因为我认为没有太大的区别("files are text, so what's the problem?"),但是...我实现了这个(通过编写一个新的类似库) 并且我现在能够将每个输入流传递给解码器,并将每个输出流传递给编码器。要制作未压缩的文件,您需要传递一个FileOutputStream。 GZip 文件可以使用 GZipOutputStreams,依赖于 FileOutputStream。如果有人想要一个包含二进制数据的字符串,可以使用 ByteArrayOutputStream。相同的规则适用于阅读,其中应使用上述流的 InputStream 变体。

不再有 UTF-8 或 ISO-8859-1 问题,它似乎可以工作,即使使用 GZip!