[解決!Python]バイナリファイルを読み書きするには:structモジュール編解決!Python

structモジュールを使って、一定の構造を持ったデータをバイナリファイルに対して読み書きする方法を紹介する。

» 2021年05月25日 05時00分 公開
[かわさきしんじDeep Insider編集部]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「解決!Python」のインデックス

連載目次

# structモジュールを使ってバイナリファイルに書き込み
from struct import pack, unpack, calcsize, iter_unpack

person = ('かわさき', 120, 99.9)
fmt = '20sid'  # 長さ20のバイト列(20s)、整数(i)、倍精度浮動小数点(d)
b = pack(fmt, person[0].encode(), person[1], person[2])  # fmtに従ってバイト列化

with open('data.bin', 'wb') as f:
    f.write(b)

# データサイズの計算
data_size = calcsize(fmt)
print(data_size)  # 32(このデータは32バイト長)

# バイナリファイルから読み込んで、structモジュールを使って復元
with open('data.bin', 'rb') as f:
    b = f.read(data_size)

data = unpack(fmt, b)
data = (data[0].strip(b'\x00').decode(), data[1], data[2])
print(data)  # ('かわさき', 120, 99.9)

# pathlibモジュールを使う
from pathlib import Path
p = Path('data2.bin')
b = pack(fmt, person[0].encode(), person[1], person[2])
p.write_bytes(b)

b = p.read_bytes()
data = unpack(fmt, b)
data = (data[0].strip(b'\x00').decode(), data[1], data[2])
print(data)  # ('かわさき', 120, 99.9)

# 複数のデータをバイナリファイルに書き込み
p_list = [('かわさき', 120, 99.9),
          ('えんどう', 60, 68.3),
          ('いっしき', 25, 65.2)]

fmt = '20sid'
with open('data.bin', 'wb') as f:
    b = [pack(fmt, p[0].encode(), p[1], p[2]) for p in p_list]
    f.writelines(b)

# 複数のデータをバイナリファイルから読み込み
data_size = calcsize(fmt)

with open('data.bin', 'rb') as f:
    b = f.read()

result = [(d[0].strip(b'\x00').decode(), d[1], d[2]) for d in iter_unpack(fmt, b)]

print(result)


structモジュールを使ったバイナリファイルの読み書き

 「[解決!Python]バイナリファイルを読み書きするには:文字列と整数編」では、バイナリファイルに対して文字列もしくは整数を読み書きする方法を紹介した。しかし、これらが混合したデータをバイナリファイルに対して読み書きしたり、浮動小数点数をバイナリファイルに対して読み書きしたり、一定の構造を持った(Cの構造体のような)データをバイナリファイルに対して読み書きしたりするには、Pythonが標準で提供するstructモジュールを使うのが簡単だ。

 structモジュールには、バイナリファイルに対して読み書きするデータの構造(フォーマット)に従って、データをバイト列に変換するpack関数、バイト列からデータを復元するunpack関数、指定したフォーマットが何バイトのデータかを計算するcalcsize関数、指定したフォーマットに従って復元したバイト列を反復するiter_unpack関数などが含まれている。

 フォーマットの指定に使える書式指定文字を幾つか以下に抜粋する。詳細についてはPythonのドキュメント「書式指定文字」を参照のこと。

書式指定文字 Pythonのデータ型 Cのデータ型 サイズ
x パディング
c 長さ1のバイト列 char 1
b 整数 signed char 1
B 整数 unsigned char 1
i 整数 int 4
I 整数 unsigned int 4
f 浮動小数点数 float 4
d 浮動小数点数 double 8
s、p 文字列をバイト列に変換したもの char[]
structモジュールで使われる書式指定文字(抜粋)

 これらの書式指定文字の前に、バイトオーダー(リトルエンディアンやビッグエンディアンなど)やアラインメントを指示する記号を付加できるが、これについては「バイトオーダ、サイズ、アラインメント」を参照のこと(省略時は「@」つまりそのコードを実行するマシンにネイティブなバイトオーダーやアラインメントが指定されたものと見なされる)。

 Pythonでは整数や浮動小数点数にはサイズという概念がないが、これらをバイナリファイルに対して読み書きするときには、それが何バイトのデータとなるかを指定する必要がある。例えば、'i'という書式指定文字を指定すると、対応する値は4バイトの符号付き整数として取り扱われるということだ。なお、書式指定文字の前にはその型のデータが連続する数を指定できる。例えば、「4i」は4バイトの符号付き整数が4つ連続することを意味する(「iiii」と同じ)。

 バイナリファイルに対して文字列を読み書きするときには、文字列のencodeメソッドやバイト列のdecodeメソッドで文字列とバイト列の変換を行う必要がある。バイト列化した文字列を表現する書式指定文字は「s」となる。「s」は1バイトのchar型配列を、「3s」は3バイトのchar型配列を表すが、これではPythonの文字列を特定のエンコーディングで変換したものを格納しきれない場合がある。

 例えば、UTF-8エンコードの日本語では多くの場合、1文字が3バイトで表現される(一部、4バイトで表現されるものもある)。

