c ++-解析二进制文件。 什么是现代方式?

我有一个已知格式的二进制文件。 例如,让format像这样:

  • 2个字节(无符号短整数)-字符串的长度
  • 5个字节(5个字符)-字符串-一些ID名称
  • 4个字节(无符号整数)-跨度
  • 24字节(6 x浮点数-2个跨步,每个3个浮点数)-浮点数据

该文件应如下所示(为可读性添加了空格):

5 hello 3 0.0 0.1 0.2 -0.3 -0.4 -0.5

这里5-是2个字节:0x05 0x00。 “ hello”-5个字节,依此类推。

现在,我想阅读此文件。 目前,我正在这样做:

  • 加载文件到ifstream
  • 将此流读取为char [x]
  • 将其强制转换为无符号短型:char [x]。现在我有一个字符串的长度。
  • 将流读取到char [x],并从该向量创建float。 现在我有了字符串ID。
  • 以相同的方式读取接下来的4个字节,并将其转换为unsigned int。 现在我大步前进。
  • 虽然未读取文件末尾以相同的方式浮动-为每个浮动创建char [x]并强制转换float

这有效,但对我来说看起来很丑。 我可以直接读取char [x]floatstring等,而无需创建char [x]吗? 如果没有,正确转换的方式是什么(我读到我正在使用的样式-是旧样式)?

附言:我写一个问题时,脑海中浮现出更清晰的解释-如何从char [x]中的任意位置转换任意数量的字节?

更新:我忘了明确提到字符串和浮点数据长度在编译时未知,并且是可变的。

nikitablack asked 2020-06-29T22:29:39Z
10个解决方案
13 votes

如果不是出于学习目的,并且您可以自由选择二进制格式,则最好考虑使用诸如protobuf之类的东西,它将为您处理序列化并允许与其他平台和语言进行互操作。

如果您不能使用第三方API,则可以查看QDataStream以获取启发

  • 文献资料
  • 源代码
fjardon answered 2020-06-29T22:30:57Z
10 votes

在C ++中可以正常工作的C方法是声明一个struct:

#pragma pack(1)

struct contents {
   // data members;
};

注意

  • 您需要使用一种编译指示来使编译器按照结构中的外观对齐数据。
  • 此技术仅适用于POD类型

然后将读取缓冲区直接转换为struct类型:

std::vector<char> buf(sizeof(contents));
file.read(buf.data(), buf.size());
contents *stuff = reinterpret_cast<contents *>(buf.data());

现在,如果您的数据大小是可变的,则可以分成几个块。 要从缓冲区读取单个二进制对象,可以使用reader函数:

template<typename T>
const char *read_object(const char *buffer, T& target) {
    target = *reinterpret_cast<const T*>(buffer);
    return buffer + sizeof(T);
}

主要优点是这样的阅读器可以专门用于更高级的c ++对象:

template<typename CT>
const char *read_object(const char *buffer, std::vector<CT>& target) {
    size_t size = target.size();
    CT const *buf_start = reinterpret_cast<const CT*>(buffer);
    std::copy(buf_start, buf_start + size, target.begin());
    return buffer + size * sizeof(CT);
}

现在在您的主解析器中:

int n_floats;
iter = read_object(iter, n_floats);
std::vector<float> my_floats(n_floats);
iter = read_object(iter, my_floats);

注意:正如Tony D观察到的那样,即使您可以通过#pragma指令和手动填充(如果需要)正确地进行对齐,也可能会遇到(最佳情况)性能问题或(最坏情况)形式的处理器对齐方式不兼容的情况。 )捕获信号。 仅当您可以控制文件格式时,此方法才可能很有趣。

slaphappy answered 2020-06-29T22:30:24Z
9 votes

目前,我正在这样做:

  • 加载文件到ifstream

  • 将此流读取到char缓冲区[2]

  • 将其投射到reinterpret_castchar。现在我有一个字符串的长度。

最后一个冒着reinterpret_cast的风险(如果您的字符数组恰好从奇数地址开始,并且您的CPU只能读取在偶数地址处对齐的16位值),性能(某些CPU会读取未对齐的值,但速度较慢;其他的则是现代的) x86很好且很快)和/或字节序问题。 我建议阅读这两个字符,然后您可以说char,反之亦然,如果需要更正字节序,请使用reinterpret_cast

  • 读取流到reinterpret_cast并从此std::string创建char。现在我有了字符串id。

不需要...直接读入字符串:

std::string s(the_size, ' ');

