2.29. Struct ונתונים בינאריים

המודול struct אורז ערכי Python לתוך מבנה בינארי קבוע ומפענח בתים בחזרה לערכי Python. השתמשו בו כשעובדים עם פורמט קובץ בינארי, פרוטוקול רשת, או התקן שמחליף רשומות בגודל קבוע.

שתי פונקציות מכסות את רוב המקרים:

  • struct.pack() – מקבלת ערכי Python ומחרוזת פורמט, ומחזירה אובייקט bytes בעל המבנה המדויק.

  • struct.unpack() – מקבלת מחרוזת פורמט ואובייקט bytes, ומחזירה tuple של ערכי Python.

2.29.1. מחרוזות פורמט

מחרוזת פורמט מפרטת קוד אחד לכל שדה ברשומה. הקודים מתארים גם את הגודל וגם את הפרשנות של כל שדה.

ל-int של Python אין גודל קבוע – הוא גדל כדי להכיל כל ערך שתשייכו לו. לפורמטים בינאריים כן יש גדלים קבועים: כל שדה מספר שלם משתמש במספר בתים מוסכם. struct ממיר בין מספרים שלמים בלתי חסומים של Python לבין ייצוגים אלה בגודל קבוע.

הרוחב של מספר שלם הוא מספר הביטים שבהם הוא משתמש. בית אחד הוא שמונה ביטים. הקוד באות קטנה הוא הווריאנט עם סימן; הקוד באות גדולה הוא חסר הסימן (ערכים אי-שליליים בלבד):

  • b / B8 ביט (בית אחד). -128..127 עם סימן, 0..255 ללא סימן.

  • h / H16 ביט (שני בתים). -32768..32767 עם סימן, 0..65535 ללא סימן.

  • i / I32 ביט (ארבעה בתים). בערך ±שני מיליארד עם סימן, ארבעה מיליארד ללא סימן.

  • q / Q64 ביט (שמונה בתים). למעשה בלתי חסום לשימוש יומיומי.

בחרו רוחב שמכסה בנוחות את הטווח שאתם מצפים לו. אריזת ערך מחוץ לטווח המוצהר או עוטפת מחדש בשקט או מעלה struct.error, תלוי ב-build.

הקודים הנפוצים הנותרים הם עבור מספרים ממשיים ומחרוזות בתים:

  • f – מספר ממשי של 32 ביט (דיוק יחיד; בערך שבע ספרות עשרוניות). ה-float הרגיל של Python ב-MicroPython הוא כבר בגודל הזה, ולכן אריזה שלו ל-f היא ללא אובדן.

  • d – מספר ממשי של 64 ביט (דיוק כפול; בערך חמש-עשרה ספרות עשרוניות). אריזת float של 32 ביט מ-MicroPython ל-d מרחיבה אותו לשמונה בתים אך אינה מוסיפה דיוק.

  • s – מחרוזת בתים באורך קבוע, קודמת לה ספירה (8s עבור שדה בן שמונה בתים).

2.29.2. סדר בתים

מספר שלם רב-בתים יכול להישמר בזיכרון בשתי דרכים. המספר 0x12345678 בשדה של 32 ביט מסודר כך:

  • Little-endian – הבית הפחות משמעותי ראשון: 78 56 34 12.

  • Big-endian – הבית המשמעותי ביותר ראשון: 12 34 56 78.

שניהם מקודדים את אותו ערך; הם רק חלוקים על איזה קצה של השדה הוא הבית הנמוך. קובץ שנכתב על ידי מערכת אחת ייקרא משובש על ידי האחרת אם סדר הבתים אינו תואם.

התו המוביל של מחרוזת הפורמט בוחר את הסדר:

  • < – little-endian. נפוץ ב-x86 וב-ARM.

  • > – big-endian. נפוץ בפרוטוקולי רשת.

  • ! – סדר רשת, שווה-ערך ל->.

ללא תו מוביל, נעשה שימוש בסדר הבתים והיישור המקוריים; הגדרת < או > במפורש מסירה את העמימות הזו והיא בדרך כלל מה שתרצו בקריאת קובץ או בתקשורת עם מכונה אחרת.

הערה

ה-OpenMV Cam הוא little-endian – בדיוק כמו המחשב המארח שלו. השתמשו ב-< במחרוזות פורמט עבור קבצים מקומיים של המצלמה ועבור נתונים בינאריים שעוברים אל מחשב שולחני או ממנו. השתמשו ב-> (או !) עבור פרוטוקולי רשת ועבור כל פורמט שהמפרט שלו דורש big-endian.

Six bytes laid out in a row, with the first two bytes grouped as an "H" field (16-bit unsigned) and the next four as an "I" field (32-bit unsigned), each labelled with their little-endian byte order.

"<HI" אורז ערך של 16 ביט ואחריו ערך של 32 ביט לתוך שישה בתים בסדר little-endian.

2.29.3. אריזה

import struct

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

פלט:

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

הפורמט <HI מייצר שישה בתים: שניים עבור שדה ה-H וארבעה עבור שדה ה-I, כולם little-endian. העבירו בדיוק את מספר הערכים שהפורמט מצפה לו – אי-התאמה מעלה struct.error.

2.29.4. פירוק אריזה

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

פלט:

320 1000000

struct.unpack() תמיד מחזירה tuple, גם כאשר הפורמט מתאר שדה יחיד. פרקו אותו באותה שורה לשם קריאוּת.

2.29.5. מחרוזות בתים באורך קבוע

הקוד s קורא או כותב נתח בתים מילולית. הספירה מופיעה לפני ה-s4s פירושו ”ארבעה בתים המטופלים כמחרוזת בתים יחידה“. זוהי הדרך הרגילה להטמיע ערך קסם, תגית בגודל קבוע, או שדה שם מרופד ברשומה:

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

פלט:

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

ארבעת הבתים הראשונים הם ערך הקסם המילולי b"OMV0"; השניים הבאים הם שדה ה-H (320); הארבעה האחרונים הם שדה ה-I (1000000). פירוק האריזה מחזיר את הבתים בחזרה כאובייקט bytes:

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

פלט:

b'OMV0' 320 1000000

אם ערך המקור קצר מהספירה המוצהרת, התוצאה מרופדת מימין ב-\x00; אם הוא ארוך יותר, הבתים העודפים מושמטים בשקט:

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

הספירה היא אורך בבתים, לא ספירת תווים – s עוסק בבתים גולמיים, ולכן מחרוזת UTF-8 עם תווים רב-בתיים צריכה להיות מקודדת תחילה באמצעות .encode() ולהיספר בבתים.

2.29.6. קביעת גדלים וקריאות חלקיות

struct.calcsize() מחזירה את מספר הבתים שמחרוזת פורמט צורכת:

struct.calcsize("<HI")     # 6

בעת קריאת זרם רשומות מקובץ, קראו בדיוק את מספר הבתים הזה לכל רשומה:

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)

קריאה קצרה בסוף הקובץ מייצרת נתח קטן מ-record_size – התייחסו לכך כתנאי סוף-הזרם במקום לנסות לפרק רשומה חלקית.