ユーニックス総合研究所

  • home
  • archives
  • python-seek-binary-file

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()を使ったランダムアクセスについて見てみました。
ランダムアクセスが出来るようになるとバイナリファイルをほぼ自由自在に扱うことが出来るようになります。
バイナリファイルで万能感を味わえるのでおすすめな技術と言えます。

🦝 < 人生にランダムアクセスしたい