2.29. البنية والبيانات الثنائية

تقوم وحدة struct بتعبئة قيم Python في تخطيط ثنائي ثابت وفك ترميز البايتات لإعادتها إلى قيم Python. استخدمها عند التعامل مع تنسيق ملف ثنائي أو بروتوكول شبكة أو جهاز يتبادل سجلات ذات حجم ثابت.

تغطي دالتان معظم الحالات:

  • struct.pack() -- تأخذ قيم Python وسلسلة تنسيق، وتعيد كائن bytes بالتخطيط المضبوط.

  • struct.unpack() -- تأخذ سلسلة تنسيق وكائن bytes، وتعيد صفًا (tuple) من قيم Python.

2.29.1. سلاسل التنسيق

تسرد سلسلة التنسيق رمزًا واحدًا لكل حقل في السجل. تصف الرموز كلًا من حجم كل حقل وطريقة تفسيره.

ليس لنوع int في Python حجم ثابت -- فهو ينمو ليتسع لأي قيمة تسندها إليه. أما التنسيقات الثنائية فلها أحجام ثابتة فعلًا: يستخدم كل حقل عدد صحيح عددًا متفقًا عليه من البايتات. تقوم وحدة struct بالتحويل بين أعداد Python الصحيحة غير المحدودة وهذه التمثيلات ذات الحجم الثابت.

عرض العدد الصحيح هو عدد البتات التي يستخدمها. البايت الواحد ثماني بتات. الرمز بالأحرف الصغيرة هو الصيغة ذات الإشارة؛ والرمز بالأحرف الكبيرة هو الصيغة عديمة الإشارة (القيم غير السالبة فقط):

  • b / B -- 8 بتات (بايت واحد). -128..127 مع إشارة، 0..255 دون إشارة.

  • h / H -- 16 بت (بايتان). -32768..32767 مع إشارة، 0..65535 دون إشارة.

  • i / I -- 32 بت (أربعة بايتات). نحو ±ملياري قيمة مع إشارة، وأربعة مليارات دون إشارة.

  • q / Q -- 64 بت (ثمانية بايتات). غير محدود عمليًا للاستخدام اليومي.

اختر عرضًا يغطي بأريحية النطاق الذي تتوقعه. تعبئة قيمة خارج النطاق المعلن إما أن تلتف بصمت أو ترفع الاستثناء struct.error، وذلك بحسب البناء.

الرموز الشائعة المتبقية مخصصة للأعداد العشرية وسلاسل البايتات:

  • f -- عدد عشري 32 بت (دقة مفردة؛ نحو سبعة أرقام عشرية). نوع float العادي في 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.

كلتاهما تشفّران القيمة نفسها؛ ولا تختلفان إلا في أي طرف من الحقل هو البايت المنخفض. الملف الذي يكتبه نظام ما يخرج مشوّهًا حين يقرؤه نظام آخر إذا لم يتطابق ترتيب البايتات.

يحدد الحرف الأول من سلسلة التنسيق الترتيب:

  • < -- النهاية الصغرى. شائعة في معماريتي x86 و ARM.

  • > -- النهاية الكبرى. شائعة في بروتوكولات الشبكات.

  • ! -- ترتيب الشبكة، مكافئ لـ >.

بدون حرف أول، يُستخدم ترتيب البايتات الأصلي والمحاذاة الأصلية؛ وتحديد < أو > صراحةً يزيل هذا الالتباس وهو عادةً ما تريده عند قراءة ملف أو التواصل مع جهاز آخر.

ملاحظة

كاميرا OpenMV Cam ذات نهاية صغرى -- تمامًا كحاسوبها المضيف. استخدم < في سلاسل التنسيق للملفات المحلية للكاميرا وللبيانات الثنائية التي تنتقل من حاسوب مكتبي أو إليه. استخدم > (أو !) لبروتوكولات الشبكات ولأي تنسيق تستدعي مواصفاته النهاية الكبرى.

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 بت في ستة بايتات ذات نهاية صغرى.

2.29.3. التعبئة

import struct

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

الناتج:

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

ينتج التنسيق <HI ستة بايتات: اثنان لحقل H وأربعة لحقل I، جميعها ذات نهاية صغرى. مرّر عدد القيم الذي يتوقعه التنسيق بالضبط -- فأي عدم تطابق يرفع الاستثناء struct.error.

2.29.4. فك التعبئة

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

الناتج:

320 1000000

تعيد struct.unpack() دائمًا صفًا (tuple)، حتى عندما يصف التنسيق حقلًا واحدًا. افكك تعبئته في السطر نفسه لتسهيل القراءة.

2.29.5. سلاسل البايتات ذات الطول الثابت

يقرأ الرمز s أو يكتب جزءًا من البايتات حرفيًا. يأتي العدد قبل الرمز s -- فـ 4s تعني "أربعة بايتات تُعامَل كسلسلة بايتات واحدة". هذه هي الطريقة المعتادة لتضمين قيمة سحرية أو علامة ثابتة الحجم أو حقل اسم مبطّن داخل سجل:

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 -- عامِل ذلك على أنه شرط نهاية الدفق بدلًا من محاولة فك تعبئة سجل جزئي.