parsing二进制文件。 什么是现代的方式?
我有一个二进制文件,我知道一些布局。 例如让格式是这样的:
- 2个字节(无符号短整数) – 一个string的长度
- 5个字节(5个字符) – string – 一些id名字
- 4个字节(无符号整数) – 一个步幅
- 24个字节(6个浮点数 – 每个浮点数3个浮点数) – 浮点数据
该文件应该看起来像(为了可读性,我添加了空格):
5 hello 3 0.0 0.1 0.2 -0.3 -0.4 -0.5
这里5 – 是2个字节:0x05 0x00。 “你好” – 5字节等等。
现在我想读这个文件。 目前我这样做:
- 加载文件到ifstream
- 读取这个stream到
char buffer[2]
- 将其转换为unsigned short:
unsigned short len{ *((unsigned short*)buffer) };
。 现在我有一个string的长度。 - 读一个stream来载入
vector<char>
并从这个向量创build一个std::string
。 现在我有stringID。 - 以相同的方式读取下4个字节并将它们转换为无符号整型。 现在我迈出了一大步。
- 而没有结束文件读取浮动相同的方式 – 创build一个
char bufferFloat[4]
和cast*((float*)bufferFloat)
为每个浮动。
这工作,但对我来说,它看起来很丑。 我可以直接读取unsigned short
或float
或string
等没有char [x]
创build? 如果不是,那么正确投射的方式是什么(我阅读了我使用的风格 – 是一种旧风格)?
PS:在我写了一个问题的时候,我头脑中提出了更清晰的解释 – 如何从char [x]
任意位置投射任意数量的字节?
更新:我忘了明确提到string和浮点数据长度在编译时是不知道的,并且是可变的。
在C ++中可以正常工作的C方法是声明一个结构体:
#pragma pack(1) struct contents { // data members; };
注意
- 你需要使用一个编译指示来使编译器将数据对准结构中的外观 ;
- 这种技术只适用于PODtypes
然后将读取缓冲区直接转换为结构体types:
std::vector<char> buf(sizeof(contents)); file.read(buf.data(), buf.size()); contents *stuff = reinterpret_cast<contents *>(buf.data());
现在如果你的数据的大小是可变的,你可以分成几个块。 为了从缓冲区中读取一个二进制对象,阅读器function便于使用:
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
指令和手工填充(如果需要)来获得alignment方式,您仍然可能遇到与处理器alignment方式不兼容(以最佳情况)性能问题或(最差情况)陷阱信号。 只有在您控制了文件格式的情况下,这个方法才有意思。
如果不是为了学习的目的,如果你有自由select二进制格式,你最好考虑使用类似protobuf的东西,这将为你处理序列化,并允许与其他平台和语言互操作。
如果你不能使用第三方API,你可以看看QDataStream
的灵感
- 文档
- 源代码
目前我这样做:
加载文件到ifstream
读取这个stream到字符缓冲区[2]
将其转换为
unsigned short
:unsigned short len{ *((unsigned short*)buffer) };
。 现在我有一个string的长度。
最后的风险是SIGBUS
,性能和/或sorting问题。 我build议读两个字符,然后你可以说(x[0] << 8) | x[1]
(x[0] << 8) | x[1]
,反之亦然,使用htons
如果需要校正字节顺序。
- 读一个stream来载入
vector<char>
并从这个vector
创build一个std::string
。 现在我有stringID。
不需要…直接读入string:
std::string s(the_size, ' '); if (input_fstream.read(&s[0], s.size()) && input_stream.gcount() == s.size()) ...use s...
- 以相同的方式
read
下4个字节并将它们转换为unsigned int
。 现在我迈出了一大步。while
不是文件结束read
float
相同的方式 – 为每个float
创build一个char bufferFloat[4]
和cast*((float*)bufferFloat)
。
最好是直接通过unsigned int
s和unsigned int
读取数据,这样编译器将确保正确的alignment。
这工作,但对我来说,它看起来很丑。 我可以直接读取
unsigned short
或float
或string
等没有char [x]
创build? 如果不是,那么正确投射的方式是什么(我阅读了我使用的风格 – 是一种旧风格)?
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...
注意上面的代码避免了将数据读入可能未alignment的字符数组中,其中由于alignment问题,在可能未alignment的char
数组(包括std::string
)中reinterpret_cast
数据是不安全的。 同样,如果文件内容在字节顺序上存在差异,您可能需要使用htonl
一些读后转换。 如果有一个float
未知数,则需要计算并分配至less4个字节的alignment足够的存储空间,然后将Data*
目标…这是合法的,只要索引y
的声明数组大小就足够长因为在被访问的地址上的内存内容是分配的一部分并且保持从该stream读入的有效的float
表示。 更简单 – 但有一个额外的阅读,所以可能更慢 – 阅读uint32_t
然后new float[n]
,并进一步read
到那里….
实际上,这种方法可以工作,很多低级和C代码就是这样做的。 “清洁”高级图书馆,可能会帮助您阅读文件最终必须做一些类似的内部….
我不得不一次解决这个问题。 数据文件打包FORTRAN输出。 行程全错了。 我成功地使用了预处理技巧,自动完成手动操作:将原始数据从字节缓冲区解压缩到结构体。 这个想法是描述一个包含文件中的数据:
BEGIN_STRUCT(foo) UNSIGNED_SHORT(length) STRING_FIELD(length, label) UNSIGNED_INT(stride) FLOAT_ARRAY(3 * stride) END_STRUCT(foo)
现在你可以定义这些macros来生成你需要的代码,比如结构声明,包括上面的,undef,再次定义macros来生成解包函数,接下来是另一个include,等等。
NB我首先看到了gcc中用于抽象语法树相关代码生成的技术。
如果CPPfunction不够强大(或者这样的预处理器滥用不适合你),可以用一个小的lex / yacc程序(或者select你喜欢的工具)来代替。
对于我来说,从代码生成的angular度考虑,而不是手工编写代码,至less在这样的底层代码中,我感到非常惊讶。
实际上,我实现了一个快速而脏的二进制格式parsing器,在上个月读取.zip
文件(遵循维基百科的格式描述),而且现代化的我决定使用C ++模板。
在一些特定的平台上,一个打包struct
可以工作,但是有些东西不能很好地处理…比如变长的字段。 然而,对于模板,不存在这样的问题:你可以得到任意复杂的结构(和返回types)。
.zip
压缩包相对简单,所以我实现了一些简单的function。 closures我的头顶上:
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]...
当然,基本的OffsetReader
实际上有一个不变的结果:
template <size_t O> class FixedOffsetReader { public: size_t read(Buffer const&) const { return O; } }; // class FixedOffsetReader
而且由于我们正在谈论模板,所以可以随意切换types(您可以实现一个代理读取器,将所有读取操作委派给一个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
这显然是相当简单的,但同时也是非常灵活的。
一个明显的改进方向是改善链接,因为这里存在意外重叠的风险。 我的档案阅读代码在我第一次尝试时就工作了,这足以certificate这个代码足以完成任务。
你应该更好地声明一个结构(1字节填充 – 如何 – 取决于编译器)。 使用该结构写入,并使用相同的结构进行读取。 只把POD放在结构体中,因此不需要std::string
等等。这个结构只用于文件I / O或其他进程间通信 – 使用普通的struct
或class
来保存它,以便在C ++程序中进一步使用。
由于您的所有数据都是可变的,因此您可以分别读取两个数据块,并仍使用转换:
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的答案正确parsing:
const char* buffer = getPointerToDataSomehow(); my_row data1(buffer); buffer += data1.size(); my_row data2(buffer); buffer += data2.size(); // etc.
我个人这样做:
// 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;
在文件开始时固定大小的结构非常有效。
使用序列化库。 这里有几个:
- Boost序列化和Boost融合
- 谷物 (我自己的图书馆)
- 另一个名为谷类的图书馆 (与我的名字相同,但是早于他们)
- Cap'n Proto