【Python】ランダムアクセスによる固定長レコードの読み書き・前編【入門第74回】
目次
ランダムアクセスと固定長レコード
今回から固定長レコードのランダムアクセスについてやりたいと思います。
固定長レコードとはデータの単位で、固定長のバイト数で区切られたデータのことです。
この固定長レコードとランダムアクセスは相性がよく、組み合わせるととても効率的なプログラムを書くことが出来ます。
この技術を使えば簡易的なデータベースを作ることも可能です。
今回は具体的には↓を見ていきます。
レコードとは?
固定長レコードの定義
固定長レコードを書き込む
固定長レコードを読み込む
ほかのファイル入出力についての入門記事は↓をご覧ください。
- 【Python】ファイルを開いて閉じる。open関数の使い方【入門第54回】
- 【Python】テキストファイルの読み込み(モードr, rt)【入門第55回】
- 【Python】テキストファイルへの書き込み(モードw, wt)【入門第56回】
- 【Python】テキストファイルへの追記(モードa, at)【入門第57回】
- 【Python】テキストファイルを排他的に開いて書き込む(モードx, xt)【入門第58回】
- 【Python】バイナリファイルの読み込み(モードrb)【入門第59回】
- 【Python】バイナリファイルへの書き込み(モードwb)【入門第60回】
- 【Python】バイナリファイルへの追記(モードab)【入門第61回】
- 【Python】バイナリファイルを排他的に開いて書き込む(モードxb)【入門第62回】
- 【Python】テキストファイルを読み書きモードで開く(モードr+, r+t)【入門第63回】
- 【Python】テキストファイルを読み書きモードで新規作成して開く(モードw+, w+t)【入門第64回】
- 【Python】テキストファイルを読み書きモードで、新規作成して、排他的に開く(モードx+, x+t)【入門第65回】
- 【Python】バイナリファイルを読み書きモードで開く(モードr+b)【入門第66回】
- 【Python】バイナリファイルを読み書きモードで新規作成して開く(モードw+b)【入門第67回】
- 【Python】バイナリファイルを読み書きモードで排他的に開く(モードx+b)【入門第68回】
- 【Python】テキストファイルを読み書きモードで追記する(モードa+)【入門第69回】
- 【Python】バイナリファイルを読み書きモードで追記する(モードa+b)【入門第70回】
- 【Python】バイナリファイルのランダムアクセスの基本・前編【入門第71回】
- 【Python】バイナリファイルのランダムアクセスの基本・後編【入門第72回】
- 【Python】ランダムアクセスによる固定長レコードの読み書き・前編【入門第73回】
レコードとは?
まずレコードとは何なのかと言うと、フィールドが集まったものです。
フィールドとは名前や身長や年齢などのデータです。
たとえば人の個人情報を扱うプログラムを書きたいとします。
そしてその個人情報をバイナリファイルにレコードの単位で保存して読み書きするとします。
たとえば↓のようなイメージです。
:::text
太朗 170 16
↑の太郎の行がレコード1
件分にあたります。
「太郎」という名前や「170
」という身長、「16
」という年齢がフィールドです。
バイナリファイルにはこのレコードがずら~っと並んでいます。
:::text
太朗 170 16
花子 160 12
次郎 180 30
三郎 190 51
↑の例ではわかりやすくレコードを改行で区切っていますが、実際には改行もありません。
固定長レコードとは?
固定長レコードとは、さきほどのレコードで、バイト数がある一定値で決められているレコードのことです。
このバイト数はフィールドごとにきまります。
たとえば名前は32
バイト、身長は8
バイト、年齢は4
バイトとします。
このバイト数はそれぞれのフィールドが扱えるデータの容量を表しています。
たとえば名前なら32
バイト以下は保存できるけど、それより上は保存できない! と言った感じです。
この場合、このレコードのバイト数は 32 + 8 + 4
で44
バイトになります。
ファイルにレコードを書き込むときは必ずこの44
バイトのバイト数でレコードを書き込みます。
読み込むときもかならず44
バイト読み込みます。
こうすることでファイルに保存されているレコードはすべて決まったバイト数になります。
そうすると、ランダムアクセスで都合がよくなるのです。
レコードのバイト数が決まっている、つまり固定長だと、ランダムアクセスによるレコードへのアクセスがとても速くなります。
なぜかというとseek
メソッドの第1引数にはバイト数を指定しましたが、このバイト数を求めるときにレコードが固定長だと↓のようにしてアクセスしたいレコードまでのバイト数がわかるからです。
:::text
アクセスしたいバイト数 = レコードのバイト数 * 先頭からの件数
たとえばレコードのバイト数が44
バイトとします。
そしてファイルには10
件のレコードが並んでいます。
このときに先頭から3
件目のレコードにアクセスしたいとします。
その場合は44 * 3
で先頭からのバイト数132
が求まります。
あとはこの132
バイトをseek
メソッドに指定すれば、3
件目のレコードにアクセスできるというわけです。
これがランダムアクセスが速いと言われる理由の一つです。
掛け算をしてseek
を呼び出せば、それだけで好きなレコードにアクセスすることが出来ます。
シーケンシャルアクセスの場合は、ファイルの先頭から順々にレコードを読んでいかないといけません。
それに比べてこの固定長レコードのランダムアクセスは一瞬で済みます。
固定長レコードの定義
Pythonでバイナリファイルの固定長レコードを扱うには、struct
というモジュールを使います。
このstruct
モジュールの使い方は順を追って説明しますが、struct
モジュールではデータのバイト数をフォーマットで表現しています。
たとえば1
バイトならフォーマットはc
, 4
バイトならフォーマットはi
です。
今回の固定長のレコードの定義ではこのフォーマットを使います。
struct --- バイト列をパックされたバイナリデータとして解釈する — 書式指定文字 — Python 3.7.5rc1 ドキュメント
今回は人の個人情報を扱うプログラムを想定して、レコードを定義します。
レコードのフィールドは↓の通りです。
名前
身長
年齢
まず名前のバイト数を決めます。
名前なので少し多めに取って64
バイトにしましょうか。
この場合、struct
のフォーマットは64s
になります。
この64s
は64
バイトのバイト列と言う意味です。
次に身長は4
バイトでstruct
のフォーマットはi
で、年齢も4
バイトでstruct
のフォーマットはi
にします。
すると↓のようなレコードの定義になります。
名前(64バイト, フォーマット64s)
身長(4バイト, フォーマットi)
年齢(4バイト, フォーマットi)
レコード1件分のバイト数は合計で72バイト
固定長レコードを書き込む
先ほど定義したレコードを使ってバイナリファイルにデータを書き込んでみます。
:::python
import struct
with open('persons.dat', mode='wb') as fout:
data = struct.pack('64sii', '太朗'.encode('utf-8'), 170, 16)
fout.write(data)
struct.pack
は第1引数のフォーマットに従って、第2引数以降のデータをバイト列にするstruct
モジュールの関数です。
このstruct.pack
で個人情報である名前、身長、年齢をバイト列にしています。
struct.pack
に渡せるのはバイト列かそれに相当するデータだけです。なので太朗
という文字列をencode
でutf-8
のバイト列にしています。
struct.pack
の第1引数のフォーマットには先ほど定義したフィールドのフォーマットをつなげて書きます。
:::text
64sii
↑のフォーマットは先頭から名前の64s
, 身長のi
, 年齢のi
になっています。
ちなみにどんなバイト列が返ってくるか↓のコードで確認してみましょう。
:::python
import struct
data = struct.pack('64sii', '太朗'.encode('utf-8'), 170, 16)
print(data)
↑のコードの実行結果は↓のようになります。
:::text
b'\xe5\xa4\xaa\xe6\x9c\x97\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\x00\x00\x00\x10\x00\x00\x00'
さきほどのwrite
してるコードを実行すると、↑のバイト列がファイルに書き込まれます。
固定長レコードを読み込む
先ほど書き込んだレコードを読み込んでみましょう。
:::python
import struct
# レコードのフォーマット
record_format = '64sii'
# レコードのサイズをフォーマットから計算する
record_size = struct.calcsize(record_format)
with open('persons.dat', mode='rb') as fin:
# レコードのサイズ分データを読み込む
data = fin.read(record_size)
# 読み込んだデータを指定のフォーマットでPythonの型に変換
fields = struct.unpack(record_format, data)
# 読み込んだデータを出力
print('名前:', fields[0].decode('utf-8').rstrip('\x00'))
print('身長:', fields[1])
print('年齢:', fields[2])
少し長いコードになってしまいました。
まずrecord_format
変数ですが、これはレコードのフォーマットの文字列です。
フォーマットは書き込みの時と同じ64sii
です。
:::python
# レコードのフォーマット
record_format = '64sii'
次にstruct.calcsize
でレコード全体のサイズをフォーマットから計算しています。
record_size
変数には計算したレコードのバイト数が入ります。
:::python
# レコードのサイズをフォーマットから計算する
record_size = struct.calcsize(record_format)
その次にバイナリファイルをrb
で開きます。
ファイルオブジェクトfin
のメソッドread
にレコードのバイト数を指定して、ファイルからレコードのサイズ分のデータ、バイト列を読み込みます。
:::python
# レコードのサイズ分データを読み込む
data = fin.read(record_size)
その次に読み込んだバイト列をstruct.unpack
に渡して、タプル(fields
)にします。
struct.unpack
は第1引数のフォーマットに従って第2引数のバイト列をPythonの型のタプルに変換するstruct
モジュールの関数です。
:::python
# 読み込んだデータを指定のフォーマットでPythonの型に変換
fields = struct.unpack(record_format, data)
struct.unpack
は戻り値としてタプルを返すので、fields
にはタプルが入ります。
そしてタプル内のフィールドをprint
で出力します。
:::python
# 読み込んだデータを出力
print('名前:', fields[0].decode('utf-8').rstrip('\x00'))
print('身長:', fields[1])
print('年齢:', fields[2])
fields
には0
番目の要素に名前のバイト列、1
番目の要素に身長、2
番目の要素に年齢が入っています。
名前はバイト列になっているのでdecode
メソッドでutf-8
の文字列に変換する必要があります。
この時、バイト列内の0
の値がゴミとして文字列の右側に入ってしまうので、rstrip
でゴミを削除しています。
↑のコードの実行結果は↓のようになります。
:::text
名前: 太朗
身長: 170
年齢: 16
おわりに
今回は固定長レコードのランダムアクセスの基本を解説しました。
ランダムアクセスしてないけど?
固定長レコードのランダムアクセスは次回やります。
以上、次回に続きます。
また見てね