【Python】ランダムアクセスによる固定長レコードの読み書き・前編【入門第74回】

212, 2019-10-04

目次

ランダムアクセスと固定長レコード

今回から固定長レコードのランダムアクセスについてやりたいと思います。
固定長レコードとはデータの単位で、固定長のバイト数で区切られたデータのことです。
この固定長レコードとランダムアクセスは相性がよく、組み合わせるととても効率的なプログラムを書くことが出来ます。
この技術を使えば簡易的なデータベースを作ることも可能です。

今回は具体的には↓を見ていきます。

  • レコードとは?

  • 固定長レコードの定義

  • 固定長レコードを書き込む

  • 固定長レコードを読み込む

ほかのファイル入出力についての入門記事は↓をご覧ください。

レコードとは?

まずレコードとは何なのかと言うと、フィールドが集まったものです。
フィールドとは名前や身長や年齢などのデータです。

たとえば人の個人情報を扱うプログラムを書きたいとします。
そしてその個人情報をバイナリファイルにレコードの単位で保存して読み書きするとします。
たとえば↓のようなイメージです。

太朗 170 16

↑の太郎の行がレコード1件分にあたります。
「太郎」という名前や「170」という身長、「16」という年齢がフィールドです。
バイナリファイルにはこのレコードがずら~っと並んでいます。

太朗 170 16
花子 160 12
次郎 180 30
三郎 190 51

↑の例ではわかりやすくレコードを改行で区切っていますが、実際には改行もありません。

固定長レコードとは?

固定長レコードとは、さきほどのレコードで、バイト数がある一定値で決められているレコードのことです。
このバイト数はフィールドごとにきまります。

たとえば名前は32バイト、身長は8バイト、年齢は4バイトとします。
このバイト数はそれぞれのフィールドが扱えるデータの容量を表しています。
たとえば名前なら32バイト以下は保存できるけど、それより上は保存できない! と言った感じです。

この場合、このレコードのバイト数は 32 + 8 + 444バイトになります。
ファイルにレコードを書き込むときは必ずこの44バイトのバイト数でレコードを書き込みます。
読み込むときもかならず44バイト読み込みます。

こうすることでファイルに保存されているレコードはすべて決まったバイト数になります。
そうすると、ランダムアクセスで都合がよくなるのです。

レコードのバイト数が決まっている、つまり固定長だと、ランダムアクセスによるレコードへのアクセスがとても速くなります。
なぜかというとseekメソッドの第1引数にはバイト数を指定しましたが、このバイト数を求めるときにレコードが固定長だと↓のようにしてアクセスしたいレコードまでのバイト数がわかるからです。

アクセスしたいバイト数 = レコードのバイト数 * 先頭からの件数

たとえばレコードのバイト数が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になります。
この64s64バイトのバイト列と言う意味です。

次に身長は4バイトでstructのフォーマットはiで、年齢も4バイトでstructのフォーマットはiにします。

すると↓のようなレコードの定義になります。

  • 名前(64バイト, フォーマット64s)

  • 身長(4バイト, フォーマットi)

  • 年齢(4バイト, フォーマットi)

  • レコード1件分のバイト数は合計で72バイト

固定長レコードを書き込む

先ほど定義したレコードを使ってバイナリファイルにデータを書き込んでみます。

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に渡せるのはバイト列かそれに相当するデータだけです。なので太朗という文字列をencodeutf-8のバイト列にしています。


struct.packの第1引数のフォーマットには先ほど定義したフィールドのフォーマットをつなげて書きます。

64sii

↑のフォーマットは先頭から名前の64s, 身長のi, 年齢のiになっています。
ちなみにどんなバイト列が返ってくるか↓のコードで確認してみましょう。

import struct

data = struct.pack('64sii', '太朗'.encode('utf-8'), 170, 16)
print(data)

↑のコードの実行結果は↓のようになります。

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してるコードを実行すると、↑のバイト列がファイルに書き込まれます。

固定長レコードを読み込む

先ほど書き込んだレコードを読み込んでみましょう。

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です。

# レコードのフォーマット
record_format = '64sii'

次にstruct.calcsizeでレコード全体のサイズをフォーマットから計算しています。
record_size変数には計算したレコードのバイト数が入ります。

# レコードのサイズをフォーマットから計算する
record_size = struct.calcsize(record_format)

その次にバイナリファイルをrbで開きます。

ファイルオブジェクトfinのメソッドreadにレコードのバイト数を指定して、ファイルからレコードのサイズ分のデータ、バイト列を読み込みます。

    # レコードのサイズ分データを読み込む
    data = fin.read(record_size)

その次に読み込んだバイト列をstruct.unpackに渡して、タプル(fields)にします。
struct.unpackは第1引数のフォーマットに従って第2引数のバイト列をPythonの型のタプルに変換するstructモジュールの関数です。

    # 読み込んだデータを指定のフォーマットでPythonの型に変換
    fields = struct.unpack(record_format, data)

struct.unpackは戻り値としてタプルを返すので、fieldsにはタプルが入ります。
そしてタプル内のフィールドをprintで出力します。

    # 読み込んだデータを出力
    print('名前:', fields[0].decode('utf-8').rstrip('\x00'))
    print('身長:', fields[1])
    print('年齢:', fields[2])

fieldsには0番目の要素に名前のバイト列、1番目の要素に身長、2番目の要素に年齢が入っています。
名前はバイト列になっているのでdecodeメソッドでutf-8の文字列に変換する必要があります。
この時、バイト列内の0の値がゴミとして文字列の右側に入ってしまうので、rstripでゴミを削除しています。

↑のコードの実行結果は↓のようになります。

名前: 太朗
身長: 170
年齢: 16

おわりに

今回は固定長レコードのランダムアクセスの基本を解説しました。

ランダムアクセスしてないけど?

固定長レコードのランダムアクセスは次回やります。
以上、次回に続きます。

また見てね

関連動画





スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク