Подтвердить что ты не робот

Разбор двоичного файла. Что такое современный способ?

У меня есть двоичный файл с некоторым расположением, которое я знаю. Например, пусть формат будет таким:

  • 2 байта (unsigned short) - длина строки
  • 5 байтов (5 х символов) - строка - некоторое имя id
  • 4 байта (unsigned int) - шаг
  • 24 байта (6 x float - 2 шага по 3 поплавки) - данные с плавающей запятой

Файл должен выглядеть (я добавил пробелы для чтения):

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

Здесь 5 - это 2 байта: 0x05 0x00. "привет" - 5 байт и т.д.

Теперь я хочу прочитать этот файл. В настоящее время я делаю это так:

  • загрузите файл в ifstream
  • прочитайте этот поток до char buffer[2]
  • переведите его в unsigned short: unsigned short len{ *((unsigned short*)buffer) };. Теперь у меня длина строки.
  • прочитайте поток до vector<char> и создайте std::string из этого вектора. Теперь у меня есть идентификатор строки.
  • таким же образом прочитайте следующие 4 байта и переведите их в unsigned int. Теперь у меня есть шаг.
  • в то время как конец файла не читает float одинаково - создайте char bufferFloat[4] и нарисуйте *((float*)bufferFloat) для каждого поплавка.

Это работает, но для меня это выглядит уродливо. Можно ли читать непосредственно unsigned short или float или string и т.д. Без создания char [x]? Если нет, то каков способ правильной работы (я читаю, что стиль, который я использую, - это старый стиль)?

P.S.: пока я писал вопрос, более ясное объяснение, поднятое в моей голове, - как произвольное количество байтов от произвольной позиции в char [x]?

Обновление: я забыл явно указать, что длина строки и длины плавающего файла неизвестна во время компиляции и является переменной.

4b9b3361

Ответ 1

Путь C, который отлично работал бы на С++, заключался бы в объявлении структуры:

#pragma pack(1)

struct contents {
   // data members;
};

Обратите внимание, что

  • Вам нужно использовать прагму, чтобы заставить компилятор выровнять данные как-то-в-структуре в структуре;
  • Этот метод работает только с типами POD

И затем создайте буфер чтения непосредственно в тип структуры:

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

Теперь, если ваш размер данных является переменным, вы можете разделить его на несколько кусков. Для чтения одного бинарного объекта из буфера функция чтения удобна:

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

Основное преимущество заключается в том, что такой читатель может быть специализирован для более продвинутых объектов С++:

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

Примечание.. Как заметил Тони Д, даже если вы можете получить выравнивание по директивам #pragma и ручному заполнению (если необходимо), вы все равно можете столкнуться с несовместимостью с выравниванием вашего процессора в формы (наилучшего случая) производительности или (наихудших) сигналов ловушки. Этот метод, вероятно, интересен только в том случае, если у вас есть контроль над файловым форматом.

Ответ 2

Если это не для целей обучения, и если у вас есть свобода в выборе бинарного формата, вам лучше подумать об использовании чего-то вроде protobuf, который будет обрабатывать сериализацию для вас и позволит взаимодействовать с другими платформами и языками.

Если вы не можете использовать сторонний API, вы можете посмотреть QDataStream для вдохновения

Ответ 3

На данный момент делаю это так:

  • загрузить файл в ifstream

  • читать этот поток в буфер символов [2]

  • приведите его к unsigned short: unsigned short len{ *((unsigned short*)buffer) }; , Теперь у меня есть длина строки.

Это последнее рискует SIGBUS (если ваш массив символов начинается с нечетного адреса, а ваш ЦП может читать только 16-битные значения, которые выровнены по четному адресу), производительность (некоторые ЦП будут считывать смещенные значения, но медленнее, другие как современные x86 - это нормально и быстро) и/или проблемы с порядком байтов. Я бы посоветовал прочитать два символа, тогда вы можете сказать (x[0] << 8) | x[1] (x[0] << 8) | x[1] или наоборот, используя htons если нужно исправить порядок байтов.

  • читать поток в vector<char> и создавать std::string из этого vector. Теперь у меня есть идентификатор строки.

Не нужно... просто читать прямо в строку:

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 одинаковым образом - создайте char bufferFloat[4] и char bufferFloat[4] *((float*)bufferFloat) для каждого float.

Лучше читать данные непосредственно через unsigned int и floats, так как таким образом компилятор обеспечит правильное выравнивание.

Это работает, но для меня это выглядит некрасиво. Могу ли я читать напрямую в unsigned short или float или string и т.д. Без создания char [x]? Если нет, то как правильно использовать приведение (я прочитал, какой стиль я использую - это старый стиль)?

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...

