2.29. Struct và dữ liệu nhị phân¶
Mô-đun struct đóng gói các giá trị Python thành một bố cục nhị phân cố định và giải nén các byte trở lại thành các giá trị Python. Hãy dùng nó khi làm việc với định dạng tệp nhị phân, giao thức mạng, hoặc thiết bị trao đổi các bản ghi có kích thước cố định.
Hai hàm bao phủ hầu hết các trường hợp:
struct.pack()-- nhận các giá trị Python và một chuỗi định dạng, trả về đối tượngbytesvới bố cục chính xác.struct.unpack()-- nhận một chuỗi định dạng và đối tượngbytes, trả về một tuple các giá trị Python.
2.29.1. Chuỗi định dạng¶
Một chuỗi định dạng liệt kê một mã cho mỗi trường trong bản ghi. Các mã mô tả cả kích thước lẫn cách diễn giải của từng trường.
int của Python không có kích thước cố định -- nó tự mở rộng để chứa bất kỳ giá trị nào bạn gán vào. Các định dạng nhị phân có kích thước cố định: mỗi trường số nguyên dùng một số byte đã được thống nhất trước. struct chuyển đổi giữa các số nguyên Python không giới hạn và các biểu diễn có kích thước cố định này.
Độ rộng của một số nguyên là số bit nó sử dụng. Một byte là tám bit. Mã chữ thường là biến thể có dấu; mã chữ hoa là biến thể không dấu (chỉ các giá trị không âm):
b/B-- 8-bit (một byte).-128..127có dấu,0..255không dấu.h/H-- 16-bit (hai byte).-32768..32767có dấu,0..65535không dấu.i/I-- 32-bit (bốn byte). Khoảng ±hai tỷ có dấu, bốn tỷ không dấu.q/Q-- 64-bit (tám byte). Về thực tế là không giới hạn cho các ứng dụng hàng ngày.
Chọn độ rộng đủ để bao phủ thoải mái phạm vi bạn mong đợi. Đóng gói một giá trị ngoài phạm vi đã khai báo sẽ im lặng quay vòng hoặc ném ra struct.error, tùy thuộc vào bản build.
Các mã phổ biến còn lại dành cho số thực dấu phẩy động và chuỗi byte:
f-- số thực dấu phẩy động 32-bit (độ chính xác đơn; khoảng bảy chữ số thập phân).floatthông thường của Python trên MicroPython đã có kích thước này, vì vậy đóng gói vàoflà không mất dữ liệu.d-- số thực dấu phẩy động 64-bit (độ chính xác kép; khoảng mười lăm chữ số thập phân). Đóng gói mộtfloatMicroPython 32-bit vàodsẽ mở rộng thành tám byte nhưng không thêm độ chính xác.s-- chuỗi byte có độ dài cố định, đứng trước bởi một số đếm (8scho trường tám byte).
2.29.2. Thứ tự byte¶
Một số nguyên nhiều byte có thể được lưu trong bộ nhớ theo hai cách. Số 0x12345678 trong trường 32-bit được bố trí như sau:
Little-endian -- byte kém quan trọng nhất đứng trước:
78 56 34 12.Big-endian -- byte quan trọng nhất đứng trước:
12 34 56 78.
Cả hai đều mã hóa cùng một giá trị; chúng chỉ khác nhau ở chỗ đầu nào của trường là byte thấp. Một tệp được viết bởi hệ thống này sẽ bị hỏng khi đọc bởi hệ thống kia nếu thứ tự byte không khớp.
Ký tự đứng đầu của chuỗi định dạng chọn thứ tự:
<-- little-endian. Phổ biến trên x86 và ARM.>-- big-endian. Phổ biến trong các giao thức mạng.!-- thứ tự mạng, tương đương với>.
Khi không có ký tự đứng đầu, thứ tự byte gốc và căn chỉnh gốc được sử dụng; đặt < hoặc > một cách rõ ràng sẽ loại bỏ sự mơ hồ đó và thường là điều bạn muốn khi đọc tệp hoặc giao tiếp với máy khác.
Ghi chú
OpenMV Cam là little-endian -- giống như PC host của nó. Dùng < trong chuỗi định dạng cho các tệp cục bộ của camera và cho dữ liệu nhị phân truyền đến hoặc từ máy tính để bàn. Dùng > (hoặc !) cho các giao thức mạng và cho bất kỳ định dạng nào mà đặc tả yêu cầu big-endian.
"<HI" đóng gói một giá trị 16-bit theo sau là một giá trị 32-bit thành sáu byte little-endian.¶
2.29.3. Đóng gói¶
import struct
blob = struct.pack("<HI", 320, 1000000)
print(blob, len(blob))
Kết quả:
b'@\x01@B\x0f\x00' 6
Định dạng <HI tạo ra sáu byte: hai cho trường H và bốn cho trường I, tất cả theo little-endian. Truyền đúng số lượng giá trị mà định dạng yêu cầu -- sự không khớp sẽ ném ra struct.error.
2.29.4. Giải nén¶
width, count = struct.unpack("<HI", blob)
print(width, count)
Kết quả:
320 1000000
struct.unpack() luôn trả về một tuple, ngay cả khi định dạng mô tả chỉ một trường. Giải nén nó ngay trên cùng một dòng để dễ đọc.
2.29.5. Chuỗi byte có độ dài cố định¶
Mã s đọc hoặc ghi một khối byte nguyên vẹn. Số đếm đứng trước s -- 4s nghĩa là "bốn byte được xử lý như một chuỗi byte đơn". Đây là cách thông thường để nhúng một giá trị magic, một thẻ có kích thước cố định, hoặc một trường tên có đệm trong bản ghi:
header = struct.pack("<4sHI", b"OMV0", 320, 1000000)
print(header)
Kết quả:
b'OMV0@\x01@B\x0f\x00'
Bốn byte đầu tiên là magic b"OMV0" theo nghĩa đen; hai byte tiếp theo là trường H (320); bốn byte cuối là trường I (1000000). Giải nén trả lại các byte dưới dạng đối tượng bytes:
magic, width, count = struct.unpack("<4sHI", header)
print(magic, width, count)
Kết quả:
b'OMV0' 320 1000000
Nếu giá trị nguồn ngắn hơn số đếm đã khai báo, kết quả sẽ được đệm ở bên phải bằng \x00; nếu dài hơn, các byte thừa sẽ bị bỏ đi một cách im lặng:
struct.pack("4s", b"hi") # b'hi\x00\x00'
struct.pack("4s", b"toolong") # b'tool'
Số đếm là độ dài tính bằng byte, không phải số ký tự -- s làm việc với các byte thô, vì vậy một chuỗi UTF-8 có ký tự nhiều byte cần được .encode() và đếm bằng byte trước.
2.29.6. Xác định kích thước và đọc từng phần¶
struct.calcsize() trả về số byte mà một chuỗi định dạng tiêu thụ:
struct.calcsize("<HI") # 6
Khi đọc một luồng bản ghi từ tệp, hãy đọc đúng số byte đó cho mỗi bản ghi:
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)
Một lần đọc ngắn ở cuối tệp tạo ra một đoạn nhỏ hơn record_size -- hãy coi đó là điều kiện kết thúc luồng thay vì cố gắng giải nén một bản ghi không đầy đủ.