2.29. Struct és bináris adatok

A struct modul a Python értékeket egy rögzített bináris elrendezésbe csomagolja, és a bájtokat visszafejtve ismét Python értékekké alakítja. Akkor érdemes elővenni, amikor bináris fájlformátummal, hálózati protokollal vagy rögzített méretű rekordokat cserélő eszközzel dolgozol.

Két függvény lefedi az esetek többségét:

  • struct.pack() – Python értékeket és egy formátumsztringet vesz át, és pontosan a megadott elrendezésű bytes objektumot ad vissza.

  • struct.unpack() – egy formátumsztringet és egy bytes objektumot vesz át, és Python értékek egy tuple-jét adja vissza.

2.29.1. Formátumsztringek

A formátumsztring a rekord minden mezőjéhez egy kódot sorol fel. A kódok írják le az egyes mezők méretét és értelmezését is.

A Python int típusának nincs rögzített mérete – akkorára nő, amekkora értéket hozzárendelsz. A bináris formátumoknak van rögzített méretük: minden egész mező egy előre megállapodott számú bájtot használ. A struct a korlátlan Python egészek és ezek rögzített méretű ábrázolásai között végez átalakítást.

Egy egész szám szélessége a felhasznált bitek száma. Egy bájt nyolc bit. A kisbetűs kód az előjeles változat; a nagybetűs kód az előjel nélküli (csak nem negatív értékek):

  • b / B8 bites (egy bájt). -128..127 előjellel, 0..255 előjel nélkül.

  • h / H16 bites (két bájt). -32768..32767 előjellel, 0..65535 előjel nélkül.

  • i / I32 bites (négy bájt). Hozzávetőleg ±kétmilliárd előjellel, négymilliárd előjel nélkül.

  • q / Q64 bites (nyolc bájt). A mindennapi használatban gyakorlatilag korlátlan.

Válassz olyan szélességet, amely kényelmesen lefedi a várt tartományt. A deklarált tartományon kívüli érték csomagolása a build-től függően vagy némán körbefordul, vagy struct.error kivételt vált ki.

A többi gyakori kód a lebegőpontos számokhoz és a bájtsztringekhez tartozik:

  • f – 32 bites lebegőpontos szám (egyszeres pontosság; körülbelül hét tizedesjegy). A Python szokásos float típusa a MicroPython-on már eleve ekkora, így egy ilyen érték f formátumba csomagolása veszteségmentes.

  • d – 64 bites lebegőpontos szám (kétszeres pontosság; körülbelül tizenöt tizedesjegy). Egy 32 bites MicroPython float d formátumba csomagolása nyolc bájtra szélesíti, de nem ad hozzá pontosságot.

  • s – rögzített hosszúságú bájtsztring, amelyet egy darabszám előz meg (8s egy nyolc bájtos mező esetén).

2.29.2. Bájtsorrend

Egy többbájtos egész szám kétféleképpen tárolható a memóriában. A 0x12345678 szám egy 32 bites mezőben így van elhelyezve:

  • Little-endian – a legkisebb helyiértékű bájt elöl: 78 56 34 12.

  • Big-endian – a legnagyobb helyiértékű bájt elöl: 12 34 56 78.

Mindkettő ugyanazt az értéket kódolja; csupán abban különböznek, hogy a mező melyik vége az alacsony bájt. Az egyik rendszer által írt fájl olvashatatlanná válik a másikon, ha a bájtsorrend nem egyezik.

A formátumsztring kezdő karaktere választja meg a sorrendet:

  • < – little-endian. Gyakori az x86 és az ARM platformokon.

  • > – big-endian. Gyakori a hálózati protokollokban.

  • ! – hálózati sorrend, egyenértékű a > jellel.

Kezdő karakter nélkül a natív bájtsorrendet és a natív igazítást használja a rendszer; a < vagy > explicit megadása megszünteti ezt a kétértelműséget, és általában ez az, amire fájl olvasásakor vagy másik géppel való kommunikációkor szükséged van.

Megjegyzés

Az OpenMV Cam little-endian – ugyanúgy, mint a hozzá csatlakoztatott PC. A kamerán helyileg tárolt fájlokhoz és az asztali géppel oda-vissza utazó bináris adatokhoz használj < jelet a formátumsztringekben. A > (vagy !) jelet a hálózati protokollokhoz, valamint minden olyan formátumhoz használd, amelynek specifikációja big-endian sorrendet ír elő.

Hat bájt egy sorban elhelyezve, az első két bájt egy "H" mezőként (16 bites előjel nélküli) csoportosítva, a következő négy pedig egy "I" mezőként (32 bites előjel nélküli), mindegyik a little-endian bájtsorrendjével felcímkézve.

A "<HI" egy 16 bites értéket, majd egy 32 bites értéket csomagol hat little-endian bájtba.

2.29.3. Csomagolás

import struct

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

Kimenet:

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

A <HI formátum hat bájtot állít elő: kettőt a H mezőnek és négyet az I mezőnek, mind little-endian. Pontosan annyi értéket adj át, amennyit a formátum elvár – az eltérés struct.error kivételt vált ki.

2.29.4. Kicsomagolás

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

Kimenet:

320 1000000

A struct.unpack() mindig tuple-t ad vissza, még akkor is, ha a formátum egyetlen mezőt ír le. Az olvashatóság érdekében ugyanazon a soron csomagold ki.

2.29.5. Rögzített hosszúságú bájtsztringek

Az s kód szó szerint olvas vagy ír egy bájtdarabot. A darabszám az s elé kerül – a 4s jelentése „négy bájt egyetlen bájtsztringként kezelve”. Ez a szokásos módja annak, hogy egy magic értéket, egy rögzített méretű címkét vagy egy kitöltött névmezőt ágyazz be egy rekordba:

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

Kimenet:

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

Az első négy bájt a literál magic b"OMV0"; a következő kettő a H mező (320); az utolsó négy az I mező (1000000). A kicsomagolás a bájtokat bytes objektumként adja vissza:

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

Kimenet:

b'OMV0' 320 1000000

Ha a forrásérték rövidebb a deklarált darabszámnál, az eredmény jobbról \x00 bájtokkal lesz kitöltve; ha hosszabb, a felesleges bájtokat némán eldobja:

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

A darabszám egy bájthossz, nem karakterszám – az s nyers bájtokkal dolgozik, így egy többbájtos karaktereket tartalmazó UTF-8 sztringet először .encode()-olni kell, és bájtokban kell megszámolni.

2.29.6. Méretezés és részleges olvasások

A struct.calcsize() visszaadja, hány bájtot fogyaszt egy formátumsztring:

struct.calcsize("<HI")     # 6

Amikor rekordok folyamát olvasod egy fájlból, rekordonként pontosan ennyi bájtot olvass be:

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)

A fájl végén egy rövid olvasás a record_size méretnél kisebb darabot eredményez – ezt a stream végét jelző feltételként kezeld, ahelyett, hogy megpróbálnál egy részleges rekordot kicsomagolni.