Lire un format binaire en Python avec struct


Une suite de valeurs ne veut rien dire en soi, et même le sacro-saint binaire supposé être le socle de toute l’informatique n’a aucun sens si on ne connaît pas le format utilisé pour ce qu’il doit représenter.

Toujours la même opposition entre données et représentation.

Par exemple, le binaire peut représenter un chiffre en base 2 ou un texte encodé.

Pour autant, cela ne veut pas dire qu’il n’existe pas des formats prépondérant. En informatique, beaucoup de données binaires sont organisées pour correspondre aux structures de données du langage C, ces dernières étant une implémentation du standard IEEE 754 (en effet les strings sont des arrays d’int en C, donc le texte et les nombres sont des suites de chiffres).

Par exemple, si vous créez un array numpy contenant des nombres de 0 à 1000 stockés en int32 et sauvegardez son contenu dans un fichier :

>>> import numpy
>>> numpy.arange(0, 1000, dtype=np.int32).tofile('/tmp/data')

Le fichier va ici contenir une suite de 1 et de 0 représentant 1000 entiers, chacun comme un paquet de 4 octets organisés selon la sémantique que comprend le langage C.

Pour avoir une idée de l’organisation du contenu, on peut prendre un éditeur hexa qui vous affichera :

0000 0000 0100 0000 0200 0000 0300 0000 0400 0000 0500 0000 0600 0000 0700 0000 0800 0000 0900 0000 0a00 0000 0b00 0000 0c00 0000 0d00 0000 0e00 0000 0f00 0000 1000 0000 1100 0000 1200 0000 1300 0000

Ça se lit ainsi :

0000 0000 => 0
0100 0000 => 1
0200 0000 => 2
0300 0000 => 3
0400 0000 => 4
0500 0000 => 5
0600 0000 => 6
0700 0000 => 7
0800 0000 => 8
0900 0000 => 9
0a00 0000 => 10
0b00 0000 => 11
0c00 0000 => 12
0d00 0000 => 13
0e00 0000 => 14
0f00 0000 => 15
1000 0000 => 16
1100 0000 => 17
1200 0000 => 18
1300 0000 => 19
...

Numpy étant codé en C, cela semble plutôt logique qu’il dump tout ça dans ce format.

Mais c’est une représentation tellement courante que de nombreux formats standards l’utilisent. Par exemple, les archives et les images stockent souvent leurs données ainsi.

Prenez le format d’image PNG, la RFC indique que la taille de l’image est stockée dans le fichier sous la forme de deux entiers représentés par 4 octets chacun, ordonnés en big-endian, entre l’octet 16 et l’octet 24.

On peut donc récupérer ces informations en lisant son fichier image :

with open('image.png', 'rb') as f:
    taille = f.read(24)[16:24]

Le problème étant : comment lire cette info ? C’est un blob binaire qui ne veut rien dire pour Python :

print(taille)
b'\x00\x00\x07\x80\x00\x00\x048'

Le module struct est fait pour ça, on lui passe une donnée au format structure C, et il la convertit en type Python. Cela marche ansi, pardon, ainsi :

struct.unpack('motif_du_format_a_convertir', donnee)

Le format à convertir est une chaîne de caractères qui contient des symboles décrivant la structure de la donnée qu’on souhaite récupérer. Little-endian ou big-endian ? String, Int, Bool ?

Pour la taille de la photo, on sait qu’il y a deux entiers, non signés (une taille ne va pas être négative), en big-endian. D’après la doc de struct, on peut lui désigner un entier non signé avec ‘I’, et il faut les qualifier avec ‘>’ pour l’ordre big-endian. Du coup:

taille = struct.unpack('>II', taille)
print(taille)
(1920, 1080)

Il se trouve que mon image de test est un screenshot et que mon écran a une résolution de 1920×1080 :)

On peut faire l’opération inverse avec struct.pack, et bien entendu manipuler des formats plus complexes : il suffit de changer le motif qui représente le format à convertir.

