1. pashango2

    Posted

    pashango2
Changes in title
+PythonでバイナリをあつかうためのTips
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,203 @@
+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)
+```
+
+
+## バイトオーダー、サイズ、アライメントの一覧表
+
+| 文字 | バイトオーダー | サイズ | アライメント |
+|:-:|:--|:-:|:-:|
+| @ | 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
+```
+
+## ポインタでデータマッピング
+
+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