home changes contents help options

165:固定長レコードを処理する

struct

struct モジュールは Python オブジェクトと バイナリデータの相互変換をサポートするモジュールです。

データ作成

struct モジュールを使用して「4 バイトの整数 2 つと 8 バイトの文字列、計 16 バイト」というデータを作成するには pack 関数を用い、次のようにします。

>>> import struct
>>> struct.pack('=2i8s', 27240, 6514034, 'foo') # バイトオーダー環境依存
'hj\x00\x00rec\x00foo\x00\x00\x00\x00\x00'
>>>
>>> struct.pack('<2i8s', 27240, 6514034, 'foo') # リトルエンディアン
'hj\x00\x00rec\x00foo\x00\x00\x00\x00\x00'
>>>
>>> struct.pack('>2i8s', 27240, 6514034, 'foo') # ビッグエンディアン
'\x00\x00jh\x00cerfoo\x00\x00\x00\x00\x00'

pack 関数の第 1 引数 fmt には、作成するデータの構造を表す「フォーマット文字列」を指定します。この書式は struct モジュールのドキュメントを参照してください。データの並びのほか、バイトオーダー、アラインメントを指定することもでき、フォーマット文字列の最初に「@」、「=」、「<」、「>」、「!」のいずれかを使うことで指定します。

省略するとデフォルトの「@]を記述したものとみなされます。「@」指定時は「型サイズ、バイトオーダー、アラインメントすべて環境依存」の挙動をし、同環境の C プログラムにて作成されたデータを読むのに適しています。

しかし、今回の「固定長レコードを処理する」という目的には「=」「<」「>」らを用いたほうがよいでしょう。「=」指定時は「型サイズが一意に定まり(short は 2 バイト、 int, long は 4 バイトなど)、バイトオーダーは環境依存、アラインメントはなし」となるので挙動が明白なものとなります。「<」「>」はこれに加え、バイトオーダーも確定し、それぞれリトルエンディアン、ビッグエンディアンとなります。

次のコードは複数のレコードを順次作成し、ファイル data.dat に保存します。

filename = u'data.dat'
record_format = '=2i8s'
#record_size = 16
#assert record_size == struct.calcsize(record_format)

py_datas = [
        [1, 10, 'abcdefgh'],
        [2, 20, 'ijklmnop'],
        [3, 30, 'qrstuvwx'],
        [4, 40, 'yzABCDEF'],
        ]

fobj = open(filename, 'wb')
try:
    for py_data in py_datas:
        c_data = struct.pack(record_format, *py_data)
        print repr(c_data)
        fobj.write(c_data)
finally:
    fobj.close()

データ読み込み

struct モジュールを使用して「4 バイトの整数 2 つと 8 バイトの文字列、計 16 バイト」というデータを読み込むには unpack 関数を用い、次のようにします。

>>> import struct
>>> record_native = struct.pack('=2i8s', 27240, 6514034, 'foo')
>>> record_little = struct.pack('<2i8s', 27240, 6514034, 'foo')
>>> record_big = struct.pack('>2i8s', 27240, 6514034, 'foo')
>>>
>>> struct.unpack('=2i8s', record_native) # バイトオーダー環境依存
(27240, 6514034, 'foo\x00\x00\x00\x00\x00')
>>> struct.unpack('<2i8s', record_little) # リトルエンディアン
(27240, 6514034, 'foo\x00\x00\x00\x00\x00')
>>> struct.unpack('>2i8s', record_big) # ビッグエンディアン
(27240, 6514034, 'foo\x00\x00\x00\x00\x00')

unpack 関数の第 1 引数 fmt には、読み出すデータの構造を表す「フォーマット文字列」を指定します。書式は pack 関数のものと同じです。上記の説明および struct モジュールのドキュメントを参照してください。

次のコードはファイル data.dat に保存された複数レコードを順次、読み出します。

filename = u'data.dat'
record_format = '=2i8s'
record_size = 16
assert record_size == struct.calcsize(record_format)

fobj = open(filename, 'rb')
py_datas = []
try:
    while True:
        c_data = fobj.read(record_size)
        if not c_data:
            break
        py_data = struct.unpack(record_format, c_data)
        print repr(py_data)
        py_datas.append(py_data)
finally:
    fobj.close()

任意の位置のデータにアクセスするには ファイルオブジェクト のドキュメント、特に seek メソッドの説明が役に立つはずです。次のコードはファイル data.dat の先頭と末尾のデータを交換します。

n = 2
filename = u'data.dat'
record_format = '=2i8s'
record_size = 16

fobj = open(filename, 'rb+')
try:
    head = fobj.read(record_size)
    fobj.seek(-record_size, 2) # 末尾データの先頭に移動
    tail = fobj.read(record_size)
    fobj.seek(0) # ファイルの先頭に移動
    fobj.write(tail)
    fobj.seek(-record_size, 2) # 末尾データの先頭に移動
    fobj.write(head)
finally:
    fobj.close()

次のコードはファイル data.dat に保存されたレコードから n 番目のデータを取り出します。

n = 2
filename = u'data.dat'
record_format = '=2i8s'
record_size = 16

fobj = open(filename, 'rb')
try:
    fobj.seek(record_size * n)
    c_data = fobj.read(record_size)
    py_data = struct.unpack(record_format, c_data)
    print repr(py_data)
finally:
    fobj.close()