24 thoughts on “Lire un format binaire en Python avec struct

  • Betepoilue

    C’est super cool et en plus dans la stdlib, que demander de plus ? Merci pour l’article !!!!

  • Paradox

    C’est pas plutôt parce que, ce que tu appelles “la représentation en langage C”, se conforme à l’IEEE 754 ?

  • Pypou

    Merci pour cet article.

    Petit patch : “with open(‘image.png’, ‘rb’) f: ” → “with open(‘image.png’, ‘rb’) as f: “

  • Sam Post author

    @Paradox : oui mais si je dis “IEEE 754” à un codeur Python ça lui parlera pas. Même la doc de struct fait très attention à ne mentionner ce standard qu’une fois discrètement.

  • Furankun

    Ah c’est bon ça! quand je pense que je me suis fait chier avec bitstring (qui est déjà vachement pratique, il faut avouer)!

    j’en profite pour lever quelques typos:

    ne veut rien dire en soit -> ne veut rien dire en soi

    des formats prépondérant -> des formats prépondérants

    la sémantique que comprends -> la sémantique que comprend

    il la converti -> il la convertit

    et un il faut les qualifier de -> et il faut les qualifier de

  • Paradox

    @Sam : malgré tout, je trouve ça plus trompeur de dire “langage C” comme si c’était de la faute d’un langage en particulier et pas l’implémentation d’un standard. Je veux bien que sur Python on soit sur du haut niveau, n’empêche que dès que tu sors du cadre “web”, info de gestion, etc… les standards tu as besoin de savoir comment ils sont implémentés (je pense évidemment à Numpy/Scipy, mais également à Astropy, Scikit-learn/image, etc…) et la documentation mets quand même l’accent dessus.

    Bref, “haut niveau” oui, “mettre des oeillères”, pour coder sans savoir, je trouve ça pas glop.

  • Abject

    Unpack est utile je préfère array.array pour des raisons de performances.

    Si le fichier est lourd il est beaucoup plus judicieux d’utiliser array.array pour lire le fichier et obtenir un tableau python.

    J’ai découvert array grâce à cette page : Python Patterns – An Optimization Anecdote (qui en passant est très intéressante).

  • Sam Post author

    @Paradox : pas grave, mon but n’est pas de faire de l’exact, mon but est que les gens puissent être intéressés par l’outil et l’utiliser.

    @Abject: c’est vrai pour l’exemple du fichier numpy par exemple (en supposant qu’on ne veuille pas le relire avec numpy) ou alors les pixels d’une image. Par contre pour des metadata comme la taille de l’image ou l’artiste d’un morceau mp3, alors struct est plus adapté.

  • Moato

    Cool pour convertir des formats binaires en variables Python :)

    Petites typos:

    “un chiffre en base 2” -> “un nombre en base 2”, un chiffre c’est le symbole pour écrire le nombre, par exemple en binaire c’est les chiffres ‘0’ et ‘1’.

    “Ca se lit ainsi:” -> “Ça se lit ainsi:”

    “blog binaire” -> ça serait pas plutôt “blob binaire”?

    Du coup j’en profite pour poser une question, est-ce qu’il y a une manière élégante pour lire ou écrire des données au niveau des bits en Python ? Imaginons que je reçoive une string “OuiAhOuiOuiAhAhOuiOui”, que je veuille transformer les ‘Oui’ en 0 et les ‘Ah’ en 110 pour que cela me donne un nombre écrit en base 2. En C je pourrais utiliser du masquage avec des opérations logiques mais en Python je ne saurais pas trop comment m’y prendre.

  • Sam Post author

    Merci à tous pour les corrections, j’ai fais le ménage.

    @paradox : j’ai ajouté une mention de IEEE 754.

    @Moato : aucune idée.

  • Paradox

    @Sam : Bien plus propre comme ça ! Au moins ça rejoint la “cohérence” des autres articles qui, à défaut de ne pouvoir être exhaustifs, permettent d’aller voir plus loin que les (déjà) très bon tutos que tu nous proposes. :)

    Pour être honnête, ça m”emmerdait que mon 1er commentaire soit un “reproche”, néanmoins, je vais profiter de celui-ci pour faire ce que je souhaitais faire lors de mon 1er commentaire i.e. saluer l’excellent travail qui est fait ici, tant sur la forme que sur le fond. Continue comme ça, Sam (et réveille Max, qui dort au fond) !

  • boblinux

    @Moato

    Au pire, si tu veux vraiment une réponse à ta question, va faire un tour sur http://indexerror.net/questions , j’suis sûr que tu trouveras un tas de mecs (oé, les meufs c’est une espece en voie d’extinction dans le monde du dev) dispo pour répondre à ta question !

    ps : fu..k les balises html

  • buffalo974

    Un conseil ou un tuyau, pour un pythoniste amateur qui voudrait faire un peu de C++, pour améliorer son python, par curiosité et par “plaisir” ?

  • YCL1

    struct est pratique pour convertir rapidement des éléments de même taille, regrouper des blocs pour en calculer le checksum par exemple.

    Mais c’est beaucoup moins aisé quand il s’agit d’extraire des éléments de taille variables, de structurer des éléments.

    Les structures ctypes sont beaucoup plus efficaces pour décoder/décommuter des paquets binaires.

    Une petite démo :

     
    import ctypes
     
    class Header_Decoder(ctypes.BigEndianStructure): # {
        _pack_ = 1
        _fields_ = [
            ('protocol_id', ctypes.c_uint8, 8),      # 1 byte:  byte 1,      bits 00-07
            ('segmentation', ctypes.c_uint8, 3),     # 3 bits:  byte 2,      bits 08-10
            ('transaction_type', ctypes.c_uint8, 5), # 5 bits:  byte 2,      bits 11-15
            ('packet_length', ctypes.c_uint32),      # 4 bytes: bytes 3-6,   bits 16-47
            ('checksum', ctypes.c_uint16),           # 2 bytes: bytes 7-8,   bits 48-63
        ]
    # } Header_Decoder
     
    HEADER_LENGHT_BYTES = ctypes.sizeof(Header_Decoder)
     
    packet = b'xFFx10x00x00x00x0Cx00xE3x48x65x79x21'
     
    header = Header_Decoder.from_buffer_copy(packet[0 : 0 + HEADER_LENGHT_BYTES])
     
    print("Protocol ID:", header.protocol_id)
    print("Segmentation:", header.segmentation)
    print("Transaction type:", header.transaction_type)
    print("Checksum:", header.checksum)
    print("Data:", packet[HEADER_LENGHT_BYTES : header.packet_length].decode('ascii'))
  • cendrieR

    Merci pour l’article !

    Petite question : le lien vers la doc pointe toujours sur python 2.7. Tu ne voulais pas encourager le passage en python 3 ?

  • Sam Post author

    Bien vu. Une des conneries de la PSF est leur volonté de rediriger par défaut sur la doc de la 2.7.

  • kontre

    @YCL1 Le problème de ctypes c’est qu’il n’y a aucun garde-fou, quand ça merde ça segfault python lui-même (ce qui a peu de chance d’arriver avec des uint, certes). Au moins struct est safe. De plus ton exemple se fait assez facilement avec struct.

  • Romain

    C’est vrai que c’est un module assez génial et en même temps c’est dommage que la description du format soit si cryptique (quoique ce ne doit pas tellement pire que des regexes).

    À quand un article sur du décodage asn1 en python :-) ?

  • bizulk

    Salut !

    Merci pour ce petit tuto et merci aussi @YCL1 pour le commentaire.

    Je préfère ctypes pour l’aspect “déclaratif” de la donnée.

    Cela dit j’aimerais bien un outil qui génère la déclaration de la classe(ASN1 si j’ai bien compris) à partir de l’entête du fichier C pour une structure donnée.

    Je pourrai bien partir de pycparser ou pyparsing mais j’ai l’impression de prendre un sentier déjà battu…

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.