【Python】バイナリファイルのランダムアクセスの基本・前編【入門第72回】

208, 2019-10-02

目次

バイナリファイルへのランダムアクセス

ランダムアクセスとはファイルのデータに自由自在にアクセスする技術です。
これが使えるようになると簡単なデータベースなどを作れるようになります。

ちなみにランダムアクセスと対になるのがシーケンシャルアクセスで、これはファイルの先頭から順々にアクセスしていく方法です。

ファイルの入出力において、バイナリファイルのランダムアクセスは初心者の方にとっては1つの難関となるでしょう。
一部では、ファイルのランダムアクセスができるようになったら、その人のプログラミングの腕は中級レベルだと言われています。
ですので初心者の方はできなくても気にせずに何度もトライしてみてください。

ランダムアクセスでは主にseekメソッドを多用します。
このメソッドでファイルオブジェクト内のオフセット位置を変更し、読み込みや書き込みを行います。
seekメソッドはプログラマーの好きな位置にファイルのオフセット位置を変更することが出来ますが、これをランダムアクセスと呼びます。

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

  • バイナリファイルを読み書きモードで開く

  • ファイルオブジェクトのオフセット位置とは?

  • seekメソッドの詳細

  • ファイルの先頭にシークする

  • ファイルの先頭からデータを読み込む

  • ファイルの末尾にシークする

  • ファイルの末尾にデータを追記する

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

バイナリファイルを読み書きモードで開く

ランダムアクセスは基本的にseekメソッドが使えれば可能です。
そのため、特に「このモードで開かなくてはいけない」というのはありません。

今回は例として、r+bモードを使いたいと思います。
このモードは読み書きモードでバイナリファイルを開くモードです。
特徴として、ファイルが存在しない場合はエラーを送出し、ファイルが存在する場合はオフセット位置をファイル先頭にしてファイルを開きます。

たとえば↓のような内容のバイナリファイルalphas.datがあるとします。

abc

このファイルを開くには↓のようにします。

with open('alphas.dat', mode='r+b') as fp:
    pass

r+bモードについて詳しく知りたい方は↓の記事をご覧ください。

ファイルオブジェクトのオフセット位置とは?

開いたファイルのファイルオブジェクトはすべて内部に「ファイルのオフセット位置」を持っています。
ファイルオブジェクトはファイルを読み書きするとき、このオフセット位置を基準にしてデータを読み書きします。
たとえばオフセット位置が0の位置だったら、0の位置からreadwriteを行い、オフセット位置が32だったら、32の位置からreadwriteを行います。

このオフセット位置はtellメソッドで確認できます。
たとえば先ほどのalphas.datをモードr+bで開いた直後のファイルオブジェクト内のオフセット位置を↓のようにして出力してみましょう。

with open('alphas.dat', mode='r+b') as fp:
    print(fp.tell())

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

0

バイナリファイルの場合、tellメソッドの返す値はファイル先頭から現在のオフセット位置までのバイト数です。
↑の結果は0なのでファイル先頭から0バイト、つまりオフセット位置はファイル先頭になっています。

オフセット位置はreadwriteを呼び出すと、その分オフセット位置が進んで更新されます。
readwriteはデータを読み書きするとき、オフセット位置を進めながらデータを読み書きするためです。
そのためデータの読み書き前のオフセット位置と、データの読み書き後のオフセット位置は異なっている場合があります。

seekメソッドの詳細

このオフセット位置を手動で変更するのがランダムアクセスです。
オフセット位置を手動で変更するにはseekメソッドを使います。
seekメソッドはたとえば↓のようにして使います。

fp.seek(0, os.SEEK_SET)

seekメソッドの第1引数には第2引数で指定した基点からの相対位置、第2引数には基点となる定数を指定します。
os.SEEK_SETはファイル先頭を表す定数です。
この定数を使うには↓のようにしてosモジュールをインポートする必要があります。

import os

seek(0, os.SEEK_SET)の場合、基点はSEEK_SETなのでファイル先頭です。
その基点からの相対位置は0になっているので、この命令の意味は「ファイル先頭にシーク(移動)する」という意味になります。

