今さらPython3 (47) - バイナリデータ

第7章継続中。このブログ史上最も長く続くシリーズになるかも。

入門 Python 3

入門 Python 3

分かるようで分からないバイナリデータの話。

バイナリデータ

習うより慣れろ。見て感じろって?

>>> blist = [1, 2, 3, 255]
>>> the_bytes = bytes(blist)
>>> the_bytes
b'\x01\x02\x03\xff'
>>> the_byte_array = bytearray(blist)
>>> the_byte_array
bytearray(b'\x01\x02\x03\xff')
>>> 
  • bytesはバイトのタプルのようなもの(イミュータブル)
  • bytearrayはバイトのリストのようなもの(ミュータブル)

残念ながら、頭にスーッと入っていく感覚が全くない。

>>> b'\x61'
b'a'
>>> b'\x01abc\xff'
b'\x01abc\xff'
>>> b'\x01\x61bc\xff'
b'\x01abc\xff'
>>> 

byte値は、b始まりの''で囲った形になる。アルファベットのようなascii文字はそのまま表示される。2つ目と3つ目にあるabcがそうだね。

>>> the_bytes[1] = 127
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment
>>> the_byte_array = bytearray(blist)
>>> the_byte_array
bytearray(b'\x01\x02\x03\xff')
>>> the_byte_array[1] = 127
>>> the_byte_array
bytearray(b'\x01\x7f\x03\xff')
>>> 

bytesはイミュータブルだから変更できないけど、bytearrayの場合は書き換えも可能ですと。

