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 unstd::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.
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.
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
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 unstd::string
à partir de cevector
. 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 fichierread
float
s de la même manière - créer unchar bufferFloat[4]
et coulé*((float*)bufferFloat)
pour chaquefloat
.
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
oufloat
oustring
etc. sans créerchar [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....
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.
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.
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++.
é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.
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.
utilisez une bibliothèque de sérialisation. Voici quelques-uns:
- augmenter la sérialisation et Boost fusion
- Céréales (ma propre bibliothèque)
- une Autre bibliothèque appelée céréales (même nom que le mien, mais le mien est antérieure à la sienne)
- Cap'n Proto