2.29. Struct et données binaires

Le module struct regroupe des valeurs Python dans une disposition binaire de taille fixe et décode des octets pour retrouver des valeurs Python. Utilisez-le lorsque vous travaillez avec un format de fichier binaire, un protocole réseau ou un périphérique qui échange des enregistrements de taille fixe.

Deux fonctions couvrent la plupart des cas :

  • struct.pack() – prend des valeurs Python et une chaîne de format, et renvoie un objet bytes ayant exactement la disposition voulue.

  • struct.unpack() – prend une chaîne de format et un objet bytes, et renvoie un tuple de valeurs Python.

2.29.1. Chaînes de format

Une chaîne de format liste un code par champ de l’enregistrement. Les codes décrivent à la fois la taille et l’interprétation de chaque champ.

Le type int de Python n’a pas de taille fixe – il s’agrandit pour contenir n’importe quelle valeur que vous lui affectez. Les formats binaires, eux, ont une taille fixe : chaque champ entier utilise un nombre d’octets convenu. struct effectue la conversion entre les entiers Python non bornés et ces représentations de taille fixe.

La largeur d’un entier est le nombre de bits qu’il utilise. Un octet vaut huit bits. Le code en minuscule est la variante signée ; le code en majuscule est la variante non signée (uniquement des valeurs positives ou nulles) :

  • b / B8 bits (un octet). -128..127 signé, 0..255 non signé.

  • h / H16 bits (deux octets). -32768..32767 signé, 0..65535 non signé.

  • i / I32 bits (quatre octets). Environ ±deux milliards en signé, quatre milliards en non signé.

  • q / Q64 bits (huit octets). Effectivement non borné pour un usage courant.

Choisissez une largeur qui couvre confortablement la plage que vous attendez. Empaqueter une valeur en dehors de la plage déclarée entraîne soit un débordement silencieux par enroulement, soit la levée de struct.error, selon la version compilée.

Les autres codes courants concernent les flottants et les chaînes d’octets :

  • f – flottant 32 bits (simple précision ; environ sept chiffres décimaux). Le type float standard de Python sur MicroPython a déjà cette taille, donc en empaqueter un dans f se fait sans perte.

  • d – flottant 64 bits (double précision ; environ quinze chiffres décimaux). Empaqueter un float MicroPython de 32 bits dans d l’élargit à huit octets mais n’ajoute aucune précision.

  • s – chaîne d’octets de longueur fixe, précédée d’un compteur (8s pour un champ de huit octets).

2.29.2. Ordre des octets

Un entier multi-octets peut être stocké en mémoire de deux façons. Le nombre 0x12345678 dans un champ de 32 bits est disposé ainsi :

  • Petit-boutiste (little-endian) – l’octet de poids faible en premier : 78 56 34 12.

  • Gros-boutiste (big-endian) – l’octet de poids fort en premier : 12 34 56 78.

Les deux encodent la même valeur ; ils diffèrent seulement sur l’extrémité du champ où se trouve l’octet de poids faible. Un fichier écrit par un système est illisible lorsqu’il est lu par l’autre si l’ordre des octets ne correspond pas.

Le premier caractère de la chaîne de format choisit l’ordre :

  • < – petit-boutiste. Courant sur x86 et ARM.

  • > – gros-boutiste. Courant dans les protocoles réseau.

  • ! – ordre réseau, équivalent à >.

Sans caractère de tête, l’ordre des octets natif et l’alignement natif sont utilisés ; spécifier explicitement < ou > lève cette ambiguïté et c’est généralement ce que vous voulez lors de la lecture d’un fichier ou de la communication avec une autre machine.

Note

L’OpenMV Cam est petit-boutiste – comme son PC hôte. Utilisez < dans les chaînes de format pour les fichiers locaux à la caméra et pour les données binaires qui transitent depuis ou vers un ordinateur de bureau. Utilisez > (ou !) pour les protocoles réseau et pour tout format dont la spécification exige le gros-boutiste.

Six octets disposés en ligne, les deux premiers octets regroupés en un champ « H » (16 bits non signé) et les quatre suivants en un champ « I » (32 bits non signé), chacun étiqueté selon leur ordre d'octets petit-boutiste.

"<HI" empaquette une valeur de 16 bits suivie d’une valeur de 32 bits en six octets petit-boutistes.

2.29.3. Empaquetage

import struct

blob = struct.pack("<HI", 320, 1000000)
print(blob, len(blob))

Sortie

b'@\x01@B\x0f\x00' 6

Le format <HI produit six octets : deux pour le champ H et quatre pour le champ I, le tout en petit-boutiste. Passez exactement le nombre de valeurs attendu par le format – une incohérence lève struct.error.

2.29.4. Décodage

width, count = struct.unpack("<HI", blob)
print(width, count)

Sortie

320 1000000

struct.unpack() renvoie toujours un tuple, même lorsque le format ne décrit qu’un seul champ. Décodez-le sur la même ligne pour plus de lisibilité.

2.29.5. Chaînes d’octets de longueur fixe

Le code s lit ou écrit un bloc d’octets tel quel. Le compteur se place avant le s4s signifie « quatre octets traités comme une seule chaîne d’octets ». C’est la manière habituelle d’intégrer une valeur magique, une étiquette de taille fixe ou un champ de nom complété dans un enregistrement :

header = struct.pack("<4sHI", b"OMV0", 320, 1000000)
print(header)

Sortie

b'OMV0@\x01@B\x0f\x00'

Les quatre premiers octets sont la valeur magique littérale b"OMV0" ; les deux suivants sont le champ H (320) ; les quatre derniers sont le champ I (1000000). Le décodage renvoie les octets sous forme d’un objet bytes :

magic, width, count = struct.unpack("<4sHI", header)
print(magic, width, count)

Sortie

b'OMV0' 320 1000000

Si la valeur source est plus courte que le compteur déclaré, le résultat est complété à droite par des \x00 ; si elle est plus longue, les octets excédentaires sont silencieusement supprimés :

struct.pack("4s", b"hi")        # b'hi\x00\x00'
struct.pack("4s", b"toolong")   # b'tool'

Le compteur est une longueur en octets, et non un nombre de caractères – s manipule des octets bruts, donc une chaîne UTF-8 contenant des caractères multi-octets doit d’abord être encodée avec .encode() et comptée en octets.

2.29.6. Dimensionnement et lectures partielles

struct.calcsize() renvoie le nombre d’octets consommés par une chaîne de format :

struct.calcsize("<HI")     # 6

Lors de la lecture d’un flux d’enregistrements depuis un fichier, lisez exactement ce nombre d’octets par enregistrement :

record_size = struct.calcsize("<HI")
with open("data.bin", "rb") as f:
    while True:
        chunk = f.read(record_size)
        if len(chunk) < record_size:
            break
        width, count = struct.unpack("<HI", chunk)
        print(width, count)

Une lecture incomplète en fin de fichier produit un bloc plus petit que record_size – considérez cela comme la condition de fin de flux plutôt que de tenter de décoder un enregistrement partiel.