用Java读取结构化二进制文件的最佳方法

Best way to read structured binary files with Java

我必须使用Java读取旧格式的二进制文件。

简而言之,该文件的标头由几个整数,字节和固定长度的char数组组成,后跟一系列由整数和char组成的记录列表。

用任何其他语言,我都会创建struct s(C / C ++)或record s(Pascal / Delphi),它们是标题和记录的逐字节表示。 然后,将sizeof(header)字节读入标头变量,并对记录执行相同的操作。

像这样:(Delphi)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type
  THeader = record
    Version: Integer;
    Type: Byte;
    BeginOfData: Integer;
    ID: array[0..15] of Char;
  end;

...

procedure ReadData(S: TStream);
var
  Header: THeader;
begin
  S.ReadBuffer(Header, SizeOf(THeader));
  ...
end;

用Java做类似的事情的最好方法是什么? 我是否必须自己读取每个值,或者是否有其他方法可以进行这种"块读取"?


据我所知,Java强迫您以字节为单位读取文件,而不是阻止读取。如果要序列化Java对象,那就大不一样了。

显示的其他示例将DataInputStream类与File一起使用,但您也可以使用快捷方式:RandomAccessFile类:

1
2
3
4
5
6
7
RandomAccessFile in = new RandomAccessFile("filename","r");
int version = in.readInt();
byte type = in.readByte();
int beginOfData = in.readInt();
byte[] tempId;
in.read(tempId, 0, 16);
String id = new String(tempId);

请注意,您可以将响应对象变成一个类,如果那样的话会更容易。


如果您要使用Preon,那么您要做的就是:

1
2
3
4
5
6
public class Header {
    @BoundNumber int version;
    @BoundNumber byte type;
    @BoundNumber int beginOfData;
    @BoundString(size="15") String id;
}

一旦有了这个,就可以使用一行创建编解码器:

1
Codec<Header> codec = Codecs.create(Header.class);

您可以像这样使用编解码器:

1
Header header = Codecs.decode(codec, file);


您可以如下使用DataInputStream类:

1
2
3
4
5
6
DataInputStream in = new DataInputStream(new BufferedInputStream(
                         new FileInputStream("filename")));
int x = in.readInt();
double y = in.readDouble();

etc.

一旦获得这些值,就可以随意使用它们。在API中查找java.io.DataInputStream类以获取更多信息。


我可能误会了您,但是在我看来,您正在创建内存结构,希望该结构可以逐字节精确地表示您要从硬盘读取的内容,然后将整个内容复制到内存中并从那里操纵?

如果确实如此,那么您正在玩非常危险的游戏。至少在C语言中,该标准不会强制执行诸如填充或对齐结构成员之类的操作。更不用说诸如大/小尾数或奇偶校验位之类的东西了……因此,即使您的代码碰巧运行了,它也是非常不可移植且具有风险的-您依赖于编译器的创建者在未来版本上不会改变主意。

最好创建一个自动机,以验证从HD读取的结构(字节/字节)是否有效,如果确实可以,则填充内存结构。尽管您获得了平台和编译器的独立性,但您可能要花几毫秒的时间(不如现代操作系统执行大量磁盘读取缓存的时间少)。另外,您的代码将轻松移植到另一种语言。

编辑后:我以某种方式同情你。在DOS / Win3.11时代,我曾经创建一个C程序来读取BMP文件。并使用了完全相同的技术。一切都很好,直到我尝试为Windows编译它-糟糕!整数现在是32位长,而不是16位!当我尝试在Linux上进行编译时,发现的gcc与Microsoft C(6.0!)相比,位字段分配规则非常不同。我不得不借助宏技巧来使其可移植...


我使用了Javolution和javastruct,它们都处理字节和对象之间的转换。

Javolution提供了代表C类型的类。您所需要做的就是编写一个描述C结构的类。例如,从C头文件中,

1
2
3
4
5
struct Date {
    unsigned short year;
    unsigned byte month;
    unsigned byte day;
};

应该翻译成:

1
2
3
4
5
public static class Date extends Struct {
    public final Unsigned16 year = new Unsigned16();
    public final Unsigned8 month = new Unsigned8();
    public final Unsigned8 day   = new Unsigned8();
}

然后调用setByteBuffer初始化对象:

1
2
Date date = new Date();
date.setByteBuffer(ByteBuffer.wrap(bytes), 0);

javastruct使用注释来定义C结构中的字段。

1
2
3
4
5
6
7
8
9
@StructClass
public class Foo{

    @StructField(order = 0)
    public byte b;

    @StructField(order = 1)
    public int i;
}

初始化对象:

1
2
Foo f2 = new Foo();
JavaStruct.unpack(f2, b);

这是使用ByteBuffer(Java NIO)读取字节的链接

http://exampledepot.com/egs/java.nio/ReadChannel.html


我想FileInputStream可以读取字节数。因此,使用FileInputStream打开文件并读入sizeof(header)。我假设标题具有固定的格式和大小。我没有看到在最初的文章中提到的内容,但是假设是这种情况,因为如果标头具有可选的args和不同的大小,它将变得更加复杂。

获得信息后,可以有一个标头类,在其中分配已经读取的缓冲区的内容。然后以类似的方式解析记录。


我将创建一个对象,该对象包装数据的ByteBuffer表示形式并提供直接从缓冲区读取的吸气剂。这样,可以避免将数据从缓冲区复制到基本类型。此外,您可以使用MappedByteBuffer来获取字节缓冲区。如果二进制数据很复杂,则可以使用类对其进行建模,并为每个类提供缓冲区的切片版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
class SomeHeader {
    private final ByteBuffer buf;
    SomeHeader( ByteBuffer fileBuffer){
       // you may need to set limits accordingly before
       // fileBuffer.limit(...)
       this.buf = fileBuffer.slice();
       // you may need to skip the sliced region
       // fileBuffer.position(endPos)
    }
    public short getVersion(){
        return buf.getShort(POSITION_OF_VERSION_IN_BUFFER);
    }
}

从字节缓冲区读取无符号值的方法也很有用。

高温超导


正如其他人提到的那样,DataInputStream和Buffers可能是您在Java中处理二进制数据所追求的低级API。

但是,您可能想要类似Construct的东西(维基页面上也有很好的示例:http://en.wikipedia.org/wiki/Construct_(python_library),但是对于Java。

我暂时还不知道任何Java版本,但是采用这种方法(在代码中声明性地指定结构)可能是正确的方法。使用Java中合适的流利接口,它可能与DSL非常相似。

编辑:谷歌搜索揭示了这一点:

http://javolution.org/api/javolution/io/Struct.html

您可能正在寻找哪种东西。我不知道它是否有效或有什么好处,但它似乎是一个明智的起点。


我已经写了一种在Java中做这种事情的技术-类似于读取位域的古老C类习惯用法。请注意,这只是一个开始,但可以扩展。

这里


前一阵子,我发现这篇文章是关于使用反射和解析来读取二进制数据的。在这种情况下,作者正在使用反射读取Java二进制.class文件。但是,如果您将数据读取到类文件中,则可能会有帮助。


过去,我使用DataInputStream按指定顺序读取任意类型的数据。这将使您无法轻松解决大端/小端问题。

从1.4版本开始,也许可以使用java.nio.Buffer系列,但是看来您的代码实际上可能更复杂。这些类确实支持处理字节序问题。