2.29. Struct en binaire gegevens

De struct-module verpakt Python-waarden in een vaste binaire indeling en pakt bytes weer uit tot Python-waarden. Gebruik deze module wanneer je werkt met een binair bestandsformaat, een netwerkprotocol of een apparaat dat records met vaste grootte uitwisselt.

Twee functies dekken de meeste gevallen:

  • struct.pack() – neemt Python-waarden en een format string en retourneert een bytes-object met precies de juiste indeling.

  • struct.unpack() – neemt een format string en een bytes-object en retourneert een tuple met Python-waarden.

2.29.1. Format strings

Een format string somt één code per veld in het record op. De codes beschrijven zowel de grootte als de interpretatie van elk veld.

Pythons int heeft geen vaste grootte – hij groeit mee met elke waarde die je toekent. Binaire formaten hebben wel vaste groottes: elk geheeltallig veld gebruikt een afgesproken aantal bytes. struct zet om tussen onbegrensde Python-ints en deze representaties met vaste grootte.

De breedte van een geheel getal is het aantal bits dat het gebruikt. Eén byte is acht bits. De code in kleine letters is de signed-variant; de code in hoofdletters is de unsigned-variant (alleen niet-negatieve waarden):

  • b / B8-bit (één byte). -128..127 signed, 0..255 unsigned.

  • h / H16-bit (twee bytes). -32768..32767 signed, 0..65535 unsigned.

  • i / I32-bit (vier bytes). Ongeveer ±twee miljard signed, vier miljard unsigned.

  • q / Q64-bit (acht bytes). Voor dagelijks gebruik in feite onbegrensd.

Kies een breedte die het verwachte bereik ruim dekt. Het verpakken van een waarde buiten het opgegeven bereik leidt ofwel stilzwijgend tot omslaan (wraparound) ofwel tot een struct.error, afhankelijk van de build.

De overige veelvoorkomende codes zijn voor floats en byte-strings:

  • f – 32-bit float (single precision; ongeveer zeven decimalen). Pythons gewone float op MicroPython heeft al deze grootte, dus het verpakken ervan in f gaat zonder verlies.

  • d – 64-bit float (double precision; ongeveer vijftien decimalen). Het verpakken van een 32-bit MicroPython-float in d verbreedt deze tot acht bytes, maar voegt geen precisie toe.

  • s – byte-string met vaste lengte, voorafgegaan door een aantal (8s voor een veld van acht bytes).

2.29.2. Byte-volgorde

Een geheel getal van meerdere bytes kan op twee manieren in het geheugen worden opgeslagen. Het getal 0x12345678 in een 32-bit veld wordt als volgt geplaatst:

  • Little-endian – minst significante byte eerst: 78 56 34 12.

  • Big-endian – meest significante byte eerst: 12 34 56 78.

Beide coderen dezelfde waarde; ze verschillen alleen over welk uiteinde van het veld de lage byte is. Een bestand dat door het ene systeem is geschreven, wordt onleesbaar wanneer het door het andere wordt gelezen als de byte-volgorde niet overeenkomt.

Het eerste teken van de format string bepaalt de volgorde:

  • < – little-endian. Gebruikelijk op x86 en ARM.

  • > – big-endian. Gebruikelijk in netwerkprotocollen.

  • ! – netwerkvolgorde, gelijk aan >.

Zonder een eerste teken worden de native byte-volgorde en native uitlijning gebruikt; door < of > expliciet in te stellen verwijder je die dubbelzinnigheid, en dat is meestal wat je wilt bij het lezen van een bestand of het communiceren met een andere machine.

Notitie

De OpenMV Cam is little-endian – net als zijn host-pc. Gebruik < in format strings voor camera-lokale bestanden en voor binaire gegevens die van of naar een desktop reizen. Gebruik > (of !) voor netwerkprotocollen en voor elk formaat waarvan de specificatie om big-endian vraagt.

Zes bytes op een rij geplaatst, waarbij de eerste twee bytes gegroepeerd zijn als een "H"-veld (16-bit unsigned) en de volgende vier als een "I"-veld (32-bit unsigned), elk gelabeld met hun little-endian byte-volgorde.

"<HI" verpakt een 16-bit waarde gevolgd door een 32-bit waarde in zes little-endian bytes.

2.29.3. Verpakken

import struct

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

Uitvoer:

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

Het <HI-formaat produceert zes bytes: twee voor het H-veld en vier voor het I-veld, alle little-endian. Geef precies het aantal waarden door dat het formaat verwacht – een verschil veroorzaakt een struct.error.

2.29.4. Uitpakken

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

Uitvoer:

320 1000000

struct.unpack() retourneert altijd een tuple, zelfs wanneer het formaat één enkel veld beschrijft. Pak het op dezelfde regel uit voor de leesbaarheid.

2.29.5. Byte-strings met vaste lengte

De s-code leest of schrijft een stuk bytes letterlijk. Het aantal komt vóór de s4s betekent “vier bytes behandeld als één byte-string”. Dit is de gebruikelijke manier om een magic value, een tag met vaste grootte of een opgevuld naamveld in een record op te nemen:

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

Uitvoer:

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

De eerste vier bytes zijn de letterlijke magic b"OMV0"; de volgende twee zijn het H-veld (320); de laatste vier zijn het I-veld (1000000). Bij het uitpakken worden de bytes als een bytes-object teruggegeven:

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

Uitvoer:

b'OMV0' 320 1000000

Als de bronwaarde korter is dan het opgegeven aantal, wordt het resultaat aan de rechterkant opgevuld met \x00; als deze langer is, worden de overtollige bytes stilzwijgend weggelaten:

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

Het aantal is een bytelengte, geen aantal tekens – s werkt met ruwe bytes, dus een UTF-8-string met multi-byte tekens moet eerst worden ge-.encode()‘d en in bytes worden geteld.

2.29.6. Grootte bepalen en gedeeltelijke reads

struct.calcsize() retourneert het aantal bytes dat een format string verbruikt:

struct.calcsize("<HI")     # 6

Lees bij het lezen van een stroom records uit een bestand precies dat aantal bytes per record:

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)

Een korte read aan het einde van het bestand levert een stuk op dat kleiner is dan record_size – behandel dat als de end-of-stream-conditie in plaats van te proberen een gedeeltelijk record uit te pakken.