if (input_fstream.read(&s[0], s.size()) &&
    input_stream.gcount() == s.size())
    ...use s...
  • 以同样的方式reinterpret_cast接下来的4个字节,并将其转换为char。现在,我迈出了一大步。 std::string不是文件的结尾htonl floats以相同的方式-创建Data*,并为每个float投射y

最好直接在reinterpret_castchar上读取数据,因为这样编译器将确保正确对齐。

这有效,但对我来说看起来很丑。 我可以直接阅读reinterpret_castcharstd::string等,而无需创建htonl吗? 如果没有,正确转换的方式是什么(我读到我正在使用的样式-是旧样式)?

struct Data
{
    uint32_t x;
    float y[6];
};
Data data;
if (input_stream.read((char*)&data, sizeof data) &&
    input_stream.gcount() == sizeof data)
    ...use x and y...

请注意,上面的代码避免将数据读取到可能未对齐的字符数组中,其中由于对齐问题,它对于可能未对齐的char数组(包括在std::string中)中的reinterpret_cast数据不安全。 同样,如果文件内容的字节序可能不同,则可能需要使用htonl进行一些读后转换。 如果存在未知数目的float,则需要计算并分配至少4个字节对齐的足够存储空间,然后将Data*对准它……只要索引超出了2944941176723604604的声明数组大小,这是合法的。 被访问地址处的内存内容是分配的一部分,并拥有从流中读取的有效float表示形式。 更简单-但需要额外阅读,因此可能会更慢-首先阅读uint32_t,然后阅读new float[n],然后再阅读read...。

实际上,这种方法可以工作,并且许多底层代码和C代码都可以做到这一点。 可能有助于您读取文件的“更干净”的高级库最终必须在内部做类似的事情。

Tony Delroy answered 2020-06-29T22:32:09Z
7 votes

实际上,我上个月才实现了一个快速而肮脏的二进制格式解析器,以读取shared_ptr文件(遵循Wikipedia的格式描述),并且由于现代,我决定使用C ++模板。

在某些特定平台上,压缩的2944942377183183740928可以工作,但是有些事情处理不好……例如可变长度的字段。 但是,有了模板,就不会出现这样的问题:您可以得到任意复杂的结构(和返回类型)。

幸运的是,一个shared_ptr存档相对简单,所以我实现了一些简单的方法。 从我的头顶上:

using Buffer = std::pair<unsigned char const*, size_t>;

template <typename OffsetReader>
class UInt16LEReader: private OffsetReader {
public:
    UInt16LEReader() {}
    explicit UInt16LEReader(OffsetReader const or): OffsetReader(or) {}

    uint16_t read(Buffer const& buffer) const {
        OffsetReader const& or = *this;

        size_t const offset = or.read(buffer);
        assert(offset <= buffer.second && "Incorrect offset");
        assert(offset + 2 <= buffer.second && "Too short buffer");

        unsigned char const* begin = buffer.first + offset;

        // http://commandcenter.blogspot.fr/2012/04/byte-order-fallacy.html
        return (uint16_t(begin[0]) << 0)
             + (uint16_t(begin[1]) << 8);
    }
}; // class UInt16LEReader

// Declined for UInt[8|16|32][LE|BE]...

当然,基本的2944942377183740740928实际上具有恒定的结果:

template <size_t O>
class FixedOffsetReader {
public:
    size_t read(Buffer const&) const { return O; }
}; // class FixedOffsetReader

并且由于我们正在讨论模板,因此您可以随意切换类型(您可以实现代理读取器,将所有读取委托给shared_ptr进行记忆)。

但是,有趣的是最终结果:

// http://en.wikipedia.org/wiki/Zip_%28file_format%29#File_headers
class LocalFileHeader {
public:
    template <size_t O>
    using UInt32 = UInt32LEReader<FixedOffsetReader<O>>;
    template <size_t O>
    using UInt16 = UInt16LEReader<FixedOffsetReader<O>>;

    UInt32< 0> signature;
    UInt16< 4> versionNeededToExtract;
    UInt16< 6> generalPurposeBitFlag;
    UInt16< 8> compressionMethod;
    UInt16<10> fileLastModificationTime;
    UInt16<12> fileLastModificationDate;
    UInt32<14> crc32;
    UInt32<18> compressedSize;
    UInt32<22> uncompressedSize;

    using FileNameLength = UInt16<26>;
    using ExtraFieldLength = UInt16<28>;

    using FileName = StringReader<FixedOffsetReader<30>, FileNameLength>;