>>> the_bytes = bytes(range(0,256))
>>> the_byte_array = bytearray(range(0,256))
>>> the_bytes
b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'
>>>
>>> the_byte_array
bytearray(b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff')
>>>  

印字可能街とはASCIIで表すので、時々見たことのある文字が並ぶわけですね。

ここまで読んでも、何が嬉しいのかイメージできない。いわゆるバイナリサーチみたいな感じで検索が早くなるとか?

structによるバイナリデータの変換

このファイルの情報を取り出すんだね。

f:id:deutschina:20151222072353p:plain

>>> import struct
>>> valid_png_header = b'\x89PNG\r\n\x1a\n'
>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \
...    b'x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> if data[:8] == valid_png_header:
...     width, height = struct.unpack('>LL', data[16:24])
...     print('Valid PNG, width', width, 'height', height)
... else:
...     print('Not a valid PNG')
... 
Valid PNG, width 2016423936 height 10092544

幅と高さが随分大きく出てきているけど、どこか間違えたかな?

>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \
...    \x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> if data[:8] == valid_png_header:
...     width, height = struct.unpack('>LL', data[16:24])
...     print('Valid PNG, width', width, 'height', height)
... else:
...     print('Not a valid PNG')
... 
Valid PNG, width 154 height 141

dataの2行目の先頭にあるはずの\が抜けてました。これで1バイトずつズレたんだね。普通は、このあたりの情報はファイルから読み込むので間違えることはないんだろうけどね。

やっぱり肝はstruct.unpack('>LL', data[16:24])のあたり。幅と高さの情報はこのあたりに入っていて、

>>> data[16:20]
b'\x00\x00\x00\x9a'
>>> data[20:24]
b'\x00\x00\x00\x8d'
>>> 

それぞれ4バイトのビッグエンディアン形式(>)の4バイト整数(LL)入っている事が前提として分かっているんですね。

エンディアン - Wikipedia

>>> 0x9a
154
>>> 0x8d
141
>>> 0x0
0
>>> 

4バイトで入っているとは言え、最初の3バイトは両方ともゼロ(x00)なので、実質的には最後の1バイトのところに入っている数字がサイズを表しているというところでしょうか。

>>> import struct
>>> struct.pack('>L', 154)
b'\x00\x00\x00\x9a'
>>> struct.pack('>L', 141)
b'\x00\x00\x00\x8d'

struct.pack()を使えば、Pythonデータをバイトに変換できる。ここら辺の書式指定については、このドキュメントを参照すればよし。

7.1. struct — バイト列をパックされたバイナリデータとして解釈する — Python 3.4.3 ドキュメント

リンクと本の説明を見ながら理解すると、

>>> struct.unpack('>2L', data[16:24])
(154, 141)

さっきは、'>LL'という指定だったけど、2Lという表記でも同じなんだね。data[16:24]の中から、ビッグエンディアンの符号なし調整数(4バイト)を2つ取り出してねという意味になると理解。

>>> struct.unpack('>x16x2L6x', data)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
struct.error: unpack requires a bytes object of length 31
>>> data
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> 

こっちはエラーになってしまった。意味するところとしては、最初の16byteを飛ばし(x16)、4バイトの2つの長整数を拾い(2L)、最後の6byteを飛ばすという意味らしいけど、dataの内容が間違ってるのかな?dataの中身を見ると全長30バイトしかないように見える。

>>> struct.unpack('>x16x2L5x', data)
(39424, 36104)

エラーは出なくなったけど、数字が変。つまり前半のどこかが間違っているのかな?

>>> struct.unpack('>x15x2L6x', data)
(154, 141)

最初に飛ばすのを15byteに変更して、最後に飛ばすのを6byteに戻したところ、期待していた数字が出てきました。でも、これでいいのか?

>>> struct.unpack('>16x2L6x', data)
(154, 141)

と思ったら、ようやく分かりました。'>x16x'と書き間違えていました。x単体だと1バイト読み飛ばすので、計17バイト飛ばしていたんですね。xを消してやり直したら正しい数字が出ました。

(まだ続くけど長いので折りたたんでおきます)

construct / binascii

続いての例では、constructが必要になるというのでインストールしてみる。

$ pip3 install construct
Collecting construct
  Downloading construct-2.5.2-py2.py3-none-any.whl (72kB)
    100% |████████████████████████████████| 73kB 4.8MB/s 
Requirement already satisfied (use --upgrade to upgrade): six in /Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages (from construct)
Installing collected packages: construct
Successfully installed construct-2.5.2
$ 

pip3になっているのは、今使っているMac OSX Yosemiteの環境にPython2.7がすでに入っており、pipで起動するとPython 2.7へのインストールになってしまうため、Python3.4.3にインストールする用のpipにはpip3という名称が与えられているから。(もう1つのWindows7環境にはPython3.4.3のみが入ってるため、pipで起動してインストールできる)

やることは同じですね。

>>> from construct import Struct, Magic, UBInt32, Const, String
>>> # Revised the code on http://github.com/construct
... 
>>> fmt = Struct('png',
...     Magic(b'\x89PNG\r\n\x1a\n'),
...     UBInt32('length'),
...     Const(String('type', 4), b'IHDR'),
...     UBInt32('width'),
...     UBInt32('height')
...     )
>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \
...      b'\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> result = fmt.parse(data)
>>> print(result)
Container:
    length = 13
    type = b'IHDR'
    width = 154
    height = 141
>>> print(result.width, result.height)
154 141
>>> 

長さ13の意味するところはよく分からないけど、これの方が直感的に分かりやすいかもしれないですね。

今度は、binasciiですね。

>>> import binascii
>>> valid_png_header = b'\x89PNG\r\n\x1a\n'
>>> print(binascii.hexlify(valid_png_header))
b'89504e470d0a1a0a'
>>> print(binascii.unhexlify(b'89504e470d0a1a0a'))
b'\x89PNG\r\n\x1a\n'

\xxxとASCIIが混在している形式から、16進数に変換している。さらにその逆方向もできてますと。

ビット演算子のまとめは、ドキュメントで言うとこのあたりに書いてある。

https://docs.python.org/3.4/reference/expressions.html#shifting-operations

(つづく)