Обратите внимание, что приведенный выше код избегает чтения данных в потенциально невыровненные символьные массивы, при этом небезопасно reinterpret_cast данные в потенциально невыровненном массиве char (в том числе внутри std::string) из-за проблем с выравниванием. Опять же, вам может потребоваться некоторое преобразование после чтения с помощью htonl если есть вероятность, что содержимое файла отличается порядком байтов. Если существует неизвестное число чисел с float, вам нужно будет рассчитать и выделить достаточное хранилище с выравниванием не менее 4 байтов, а затем нацелить Data* на него... допустимо индексировать после объявленного размера массива y так долго поскольку содержимое памяти по адресу, к которому осуществляется доступ, было частью выделения и содержит действительное представление с float считанное из потока. Проще - но с дополнительным чтением, возможно, медленнее - сначала прочитайте uint32_t затем new float[n] и выполните дальнейшее read там....

Практически, этот тип подхода может работать, и много низкоуровневого кода и кода C делают именно это. "Чистые" высокоуровневые библиотеки, которые могут помочь вам прочитать файл, должны в конечном итоге делать что-то похожее внутри...

Ответ 4

Я фактически реализовал быстрый и грязный двоичный формат анализатора, чтобы читать файлы .zip (после описания формата Википедии) только в прошлом месяце, и, будучи современным, я решил использовать шаблоны С++.

На некоторых конкретных платформах упакованный struct может работать, однако есть вещи, которые он плохо обрабатывает... например, поля переменной длины. С шаблонами, однако, такой проблемы нет: вы можете получить произвольно сложные структуры (и типы возврата).

A .zip архив относительно прост, к счастью, поэтому я реализовал что-то простое. Сверху моей головы:

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

и поскольку мы говорим о шаблонах, вы можете переключать типы на досуге (вы можете реализовать прокси-ридер, который делегирует все чтения shared_ptr, который их memoizes).

Интересным, однако, является конечный результат:

// 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

Это довольно упрощенно, очевидно, но невероятно гибко в то же время.

Очевидной осью улучшения будет улучшение сцепления, поскольку здесь существует риск случайного перекрытия. Код моего архивного чтения работал в первый раз, когда я попробовал это, хотя это было достаточным доказательством для меня, что этого кода было достаточно для этой задачи.

Ответ 5

Мне пришлось решить эту проблему один раз. Файлы данных были упакованы для выхода FORTRAN. Все смещения были неправильными. Мне удалось использовать препроцессорные трюки, которые автоматически выполняли то, что вы делаете вручную: распакуйте необработанные данные из байтового буфера в структуру. Идея состоит в том, чтобы описать данные в файле include:

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

Теперь вы можете определить эти макросы для генерации нужного вам кода, например объявление структуры, включить выше, undef и снова определить макросы для генерации распаковки функций, а затем еще один include и т.д.

NB Я впервые увидел эту технику, используемую в gcc для генерации генерации абстрактного синтаксиса.

Если CPP недостаточно мощный (или такое злоупотребление препроцессором не для вас), замените небольшую программу lex/yacc (или выберите свой любимый инструмент).

Удивительно, как часто приходится думать о том, как генерировать код, а не писать его вручную, по крайней мере, в базовом коде низкого уровня, подобном этому.

Ответ 6

Лучше объявить структуру (с 1-байтным заполнением - как - зависит от компилятора). Напишите с использованием этой структуры и прочитайте эту же структуру. Поместите только POD в структуру и, следовательно, нет std::string и т.д. Используйте эту структуру только для ввода/вывода файлов или другого взаимодействия между процессами - используйте обычный struct или class, чтобы удерживать его для дальнейшего использования в программе на С++.

Ответ 7

Поскольку все ваши данные являются переменными, вы можете прочитать два блока отдельно и по-прежнему использовать кастинг:

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.

Ответ 8

Я лично так делаю:

// 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;

Очень эффективный способ для структур фиксированного размера в начале файла.

Ответ 10

Я использую инструмент ragel для генерации процедурного исходного кода на языке C (без таблиц) для микроконтроллеров с 1-2 КБ ОЗУ. Он не использовал ни один файл io, буферизацию и производит как легко отлаживаемый код, так и файл .dot/.pdf со схемой конечного автомата.

ragel также может выводить go, Java,.. код для разбора, но я не использовал эти возможности.

Ключевая особенность ragel - возможность разбирать любые байтовые данные, но вы не можете копаться в битовых полях. Другая проблема заключается в том, что ragel способен анализировать регулярные структуры, но не имеет синтаксического анализа рекурсии и синтаксической грамматики.