L'analyse d'un fichier binaire. Ce qui est une manière moderne?

j'ai un fichier binaire avec une mise en page que je connais. Par exemple, que le format soit comme ceci:

  • 2 octets (non signé court) - longueur d'une chaîne de caractères
  • 5 octets (5 x caractères) - de la ficelle - une pièce d'identité nom
  • 4 octets (int non signé) - a stride
  • 24 octets (6 x flotteur - 2 pas de 3 flotteurs chacun) - données du flotteur

le fichier devrait ressembler à (j'ai ajouté espaces pour la lisibilité):

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

Ici 5 - est de 2 octets: 0x05 0x00. "bonjour" - 5 octets, et ainsi de suite.

maintenant je veux lire ce dossier. Actuellement je le fais ainsi:

  • charger le fichier ifstream
  • lire ce flux char buffer[2]
  • convertir en unsigned short: unsigned short len{ *((unsigned short*)buffer) }; . Maintenant, j'ai la longueur d'une chaîne.
  • lire un flux vers vector<char> et créer un std::string à partir de ce vecteur. Maintenant, j'ai l'identification.
  • de la même façon, lire les 4 octets suivants et les lancer sur int. Maintenant, j'ai une foulée.
  • bien que non fin du fichier Lire flotte de la même façon - créer un char bufferFloat[4] et lancer *((float*)bufferFloat) pour chaque flotteur.

ça marche, mais pour moi ça a l'air moche. Puis - je lire directement à unsigned short ou float ou string etc. sans créer char [x] ? Si non, Quelle est la façon de mouler correctement (j'ai lu que le style que j'utilise - est un vieux style)?

P.S.: pendant que j'écrivais une question, l'explication la plus claire soulevée dans ma tête - comment lancer le nombre arbitraire d'octets de la position arbitraire dans char [x] ?

mise à jour: j'ai oublié de mentionner explicitement que la longueur des données de la chaîne et du flotteur n'est pas connue au moment de la compilation et qu'elle est variable.

44
demandé sur nikitablack 2014-11-10 17:00:26

9 réponses

la voie C, qui fonctionnerait très bien en C++, serait de déclarer une structure:

#pragma pack(1)

struct contents {
   // data members;
};

noter que

  • Vous devez utiliser un pragma pour faire le compilateur aligner les données comme-il-regarde dans la structure;
  • cette technique ne fonctionne qu'avec POD types

et ensuite lancer le tampon de lecture directement dans le type de structure:

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

maintenant si la taille de vos données est variable, vous pouvez vous séparer en plusieurs morceaux. Pour lire un seul objet binaire à partir du buffer, une fonction de lecteur est utile:

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

le principal avantage est qu'un tel lecteur peut être spécialisé pour des objets c++ plus avancés:

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

et maintenant dans votre analyseur principal:

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

Note: comme L'a observé Tony D, même si vous pouvez obtenir l'alignement correctement via les directives #pragma et le rembourrage manuel (si nécessaire), vous pouvez toujours rencontrer une incompatibilité avec l'alignement de votre processeur, sous la forme de (meilleur cas) problèmes de performance ou (pire cas) signaux de trappe. Cette méthode est probablement intéressante seulement si vous avez le contrôle sur le format du fichier.

9
répondu slaphappy 2017-05-23 12:32:28

si ce n'est pas à des fins d'apprentissage, et si vous avez la liberté de choisir le format binaire, vous feriez mieux d'envisager d'utiliser quelque chose comme protobuf qui gérera la sérialisation pour vous et permettra d'interopérer avec d'autres plates-formes et d'autres langues.

si vous ne pouvez pas utiliser L'API d'un tiers, vous pouvez regarder QDataStream comme source d'inspiration

12
répondu fjardon 2014-11-10 14:49:44

actuellement je le fais ainsi:

  • charger le fichier pour ifstream

  • lire ce flux de char tampon[2]

  • cast unsigned short : unsigned short len{ *((unsigned short*)buffer) }; . Maintenant, j'ai la longueur d'une chaîne.

qui dernier risque un SIGBUS (si votre tableau de caractères se produit pour commencer à une adresse impaire et votre CPU ne peut lire que des valeurs 16 bits qui sont alignées à une adresse Pair), performances (certains CPU liront des valeurs mal alignées mais plus lentes; d'autres comme x86s moderne sont fines et rapides) et/ou endianness questions. Je suggère de lire les deux caractères, puis vous pouvez dire (x[0] << 8) | x[1] ou vice versa, en utilisant htons si vous avez besoin de corriger pour l'ennui.

  • lire un flux vers vector<char> et créer un std::string à partir de ce vector . Maintenant, j'ai l'identification.

pas besoin... il suffit de lire directement dans la chaîne:

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

if (input_fstream.read(&s[0], s.size()) &&
    input_stream.gcount() == s.size())
    ...use s...
  • de la même manière read 4 octets, et les jetèrent à unsigned int . Maintenant, j'ai une foulée. while pas la fin du fichier read float s de la même manière - créer un char bufferFloat[4] et coulé *((float*)bufferFloat) pour chaque float .

mieux lire les données directement sur les unsigned int s et floats , car de cette façon le compilateur assurera l'alignement correct.

ça marche, mais pour moi ça a l'air moche. Puis-je lire directement unsigned short ou float ou string etc. sans créer char [x] ? Si non, Quelle est la façon de lancer correctement (j'ai lu ce style que j'utilise - est un vieux style)?

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

notez que le code ci-dessus évite de lire des données dans des tableaux de caractères potentiellement non alignés, où il est dangereux de reinterpret_cast données dans un tableau potentiellement non aligné char (y compris à l'intérieur d'un std::string ) en raison de problèmes d'alignement. Encore une fois, vous pourriez avoir besoin d'une conversion post-lecture avec htonl s'il y a une chance que le contenu du fichier diffère en Enness. S'il y a un nombre inconnu de float s, vous besoin de calculer et d'allouer suffisamment de stockage de l'alignement d'au moins 4 octets, puis but une Data* ... il est légal d'indexer au-delà de la taille de tableau déclarée de y aussi longtemps que le contenu de la mémoire aux adresses consultées faisait partie de l'allocation et détient une représentation valide de float lue à partir du flux. Plus simple - mais avec une lecture supplémentaire donc peut-être plus lent - lire le uint32_t d'abord puis new float[n] et faire un autre read dans y....

pratiquement, ce type d'approche peut fonctionner et beaucoup de bas niveau et de code C fait exactement cela. Les bibliothèques de haut niveau" plus propres " qui pourraient vous aider à lire le fichier doivent finalement faire quelque chose de similaire en interne....

9
répondu Tony Delroy 2018-07-15 01:03:34

j'ai en fait implémenté un analyseur de format binaire rapide et sale pour lire des fichiers .zip (suivant la description de format de Wikipedia) le mois dernier, et étant moderne j'ai décidé d'utiliser des modèles C++.

sur certaines plates-formes spécifiques, un package struct pourrait fonctionner, mais il ya des choses qu'il ne gère pas bien... comme les champs de longueur variable. Avec les modèles, cependant, il n'y a pas un tel problème: vous pouvez obtenir des structures complexes arbitrairement (et retour type.)

a .zip archive est relativement simple, heureusement, donc j'ai mis en œuvre quelque chose de simple. Du haut de ma tête:

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

bien sûr, le OffsetReader de base a en fait un résultat constant:

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

et puisque nous parlons de gabarits, vous pouvez changer les types à loisir (vous pourriez implémenter un lecteur de procuration qui délègue tout lit à un shared_ptr qui les Memoize).

ce qui est intéressant, cependant, est le résultat final:

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

c'est assez simpliste, évidemment, mais incroyablement flexible en même temps.

un axe évident d'amélioration serait d'améliorer chaînage puisqu'il y a ici un risque de chevauchements accidentels. Mon code de lecture d'archives a fonctionné la première fois que je l'ai essayé, ce qui était une preuve suffisante pour moi que ce code était suffisant pour la tâche en question.

5
répondu Matthieu M. 2014-11-12 18:37:35

j'ai dû résoudre ce problème une fois. Les fichiers de données ont été empaquetés pour la sortie FORTRAN. Les alignements étaient faux. J'ai réussi avec des trucs de préprocesseur qui ont fait automatiquement ce que vous faites manuellement: déballer les données brutes d'un tampon octet vers une structure. L'idée est de décrire les données dans un fichier include:

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

Maintenant vous pouvez définir ces macros pour générer le code dont vous avez besoin, dire la déclaration struct, inclure ce qui précède, défaire et définir à nouveau les macros pour générer des fonctions de déballage, suivi d'un autre include, etc.

NB j'ai vu pour la première fois cette technique utilisée dans gcc pour la génération de code liée à un arbre de syntaxe abstraite.

si CPP n'est pas assez puissant (ou un tel abus de préprocesseur n'est pas pour vous), remplacez un petit programme lex/yacc (ou choisissez votre outil préféré).

il est étonnant pour moi combien de fois il paie de penser en termes de génération de code plutôt que de l'écrire à la main, à au moins dans le code des fondations de bas niveau comme celui-ci.

3
répondu Gene 2014-11-10 14:29:36

vous devriez mieux déclarer une structure (avec 1-byte padding - comment - dépend du compilateur). Écrivez en utilisant cette structure, et lisez en utilisant la même structure. Mettez seulement la gousse dans la structure,et donc pas std::string etc. Utilisez cette structure uniquement pour l'entrée/sortie du fichier, ou autre communication inter-processus-utilisez la normale struct ou class pour la conserver pour une utilisation ultérieure dans le programme c++.

2
répondu Ajay 2014-11-10 14:10:52

étant donné que toutes vos données sont variables, vous pouvez lire les deux blocs séparément et toujours utiliser casting:

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_; }
};

de cette façon, vous pouvez utiliser la réponse de M. kbok pour analyser correctement:

const char* buffer = getPointerToDataSomehow();

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

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

// etc.
2
répondu Barry 2014-11-10 14:36:03

je le fais personnellement de cette façon:

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

manière très efficace pour les structures de taille fixe au début du fichier.

2
répondu rev 2014-11-10 21:43:48

utilisez une bibliothèque de sérialisation. Voici quelques-uns:

1
répondu Átila Neves 2014-11-13 14:04:40