s = 'あ'
b = s.encode()
print(f'sizeof b: {len(b)}, b: {b}'# sizeof b: 3, b: b'\xe3\x81\x82'


 そのため、'あ'という1文字をUTF-8エンコードでバイト列に変換したものをバイナリファイルに書き込むには、3バイトの領域が必要になる(終端文字のNULLを含めるのであれば、4バイトが必要になるだろう)。

バイナリファイルへの書き込み

 ここでは例として、次のようなデータ構造(名前、年齢、体重を要素とするタプル)を考える。

person = ('かわさき', 120, 99.9)


 これらをここでは「20バイトのバイト列」「4バイトの符号付き整数」「倍精度の浮動小数点数」としてバイト列に変換したいとしよう。すると、上に示した書式指定文字を使って、このデータ構造は次のように書ける。

fmt = '20sid'  # 長さ20のバイト列(20s)、整数(i)、倍精度浮動小数点(d)


 書式指定文字「20s」は20バイトのバイト列を表す(ここには6文字程度の日本語を格納できるだろう。余った部分にはb'\x00'が埋め込まれる)。次の「i」は120という整数値を4バイトの符号付き整数として、最後の「d」は99.9という浮動小数点数値を倍精度の浮動小数点数値として取り扱うことを意味する。

 これを使って、タプルに格納されたデータをバイト列に変換するにはstructモジュールのpack関数を使用する。

pack(fmt, v1, v2, ……)


 第1引数には上で見た書式指定文字を渡す。それに続けて、バイト列化したいもの(v1、v2、……)を列挙すればよい。ここではタプルpersonに格納されているデータを渡せばよいが、文字列については事前にencodeメソッドでバイト列に変換しておく必要がある点には注意しよう。

 上のデータをバイト列に変換するコードを以下に示す(バイトオーダーは実行環境にネイティブ)。

from struct import pack, unpack, calcsize, iter_unpack

person = ('かわさき', 120, 99.9)
fmt = '20sid'
b = pack(fmt, person[0].encode(), person[1], person[2])
# b = pack(fmt, person[0].encode(), *person[1:])
print(b)  # b'\xe3\x81\x8b\xe3\x82\x8f……\x00\x9a\x99\x99\x99\x99\xf9X@'


 このコードではタプルの先頭要素が文字列なのでencodeメソッドを呼び出しているが、タプルなどに文字列を含まないデータだけが格納されているのであれば、「b = pack(fmt, *data)」のように書ける。

 あとはこれをバイナリファイルに書き込むだけだ。これらをまとめると次のようになる。

from struct import pack, unpack, calcsize, iter_unpack

person = ('かわさき', 120, 99.9)
fmt = '20sid'
b = pack(fmt, person[0].encode(), person[1], person[2])

with open('data.bin', 'wb') as f:
    f.write(b)


 このデータがどのくらいのサイズになるかは、calcsize関数に書式指定文字を渡すことで計算できる。

data_size = calcsize(fmt)
print(data_size)  # 32


バイナリファイルからの読み込み

 バイナリファイルに書き込んだデータを読み込むコードは次のようになる。

with open('data.bin', 'rb') as f:
    b = f.read(data_size)


 先ほどの書き込みコードではデータを1つだけ書き込んでいるので、2行目では「b = f.read()」としても構わないが、ここではcalcsize関数で計算したデータサイズだけのデータを読み込むようにしている。

 バイナリファイルから読み込んだデータ(バイト列)を復元するには、structモジュールのunpack関数を使用する。

unpack(fmt, buffer)


 第1引数にはバイト列がどのような構造を持つかを示す書式指定文字を指定する。第2引数には読み込んだバイト列を指定する。ここでは、上で書き込んだデータを復元するので、第1引数にはこれまでと同じ'20sid'を、第2引数にはバイナリファイルから読み込んだデータを指定すればよい。外部で作成されたデータを読み込むときには、バイナリデータがどのような構造になっているかを調べて、それに応じた書式指定文字を指定する必要がある。

 unpack関数は書式指定文字に従って、バイト列を復元し、復元した値を含んだタプルを返す。実際に復元したデータに文字列が含まれていれば、それはやはりdecodeメソッドで文字列化する必要がある。以下に実際のコードを示す。

data = unpack(fmt, b)
data = (data[0].strip(b'\x00').decode(), data[1], data[2])
print(data)  # ('かわさき', 120, 99.9)


 ここではバイト列をデコードする前に「strip(b'\x00')」メソッドを呼び出しているが、これはバイト列から不要な部分を削除するためだ。

pathlibモジュールのPathクラスを使用する

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。