Pythonでバイナリを扱う時のTipsです。
Pythonでバイナリを扱う方法は2つあります、struct
モジュールを使う方法とctypes.Structure
クラスを使う方法です。
基本的にstruct
モジュールは数バイトのバイナリを扱いたい時、それ以上のバイト数やC/C++と連携したい時にctypes.Structure
クラスを使います。
struct
モジュール
例としてPNGファイルのバイナリを読んでみます、PNGファイルは頭の8バイトはヘッダで決め打ちです。
9バイト目から18バイトのデータはIHDR領域(正確にはIHDRの一部)でイメージの縦横サイズとビット深度、カラーモードが格納されています。
import struct
png_data = open("sample.png", "rb").read()
struct.unpack_from(">I4sIIBB", png_data, 8)
# (13, b'IHDR', 250, 156, 8, 2)
データの読み込みはstruct.unpack
でも良いのですが、与えるバッファのオフセットとサイズがピッタリあっていないとエラーになります。
データの一部を読みたい場合はstruct.unpack_from
が便利です。
パティングはx
で
バイナリを読んでいるとどうしてもパティング(アライメント合わせのためのゴミ領域)が出てきます。
x
フォーマットを使用すると、データを読み飛ばしてくれるので便利です。
data = b'd\x00\xb0\x04'
# NG
kind, _, value = struct.unpack("BBH", data)
# Yes!
kind, value = struct.unpack("BxH", data)
struct.Structクラス
struct.Struct
クラスは、struct
モジュールのフォーマット文字列をクラス化したものです。
クラスのインスタンス生成時にフォーマットの解析を行うので、ループ内で繰り返しpack
/unpack
する場合は事前にインスタンスを生成しておくと高速です。
ctypes.Structre
クラスと紛らわしいですね。
point = struct.Struct("HH")
for x, y in zip(range(10), range(10)):
point.pack(x, y)
フォーマット文字一覧
文字 | C言語型 | 標準サイズ |
---|---|---|
x | パティングバイト | 1 |
c | char | 1 |
b | signed char | 1 |
B | unsigned char, BYTE | 1 |
? | _Bool | 1 |
h | short | 2 |
H | unsinged short, WORD | 2 |
i | int | 4 |
I | unsigned int, DWORD | 4 |
l | long, LONG | 4 |
L | unsigned long, ULONG | 4 |
q | long long, LONGLONG | 8 |
Q | unsigned long long, ULONGLONG | 8 |
n | ssize_t(Python3.3以降) | Nativeのみ |
N | size_t(Python3.3以降) | Nativeのみ |
f | float | 4 |
d | double | 8 |
s | char[] | - |
p | char[] | - |
P | void * | - |
フォーマット文字例:
typedef struct tagBITMAPINFOHEADER {
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount;
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER;
"IllHHIIllII"
バイトオーダー、アライメントの一覧表
文字 | バイトオーダー | サイズ | アライメント |
---|---|---|---|
@ | Native | Native | Native |
= | Native | 標準のサイズ | なし |
< | リトルエンディアン | 標準のサイズ | なし |
> | ビッグエンディアン | 標準のサイズ | なし |
! | ビッグエンディアン | 標準のサイズ | なし |
※省略された場合は@
となる
struct.calcsize("BI")
# 8
struct.calcsize("=BI")
# 5
エンディアンを明示的に指定してしまうとアライメントが「なし」になるので注意。
ctypes.Structureクラス
ctypes.Structure
クラスでC/C++の構造体を扱う事ができます。
'struct'モジュールで沢山のデータを読もうとすると、フォーマットが呪文のようになりますので、大量のバイナリデータの読み込みをしっかりと書きたい場合はctypes.Structure
クラスを使った方が良いでしょう。
Structureの基本
ctypes.Structure
を継承し、_field_
に型を定義します。
from ctypes import *
"""
typedef struct {
char identity[4];
uint16_t x;
uint16_t y;
} TestStructure;
"""
class TestStructure(Structure):
_fields_ = (
('identity', c_char * 4),
('x', c_uint16),
('y', c_uint16),
)
インスタンスは以下のように定義します。
t = TestStructure(b"TEST", 100, 100)
サイズ固定の型指定を使いましょう
C言語でint
やshort
はサイズが環境によって変化します、C99からint16_t
やint32_t
などのサイズ固定の型指定が可能になったので、可能な限りサイズ固定の型指定を使用すべきです。それに伴いPython側でもctypes.c_int
ではなくctypes.c_int16
などのサイズ固定の型を使いましょう。
書き込み/読み込み
io
またはFILE
のwrite
にctypes.Structure
インスタンスをそのまま渡せば書き込みができます。
import io
buffer = io.BytesIO()
buffer.write(TestStructure(b"TEST", 100, 100))
buffer.getvalue()
# b'TESTd\x00d\x00'
readinto
にctypes.Structure
インスタンスをそのまま渡せば読み込みができます。
buffer = io.BytesIO(b'TESTd\x00d\x00')
t = TestStructure()
buffer.readinto(t)
t.identity, t.x, t.y
# (b'TEST', 100, 100)
メンバのオフセットの取得
構造体メンバのオフセット位置は、クラス名.メンバ名.offset
というクラスメソッドで取得できます。
class Point(Structure):
_fields_ = (
('x', c_uint16),
('y', c_uint16),
)
Point.y.offset
# 2
sizeof
ctypes.sizeof
で構造体のサイズを取得できます。
class TestStructure(Structure):
_fields_ = (
('flags', c_ubyte),
('value', c_int32),
)
sizeof(TestStructure)
# 8
memset / memmove
C言語のmemset
とmemmove
と同等のものがctypes.memset
とctypes.memmove
です。
c_array = (c_char * 12)()
memset(c_array, 0, sizeof(c_array))
memmove(c_array, b"test\x00", len(b"test\x00"))
ポインタでデータマッピング
C/C++のように構造体のポインタをキャストすることにより、データをマッピングできます。
構造体のポインタを指定したい場合はctypes.POINTER
、ctypes.cast
でキャストしてあげます、ポインタが参照している値はcontents
で取得できます。
class PointText(Structure):
_fields_ = (
('x', c_uint16),
('y', c_uint16),
('text', c_char * 0),
)
data = b'd\x00d\x00null terminate text\x00'
p_point = cast(data, POINTER(Point))
p_point.contents.x, p_point.contents.y
# (200, 120)
# NULL終端の文字列読み込み
string_at(addressof(p_point.contents) + PointText.text.offset)
# b'null terminate text'
ctypes.stering_at
でNULL終端の文字列を読み込み、Unicodeの場合はctypes.wstring_at
を使います。
しかし、ポインタ操作はPython自体をクラッシュさせる可能性があるので注意してください、可能であればchar []
などの長さ未指定のメンバは避けるべきです。
memoryviewでbytesに変換
memoryview
でctypes
オブジェクトをPyObject
に変換できます。
p = Point(200, 120)
memoryview(p).tobytes()
# b'\xc8\x00x\x00'
リトルエンディアン、ビッグエンディアン
class BPoint(BigEndianStructure):
_fields_ = (
('x', c_uint16),
('y', c_uint16),
)
class LPoint(LittleEndianStructure):
_fields_ = (
('x', c_uint16),
('y', c_uint16),
)
bpoint = BPoint(0x0102, 0x0304)
lpoint = LPoint(0x0102, 0x0304)
memoryview(bpoint).tobytes()
# b'\x01\x02\x03\x04'
memoryview(lpoint).tobytes()
# b'\x02\x01\x04\x03'
参考
http://docs.python.jp/3.5/library/struct.html
http://docs.python.jp/3.5/library/ctypes.html