↓が基点となる定数の一覧です。

  • SEEK_SET または 0 -- ストリームの先頭 (デフォルト)。 相対位置は 0 もしくは正の値でなければなりません。
  • SEEK_CUR または 1 -- 現在のストリーム位置。 相対位置は負の値も可能です。
  • SEEK_END または 2 -- ストリームの末尾。 相対位置は通常負の値です。

ちなみにストリームとはデータの流れのことで、この場合はファイル内のデータ全体を指しています。
つまり、データが流れている通り道のことを言います。
この概念はLinuxプログラミングなどに出てきますが、紛らわしい場合はファイルに置き換えても大丈夫です。
↓の公式ドキュメントではこのストリームという用語を使っていますが、本記事では「ファイル」と言います。

ファイルよりストリームのほうがなんかかっちょいい?

そう?

ファイルの先頭にシークする

ファイルの先頭にシークするにはseekメソッドを↓のようにして使います。

import os

with open('alphas.dat', mode='r+b') as fp:
    # ファイル先頭にシーク
    fp.seek(0, os.SEEK_SET)

    # 現在のオフセット位置を確認
    print(fp.tell())

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

0

seekメソッドの引数の意味は先ほど書いたとおりです。

ファイルの先頭からデータを読み込む

alphas.datファイルの先頭にオフセット位置をシークして、データを読み込むには↓のようにします。

import os

with open('alphas.dat', mode='r+b') as fp:
    # ファイル先頭にシーク
    fp.seek(0, os.SEEK_SET)

    # ファイル先頭からデータを読み込む
    data = fp.read()
    print(data)

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

b'abc'

readメソッドを使うとファイルのオフセット位置がファイル末尾まで自動で移動します。
そのため、ふたたびデータを読み込みたい場合は↓のようにして再びシークする必要があります。

import os

with open('alphas.dat', mode='r+b') as fp:
    # ファイル先頭にシーク
    fp.seek(0, os.SEEK_SET)

    # ファイル先頭からデータを読み込む(オフセット位置がファイル末尾に)
    data = fp.read()
    print(data)

    # ファイル先頭にシーク
    fp.seek(0, os.SEEK_SET)

    # ファイル先頭からデータを読み込む(オフセット位置がファイル末尾に)
    data = fp.read()
    print(data)

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

b'abc'
b'abc'

ファイルの末尾にシークする

alphas.datファイルの末尾にシークするには↓のようにします。

import os

with open('alphas.dat', mode='r+b') as fp:
    # ファイル末尾にシーク
    fp.seek(0, os.SEEK_END)

    # 現在のオフセット位置を確認
    print(fp.tell())

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

3

seek(0, os.SEEK_END)の第2引数のos.SEEK_ENDはファイル末尾を表す定数です。
その位置から0バイト目ということなので、これは「ファイル末尾へシークする」という意味になります。

alphas.datファイルの内容は↓のようになっています。

abc

そのため、tellメソッドが返した3バイトという値が、abccの後を指していることがわかると思います。
abca, b, cそれぞれが1バイトです。

ファイルの末尾にデータを追記する

alphas.datファイルの末尾へデータを追記するには↓のようにします。

import os

with open('alphas.dat', mode='r+b') as fp:
    # ファイル末尾にシーク
    fp.seek(0, os.SEEK_END)

    # ファイル末尾へデータを追記
    fp.write(b'def')

↑のコードを実行するとalphas.datファイルの中身は↓のようになります。

abcdef

writeメソッドを呼び出すと、ファイルのオフセット位置は書き込みに合わせて進みます。
そのため、↑の例ではwriteメソッドを呼び出した直後のオフセット位置はファイル末尾になっています。
ですので↓のようにして続けて追記することが可能です。

import os

with open('alphas.dat', mode='r+b') as fp:
    # ファイル末尾にシーク
    fp.seek(0, os.SEEK_END)

    # ファイル末尾へデータを追記
    fp.write(b'def')
    fp.write(b'ghi')
    fp.write(b'jkl')

おわりに

ランダムアクセスの基本・前編をお送りしました。
ここで紹介しているseekメソッドの使い方は基礎の基礎ですが、後編からちょっと難しくなる予定です。
後編へと続きます。

また見てね

関連動画





スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク