2.29. Struct și date binare

Modulul struct împachetează valori Python într-un format binar fix și despachetează octeții înapoi în valori Python. Apelează la el atunci când lucrezi cu un format de fișier binar, un protocol de rețea sau un dispozitiv care schimbă înregistrări de dimensiune fixă.

Două funcții acoperă majoritatea cazurilor:

  • struct.pack() – preia valori Python și un șir de format, returnând un obiect bytes cu exact acel format.

  • struct.unpack() – preia un șir de format și un obiect bytes, returnând un tuplu de valori Python.

2.29.1. Șiruri de format

Un șir de format enumeră câte un cod pentru fiecare câmp din înregistrare. Codurile descriu atât dimensiunea, cât și interpretarea fiecărui câmp.

Tipul int din Python nu are o dimensiune fixă – crește pentru a încăpea orice valoare îi atribui. Formatele binare au dimensiuni fixe: fiecare câmp întreg folosește un număr convenit de octeți. struct face conversia între întregii Python nelimitați și aceste reprezentări de dimensiune fixă.

Lățimea unui întreg este numărul de biți pe care îi folosește. Un octet are opt biți. Codul cu literă mică este varianta cu semn; codul cu literă mare este cea fără semn (doar valori nenegative):

  • b / B8 biți (un octet). -128..127 cu semn, 0..255 fără semn.

  • h / H16 biți (doi octeți). -32768..32767 cu semn, 0..65535 fără semn.

  • i / I32 de biți (patru octeți). Aproximativ ±două miliarde cu semn, patru miliarde fără semn.

  • q / Q64 de biți (opt octeți). Practic nelimitat pentru uzul zilnic.

Alege o lățime care acoperă confortabil intervalul pe care îl aștepți. Împachetarea unei valori în afara intervalului declarat fie se reia silențios prin depășire (wrap-around), fie generează struct.error, în funcție de build.

Celelalte coduri uzuale sunt pentru numere în virgulă mobilă și șiruri de octeți:

  • f – float pe 32 de biți (precizie simplă; aproximativ șapte cifre zecimale). Tipul obișnuit float din MicroPython are deja această dimensiune, deci împachetarea lui într-un f se face fără pierderi.

  • d – float pe 64 de biți (precizie dublă; aproximativ cincisprezece cifre zecimale). Împachetarea unui float de 32 de biți din MicroPython într-un d îl extinde la opt octeți, dar nu adaugă precizie.

  • s – șir de octeți de lungime fixă, precedat de un contor (8s pentru un câmp de opt octeți).

2.29.2. Ordinea octeților

Un întreg pe mai mulți octeți poate fi stocat în memorie în două moduri. Numărul 0x12345678 într-un câmp pe 32 de biți este dispus astfel:

  • Little-endian – octetul cel mai puțin semnificativ primul: 78 56 34 12.

  • Big-endian – octetul cel mai semnificativ primul: 12 34 56 78.

Ambele codifică aceeași valoare; diferă doar prin capătul câmpului la care se află octetul inferior. Un fișier scris de un sistem apare deformat la citirea de către celălalt dacă ordinea octeților nu se potrivește.

Caracterul de la începutul șirului de format alege ordinea:

  • < – little-endian. Frecvent pe x86 și ARM.

  • > – big-endian. Frecvent în protocoalele de rețea.

  • ! – ordinea de rețea, echivalentă cu >.

Fără un caracter inițial, se folosesc ordinea nativă a octeților și alinierea nativă; setarea explicită a lui < sau > elimină acea ambiguitate și este de obicei ceea ce îți dorești atunci când citești un fișier sau comunici cu o altă mașină.

Notă

OpenMV Cam este little-endian – la fel ca PC-ul gazdă. Folosește < în șirurile de format pentru fișierele locale ale camerei și pentru datele binare care circulă către sau dinspre un desktop. Folosește > (sau !) pentru protocoalele de rețea și pentru orice format a cărui specificație impune big-endian.

Șase octeți dispuși pe un rând, cu primii doi octeți grupați ca un câmp "H" (pe 16 biți, fără semn) și următorii patru ca un câmp "I" (pe 32 de biți, fără semn), fiecare etichetat cu ordinea lor de octeți little-endian.

"<HI" împachetează o valoare pe 16 biți urmată de o valoare pe 32 de biți în șase octeți little-endian.

2.29.3. Împachetare

import struct

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

Ieșire:

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

Formatul <HI produce șase octeți: doi pentru câmpul H și patru pentru câmpul I, toți little-endian. Transmite exact numărul de valori pe care îl așteaptă formatul – o nepotrivire generează struct.error.

2.29.4. Despachetare

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

Ieșire:

320 1000000

struct.unpack() returnează întotdeauna un tuplu, chiar și atunci când formatul descrie un singur câmp. Despachetează-l pe aceeași linie pentru lizibilitate.

2.29.5. Șiruri de octeți de lungime fixă

Codul s citește sau scrie o secvență de octeți ca atare. Contorul se pune înaintea lui s4s înseamnă „patru octeți tratați ca un singur șir de octeți”. Acesta este modul obișnuit de a încorpora o valoare magică, o etichetă de dimensiune fixă sau un câmp de nume completat cu spațiu într-o înregistrare:

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

Ieșire:

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

Primii patru octeți sunt valoarea magică literală b"OMV0"; următorii doi sunt câmpul H (320); ultimii patru sunt câmpul I (1000000). Despachetarea returnează octeții înapoi ca obiect bytes:

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

Ieșire:

b'OMV0' 320 1000000

Dacă valoarea sursă este mai scurtă decât contorul declarat, rezultatul este completat la dreapta cu \x00; dacă este mai lungă, octeții în plus sunt eliminați silențios:

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

Contorul este o lungime în octeți, nu un număr de caractere – s operează cu octeți bruți, așa că un șir UTF-8 cu caractere pe mai mulți octeți trebuie mai întâi codificat cu .encode() și contorizat în octeți.

2.29.6. Dimensionare și citiri parțiale

struct.calcsize() returnează numărul de octeți pe care îi consumă un șir de format:

struct.calcsize("<HI")     # 6

Când citești un flux de înregistrări dintr-un fișier, citește exact acel număr de octeți pentru fiecare înregistrare:

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)

O citire incompletă la sfârșitul fișierului produce o secvență mai mică decât record_size – tratează acest lucru ca pe condiția de sfârșit al fluxului, în loc să încerci să despachetezi o înregistrare parțială.