Pythonでバイナリファイルをシークする【ランダムアクセス】
- 作成日: 2021-04-20
- 更新日: 2023-12-24
- カテゴリ: Python
Pythonでバイナリファイルをシークする
Pythonではバイナリファイルをシークすることができます。
シークとは、ファイル内の読み込み/書き込み位置(ストリーム位置)を移動させることを言います。
バイナリファイルのシークはたとえば↓のようにします。
import os
with open('file.dat', 'rb') as fin:
fin.seek(4, os.SEEK_SET) # 先頭から4バイト目の位置に移動する
data = fin.read() # 4バイト目以降のデータをすべて読み込む
print(data)
具体的に↓を解説していきます。
- ランダムアクセスとは?
- Pythonのバイナリファイルの正体
- seekable()でランダムアクセス可能か確認
- seek()を使ったランダムアクセス
- 固定長レコードの読み込み
ランダムアクセスとは?
バイナリファイルにおいて、読み込み/書き込み位置を開発者の好きな位置に移動させることを「シークする」と言います。
そしてこのようなファイル操作のことを「ランダムアクセス」と言います。
ファイルのストリーム位置を好きな位置に移動させることで、余計なデータ読み込みなどを省略し、ファイル内の必要なデータ領域だけをピンポイントで読み込み/書き込み出来るという点で、高度な技術になっています。
文献によると「ランダムアクセスができれば中級者」というような記述もありますが(書籍名は失念)、つまりはそれぐらいファイルアクセスに踏み込んだ技術と言うことになります。
ランダムアクセスの応用としては、データベースの作成などがあげられます。
固定長のレコードを持つデータベースは、ランダムアクセスでストリーム位置を移動すれば、コストを最小限にしてレコードのデータを読み込み/書き込みできます。
このような設計のデータベースは動作が早く、実用的なデータベースになります。
さらに実用化させたい場合はB木などのデータ構造の知識が必要になります。
バイナリファイルをシークできるようになると、バイナリファイル内の好きな位置から好きなデータを取り出せるという感覚を味わうことができます。
これはプログラマーにとって、ある種の万能感のようなものを感じることが出来て、個人差ありますがやってて楽しい作業だと思います。
シークを使わないとどうなるかというと、ファイル全体を読み込んで、読み込みの分プログラムの動作速度が落ちたりなど、デメリットがあります。
もっとも、それほど大したデータ量でなければ、丸ごと読み込んでプログラム的に加工するというのも現実的な手段と言えます。
例としてはJSONファイルなどの読み込み/書き込みです。
ランダムアクセスがその威力を発揮するのは数万件と言うレコードが存在するバイナリファイルを扱う場合だと予想できます。
レコードが数万件あっても、ランダムアクセスであればレコードが固定長の場合は読み込み位置を計算式で計算することができます。
そのため現実的にはレコード長分の読み込みコストだけで済みます。
Pythonのバイナリファイルの正体
open()
にモードrb
を指定した場合、開かれるファイルオブジェクトはio.BufferedReader
になります。
このクラスはio.IOBase
を継承したクラスです。
IOBase
にはメソッドseek()
があります。
このseek()
がランダムアクセスのためのメソッドです。
つまりBufferedReader
からseek()
メソッドを呼び出すことでランダムアクセスを行うことが出来るということになります。
seekable()でランダムアクセス可能か確認
BufferedReader
からはseekable()
メソッドを呼び出すことができます。
これはファイルオブジェクトがランダムアクセス可能かを真偽値で返すメソッドです。
返り値がTrue
であればランダムアクセス可能で、False
であればランダムアクセス不可です。
このメソッドがFalse
を返すファイルオブジェクトはseek()
やtell()
などを呼び出すとOSError
が発生します。
OSError
を発生させたくない場合は例外をキャッチするか、または事前にseekable()
でランダムアクセス可能かを調べておくと良いと思います。
with open('file.dat', 'rb') as fin:
print(fin.seekable())
↑の結果は↓になります。
モードrb
はランダムアクセス可能です。
True
seek()を使ったランダムアクセス
バイナリファイルのランダムアクセスを行うにはseek()
を使います。
seek()
は第1引数にオフセット・バイト数、第2引数に基点位置を指定します。
seek()
の基本的な使い方は、まず第2引数の基点位置にオフセット・バイト数の基点となる位置を指定します。
それからオフセット・バイト数分、ストリーム位置を移動させます。
第2引数の基点位置に指定できる定数はos
モジュールに定義されています。
os
モジュール内の↓の定数を利用できます。
- SEEK_SET ... 値は0. ストリームの先頭を表す(デフォルト)。正の値のみ。
- SEEK_CUR ... 値は1. 現在のストリーム位置を表す。負の値も可能。
- SEEK_END ... 値は2. ストリームの末尾を表す。負の値を指定する。
モードrb
でファイルを開いた直後、ファイル内のストリーム位置は先頭にあるのが普通です。
もっとも、モードab
などはストリーム位置が末尾になるので注意が必要です。
その状態から基点位置をseek()
にセットし、その基点位置から何バイト分ストリーム位置を移動させたいか指定します。
たとえば↓のコードを見てください。
import os
with open('file.dat', 'rb') as fin:
fin.seek(4, os.SEEK_SET) # 先頭から4バイト目の位置に移動する
↑の場合、シーク後のストリーム位置は先頭から4
バイト目になります。
つまりこのシークさせた直後にread()
等を呼び出すと、4
バイト目からデータを読み込むということになります。
またSEEK_CUR
の使い方は↓のようになります。
import os
with open('file.dat', 'rb') as fin:
fin.seek(4, os.SEEK_SET) # 先頭から4バイト目の位置に移動する
fin.seek(-2, os.SEEK_CUR) # 現在位置から2バイト分戻る
↑の場合、最初にSEEK_SET
でストリーム位置を先頭から4
バイト目に移動しています。
それからSEEK_CUR
で現在の位置からストリーム位置を2
バイト分、先頭側に戻しています。
つまり結果のストリーム位置は先頭から2
バイト目になります。
次にSEEK_END
を見てみます。
import os
with open('file.dat', 'rb') as fin:
fin.seek(-4, os.SEEK_END) # 末尾から4バイト分戻す
↑の場合、SEEK_END
でストリーム位置を末尾から-4
バイト目に移動させています。
つまりこの状態でread()
を呼び出した場合、末尾の4
バイト分のデータを読み込めるということになります。
固定長レコードの読み込み
ではランダムアクセスで固定長レコードの読み込みにチャレンジしてみたいと思います。
↓のようなファイルfile.dat
を用意します。
1234223432344234
このファイルは1つのレコード長が4
バイトです。
つまり↑のデータは「1234」「2234」「3234」「4234」という4つのレコードが並んでいる状態です。
この3
つ目のレコード「3234」をランダムアクセスを使って読み込んでみたいと思います。
import os
with open('file.dat', 'rb') as fin:
record_size = 4 # レコードのサイズ
offs = record_size * 2 # オフセット位置を計算
fin.seek(offs, os.SEEK_SET) # ランダムアクセス
print(fin.read(record_size)) # レコードサイズ分読み込む
↑の結果は↓になります。
b'3234'
無事に3
つ目のレコードを読み込めました。
↑の場合、3
つ目のレコードの位置は8
バイト目以降になります。
そのためその前の2
つのレコード長を計算(record_size * 2
)して、オフセット位置を求めています。
あとはこのオフセット位置をseek()
にSEEK_SET
と共に渡せば、3
つ目のレコードの位置に移動できることになります。
そしてread()
でrecord_size()
分読み込めば、目的のレコードの領域のデータのみを取り出すことができます。
このようにランダムアクセスと固定長レコードを使うと、計算式で読み込み位置などを計算することが可能です。
おわりに
今回はseek()
を使ったランダムアクセスについて見てみました。
ランダムアクセスが出来るようになるとバイナリファイルをほぼ自由自在に扱うことが出来るようになります。
バイナリファイルで万能感を味わえるのでおすすめな技術と言えます。
🦝 < 人生にランダムアクセスしたい