    using ExtraField = StringReader<
        CombinedAdd<FixedOffsetReader<30>, FileNameLength>,
        ExtraFieldLength
    >;

    FileName filename;
    ExtraField extraField;
}; // class LocalFileHeader

这显然很简单,但同时却非常灵活。

一个明显的改进轴是改进链接,因为这里存在意外重叠的风险。 我的档案读取代码在我第一次尝试时就起作用了,这足以证明该代码足以完成当前的任务。

Matthieu M. answered 2020-06-29T22:33:01Z
3 votes

我不得不解决这个问题一次。 数据文件打包为FORTRAN输出。 一致都是错误的。 我成功完成了自动执行手动操作的预处理器技巧:将原始数据从字节缓冲区解压缩到结构中。 这个想法是在包含文件中描述数据:

BEGIN_STRUCT(foo)
    UNSIGNED_SHORT(length)
    STRING_FIELD(length, label)
    UNSIGNED_INT(stride)
    FLOAT_ARRAY(3 * stride)
END_STRUCT(foo)

现在,您可以定义这些宏以生成所需的代码,例如struct声明,包括上述内容,undef,然后再次定义宏以生成拆包功能,然后再定义另一个include等。

注意:我首先看到了gcc中用于抽象语法树相关代码生成的技术。

如果CPP功能不够强大(或这种滥用预处理器不适合您),请替换一个小的lex / yacc程序(或选择您喜欢的工具)。

令我惊讶的是,至少在这样的低级基础代码中,花很多时间考虑生成代码而不是手工编写代码。

Gene answered 2020-06-29T22:33:39Z
2 votes

您最好声明一个结构(使用1字节填充-如何-取决于编译器)。 使用该结构进行写入,并使用相同的结构进行读取。 仅将POD放入结构中,因此不要放入std::string等。仅将此结构用于文件I / O或其他进程间通信-使用常规的structclass将其保存以在C ++程序中进一步使用。

Ajay answered 2020-06-29T22:34:00Z
2 votes

由于所有数据都是可变的,因此您可以分别读取两个块并仍然使用强制转换:

struct id_contents
{
    uint16_t len;
    char id[];
} __attribute__((packed)); // assuming gcc, ymmv

struct data_contents
{
    uint32_t stride;
    float data[];
} __attribute__((packed)); // assuming gcc, ymmv

class my_row
{
    const id_contents* id_;
    const data_contents* data_;
    size_t len;

public:
    my_row(const char* buffer) {
        id_= reinterpret_cast<const id_contents*>(buffer);
        size_ = sizeof(*id_) + id_->len;
        data_ = reinterpret_cast<const data_contents*>(buffer + size_);
        size_ += sizeof(*data_) + 
            data_->stride * sizeof(float); // or however many, 3*float?

    }

    size_t size() const { return size_; }
};

这样,您可以使用kbok先生的答案正确解析:

const char* buffer = getPointerToDataSomehow();

my_row data1(buffer);
buffer += data1.size();

my_row data2(buffer);
buffer += data2.size();

// etc.
Barry answered 2020-06-29T22:34:24Z
2 votes

我个人这样做:

// some code which loads the file in memory
#pragma pack(push, 1)
struct someFile { int a, b, c; char d[0xEF]; };
#pragma pack(pop)

someFile* f = (someFile*) (file_in_memory);
int filePropertyA = f->a;

在文件开始处固定大小的结构的一种非常有效的方法。

rev answered 2020-06-29T22:34:48Z
1 votes

使用序列化库。 这里有一些:

  • Boost序列化和Boost融合
  • 谷物(我自己的图书馆)
  • 另一个名为谷类的图书馆(与我的名字相同,但早于我的名字)
  • Cap'n Proto
Átila Neves answered 2020-06-29T22:35:26Z
0 votes

我使用ragel工具为具有1-2K RAM的微控制器生成纯C程序源代码(无表)。 它不使用任何文件io,进行缓冲,并使用状态机图生成易于调试的代码和.dot / .pdf文件。

ragel还可以输出go,Java,..代码进行解析,但是我没有使用这些功能。

ragel的关键功能是能够解析任何字节构建数据,但是您无法挖掘位字段。 另一个问题是ragel能够解析常规结构,但没有递归和语法语法解析。

Dmitry Ponyatov answered 2020-06-29T22:35:55Z
translate from https://stackoverflow.com:/questions/26845538/parsing-a-binary-file-what-is-a-modern-way