2.29. Struktury i dane binarne

Moduł struct pakuje wartości Pythona do stałego układu binarnego oraz rozpakowuje bajty z powrotem do wartości Pythona. Sięgnij po niego, gdy pracujesz z binarnym formatem pliku, protokołem sieciowym lub urządzeniem wymieniającym rekordy o stałym rozmiarze.

Większość przypadków obejmują dwie funkcje:

  • struct.pack() – przyjmuje wartości Pythona oraz łańcuch formatujący i zwraca obiekt bytes o dokładnie określonym układzie.

  • struct.unpack() – przyjmuje łańcuch formatujący oraz obiekt bytes i zwraca krotkę wartości Pythona.

2.29.1. Łańcuchy formatujące

Łańcuch formatujący zawiera jeden kod na każde pole rekordu. Kody opisują zarówno rozmiar, jak i interpretację każdego pola.

Typ int w Pythonie nie ma stałego rozmiaru – rośnie, aby pomieścić każdą przypisaną wartość. Formaty binarne mają stałe rozmiary: każde pole całkowitoliczbowe używa uzgodnionej liczby bajtów. Moduł struct konwertuje pomiędzy nieograniczonymi liczbami całkowitymi Pythona a tymi reprezentacjami o stałym rozmiarze.

Szerokość liczby całkowitej to liczba bitów, których używa. Jeden bajt to osiem bitów. Kod małą literą oznacza wariant ze znakiem; kod wielką literą oznacza wariant bez znaku (tylko wartości nieujemne):

  • b / B8-bitowy (jeden bajt). -128..127 ze znakiem, 0..255 bez znaku.

  • h / H16-bitowy (dwa bajty). -32768..32767 ze znakiem, 0..65535 bez znaku.

  • i / I32-bitowy (cztery bajty). Około ±dwa miliardy ze znakiem, cztery miliardy bez znaku.

  • q / Q64-bitowy (osiem bajtów). Praktycznie nieograniczony do codziennego użytku.

Wybierz szerokość, która z zapasem pokrywa oczekiwany zakres. Spakowanie wartości spoza zadeklarowanego zakresu albo po cichu zawija się, albo zgłasza struct.error, w zależności od kompilacji.

Pozostałe powszechne kody dotyczą liczb zmiennoprzecinkowych i łańcuchów bajtów:

  • f – 32-bitowa liczba zmiennoprzecinkowa (pojedyncza precyzja; około siedmiu cyfr dziesiętnych). Zwykły typ float w MicroPython ma już ten rozmiar, więc spakowanie go do f jest bezstratne.

  • d – 64-bitowa liczba zmiennoprzecinkowa (podwójna precyzja; około piętnastu cyfr dziesiętnych). Spakowanie 32-bitowego typu float z MicroPython do d rozszerza go do ośmiu bajtów, ale nie dodaje precyzji.

  • s – łańcuch bajtów o stałej długości, poprzedzony liczbą (8s dla ośmiobajtowego pola).

2.29.2. Kolejność bajtów

Wielobajtową liczbę całkowitą można przechować w pamięci na dwa sposoby. Liczba 0x12345678 w polu 32-bitowym jest ułożona w następujący sposób:

  • Little-endian – najmniej znaczący bajt jako pierwszy: 78 56 34 12.

  • Big-endian – najbardziej znaczący bajt jako pierwszy: 12 34 56 78.

Oba zapisują tę samą wartość; różnią się jedynie tym, który koniec pola jest młodszym bajtem. Plik zapisany przez jeden system zostanie zniekształcony przy odczycie przez inny, jeśli kolejność bajtów się nie zgadza.

Wiodący znak łańcucha formatującego określa kolejność:

  • < – little-endian. Powszechny na x86 i ARM.

  • > – big-endian. Powszechny w protokołach sieciowych.

  • ! – kolejność sieciowa, równoważna >.

Bez wiodącego znaku używane są natywna kolejność bajtów i natywne wyrównanie; jawne ustawienie < lub > usuwa tę niejednoznaczność i jest zwykle tym, czego chcesz przy odczycie pliku lub komunikacji z inną maszyną.

Informacja

OpenMV Cam jest little-endian – tak samo jak jego komputer host. Używaj < w łańcuchach formatujących dla plików lokalnych kamery oraz dla danych binarnych przesyłanych do lub z komputera stacjonarnego. Używaj > (lub !) dla protokołów sieciowych oraz dla każdego formatu, którego specyfikacja wymaga big-endian.

Sześć bajtów ułożonych w rzędzie, gdzie pierwsze dwa bajty zgrupowano jako pole "H" (16-bitowe bez znaku), a kolejne cztery jako pole "I" (32-bitowe bez znaku), każde opisane swą kolejnością bajtów little-endian.

"<HI" pakuje wartość 16-bitową, a po niej wartość 32-bitową do sześciu bajtów w kolejności little-endian.

2.29.3. Pakowanie

import struct

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

Wyjście:

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

Format <HI tworzy sześć bajtów: dwa dla pola H i cztery dla pola I, wszystkie w kolejności little-endian. Przekaż dokładnie tyle wartości, ile oczekuje format – niezgodność zgłasza struct.error.

2.29.4. Rozpakowywanie

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

Wyjście:

320 1000000

struct.unpack() zawsze zwraca krotkę, nawet gdy format opisuje pojedyncze pole. Rozpakuj ją w tej samej linii dla czytelności.

2.29.5. Łańcuchy bajtów o stałej długości

Kod s odczytuje lub zapisuje fragment bajtów dosłownie. Liczba znajduje się przed s4s oznacza „cztery bajty traktowane jako pojedynczy łańcuch bajtów”. To zwykły sposób na osadzenie wartości magicznej, znacznika o stałym rozmiarze lub dopełnionego pola nazwy w rekordzie:

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

Wyjście:

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

Pierwsze cztery bajty to dosłowna wartość magiczna b"OMV0"; kolejne dwa to pole H (320); ostatnie cztery to pole I (1000000). Rozpakowanie zwraca bajty z powrotem jako obiekt bytes:

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

Wyjście:

b'OMV0' 320 1000000

Jeśli wartość źródłowa jest krótsza niż zadeklarowana liczba, wynik jest dopełniany z prawej strony za pomocą \x00; jeśli dłuższa, nadmiarowe bajty są po cichu odrzucane:

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

Liczba określa długość w bajtach, a nie liczbę znaków – s operuje na surowych bajtach, więc łańcuch UTF-8 ze znakami wielobajtowymi trzeba najpierw zakodować metodą .encode() i policzyć w bajtach.

2.29.6. Określanie rozmiaru i częściowe odczyty

struct.calcsize() zwraca liczbę bajtów, którą zajmuje łańcuch formatujący:

struct.calcsize("<HI")     # 6

Przy odczycie strumienia rekordów z pliku odczytuj dokładnie tyle bajtów na rekord:

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)

Skrócony odczyt na końcu pliku daje fragment mniejszy niż record_size – potraktuj to jako warunek końca strumienia, zamiast próbować rozpakować niepełny rekord.