1. pashango2
Changes in body
Source | HTML | Preview
@@ -1,203 +1,267 @@
Pythonでバイナリを扱う時のTipsです。
Pythonでバイナリを扱う方法は2つあります、`struct`モジュールを使う方法と`ctypes.Structure`クラスを使う方法です。
基本的に`struct`モジュールは数バイトのバイナリを扱いたい時、それ以上のバイト数やC/C++と連携したい時に`ctypes.Structure`クラスを使います。
# `struct`モジュール
例としてPNGファイルのバイナリを読んでみます、PNGファイルは頭の8バイトはヘッダで決め打ちです。
9バイト目から18バイトのデータはIHDR領域(正確にはIHDRの一部)でイメージの縦横サイズとビット深度、カラーモードが格納されています。
```python
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`フォーマットを使用すると、データを読み飛ばしてくれるので便利です。
```python
data = b'd\x00\xb0\x04'
# NG
kind, _, value = struct.unpack("BBH", data)
# Yes!
kind, value = struct.unpack("BxH", data)
```
+## フォーマット文字一覧
-## バイトオーダー、サイズ、アライメントの一覧表
+| 文字 | 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 * | - |
+
+フォーマット文字例:
+
+```C:BITMAPINFOHEADER構造体
+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;
+```
+```python:BITMAPINFOHEADER構造体のフォーマット文字
+"IllHHIIllII"
+```
+
+## バイトオーダー、アライメントの一覧表
| 文字 | バイトオーダー | サイズ | アライメント |
|:-:|:--|:-:|:-:|
| @ | Native | Native | Native |
| = | Native | 標準のサイズ | なし |
| < | リトルエンディアン | 標準のサイズ | なし |
| > | ビッグエンディアン | 標準のサイズ | なし |
| ! | ビッグエンディアン | 標準のサイズ | なし |
-| 標準のサイズ | 該当する型(unsignedは省略) |
-|:-:|:-:|
-| 1 | char / bool |
-| 2 | short |
-| 4 | int / long / float |
-| 8 | long long / double |
+※省略された場合は`@`となる
```python:@と=の違い(CPU=amd64,OS=Ubuntu64bit)
struct.calcsize("BI")
>>> 8
struct.calcsize("=BI")
>>> 5
```
エンディアンを明示的に指定してしまうとアライメントが「なし」になるので注意。
# ctypes.Structureクラス
`ctypes.Structure`クラスでC/C++の構造体を扱う事ができます。
'struct'モジュールで沢山のデータを読もうとすると、フォーマットが呪文のようになりますので、大量のバイナリデータの読み込みをしっかりと書きたい場合は`ctypes.Structure`クラスを使った方が良いでしょう。
## Structureの基本
`ctypes.Structure`を継承し、`_field_`に型を定義します。
```python
from ctypes import *
"""
typedef struct {
char identity[4];
int x;
int y;
} TestStructure;
"""
class TestStructure(Structure):
_fields_ = (
('identity', c_char * 4),
('x', c_uint16),
('y', c_uint16),
)
```
インスタンスは以下のように定義します。
```python
t = TestStructure(b"TEST", 100, 100)
```
### <i class="fa fa-exclamation-triangle" aria-hidden="true"></i>サイズ固定の型指定を使いましょう
C言語で`int`や`short`はサイズが環境によって変化します、C99から`int16_t`や`int32_t`などのサイズ固定の型指定が可能になったので、可能な限りサイズ固定の型指定を使用すべきです。それに伴いPython側でも`ctypes.c_int`ではなく`ctypes.c_int16`などのサイズ固定の型を使いましょう。
## 書き込み/読み込み
`io`または`FILE`の`write`にインスタンスをそのまま渡せば書き込みができます。
```python
import io
buffer = io.BytesIO()
buffer.write(TestStructure(b"TEST", 100, 100))
buffer.getvalue()
>>> b'TESTd\x00d\x00'
```
`readinto`で読み込みができます。
```python
buffer = io.BytesIO(b'TESTd\x00d\x00')
t = TestStructure()
buffer.readinto(t)
t.identity, t.x, t.y
>>> (b'TEST', 100, 100)
```
## メンバのオフセットを取得
```python
class Point(Structure):
_fields_ = (
('x', c_uint16),
('y', c_uint16),
)
Point.y.offset
>>> 2
```
+## sizeof
+`ctypes.sizeof`で構造体のサイズを取得できます。
+
+```python
+class TestStructure(Structure):
+ _fields_ = (
+ ('flags', c_ubyte),
+ ('value', c_int32),
+ )
+
+sizeof(TestStructure)
+>>> 8
+```
+
+## memset / memmove
+C言語の`memset`と`memmove`と同等のものが`ctypes.memset`と`ctypes.memmove`です。
+
+```python
+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`で取得できます。
```python
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`に変換できます。
```python
p = Point(200, 120)
memoryview(p).tobytes()
>>> b'\xc8\x00x\x00'
```
## リトルエンディアン、ビッグエンディアン
```python
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