解説: ランダム・アクセス・ファイルの扱い

今回は大きなデータを扱うために必要な技術の一つ、ランダムアクセスについて学びます。ランダムアクセスファイルとは、ファイルを「配列」のように扱う技術です。例えば、1万人以上の会員名簿のような大きなファイルを扱うことを考えましょう。 実際にはそのようなデータはSQLのような「データベース」システムで扱うのが普通ですが、ここではRubyを使って処理することを考えます。

前に取り上げた「アドレス帳ファイル」の問題では、名簿の情報はファイルに書かれており、それを処理するには一旦Rubyのプログラムですべて読み込んで(ハッシュに入れ)、searchやadd、updateのような操作をしていました。

しかし、1万個というような大きなデータをすべてファイルから読み込んだり、書いたりするのはとても時間がかかります。そこで、「必要な情報だけをRubyに読み込む」「必要な情報だけファイルに書きこむ」ことが必要になります。それを実現する一つの方法がランダムアクセスファイルです。

今回の学習項目です。

ランダムアクセスファイルとは

今まで扱ってきたファイルは、必ずファイルの先頭から順に読み込むようなものでした。これをシーケンシャル・アクセスといいます。これに対して、一行のサイズを固定にしておく(例えば100バイトとする)と、例えば10行目の内容を取り出すには、ファイルの先頭から(1行が100バイトならば)901バイト目から100バイト読み出せばよくなります。このように、一行の長さを固定したデータを「レコード」といいます。そして、レコードごとにデータを読み取りまたは書き込みすることをランダム・アクセスといいます。また、ランダムアクセスすることを前提に作られたファイルをランダムアクセス・ファイルといいます。
なお、実際にはどのようなファイルもランダムアクセス可能です。逆に言えば、ランダムアクセス・ファイルでもシーケンシャル・アクセスすることも可能です。

課題1.ランダムアクセスファイルの扱いの基礎

今までファイルを開くには、r, w, aという3つのモードのいずれかを使ってきました。例えば、sample.txt というファイルを「読み込み」モードで開くには

  fo = open("sample.txt","r")
としていました。同様にw とa というモードではファイルを「書き込み」用に開くために使ってきました。

ランダムアクセス・ファイルは、ファイルを「配列」と同じように扱うわけですから、そこからデータを読むだけではなく、「同時に」書き込みをすることができないと困ります。そのために r+ と w+ という二種類のモードがあります。

r+ もw+もランダムアクセスするために開くものですが、r+は既にあるファイルに対してん用いるのに対し、w+は新しくファイルを作る点が違います。

課題1-1

次のコードを実行し、結果を報告しなさい。またなぜそのような結果がえられたかを考えなさい。ただし、read(n)はファイルからnバイトのデータを読み込むメソッドで、write(data)はファイルにdataの値を書き込むメソッドです。これらについては次の課題1-2で詳しく取り上げます。

 fo = open("sample.txt","w+")
 fo.write("abcdefghijkl")
 fo.close
 fo1 = open("sample.txt","r+")
 p fo1.read(3)
 p fo1.read(3)
 p fo1.read(3)
 p fo1.read(3)
 fo1.close
 fo2 = open("sample.txt","w+")
 p fo2.read(3)
 fo2.close

課題1-2

今までのシーケンシャルアクセスによるファイルからデータを読み込む場合、ファイルの先頭から、順に「改行コード」までを単位としてgetsやrealineにより読み込みが行われてきました(そしてreadlinesはファイル全部を読み込んで、一行ずつのデータを文字列とする要素からなる配列を返すものでした)。

それに対し、ランダムアクセスによるファイルは、ファイルの「どこから」何バイト読み込むか(もしくは書きだすか)を指定します。また一般にはランダムアクセス・ファイルには改行コードが書かれません。従って「改行コードまでを単位とした読み込み(および書き込み)」は行われないのが普通です。

まず、ファイルのどこから読み込むか(書きだす)を指定するには pos や seekメソッドを使います。posは「ファイルポインタ」と呼ばれる値を返します。これは、ファイルの何バイト目から読み込むか(または書き出すか)の場所を表すものです。これに値をセットすれば(もしくはseekメソッドを使えば)、ファイルポインタを移動できます。また、ファイルの先頭にファイルポインタを戻すためには rewind というメソッドも用意されています。

次にファイルからデータを読み込む方法ですが、課題1-1で見たように、readメソッドを使います。readメソッドに数を引数として与えると、ファイルポインタから指定されたバイト数だけ読み出されます。そして、ファイルポインタは読み出されたバイト数だけ加算されます。

ファイルにデータを書き出す場合も同様に、writeメソッドを使います。writeメソッドに変数を引数に与えると、その値が文字列として、ファイルポインタの場所から書き出されます。そして、ファイルポインタは書き出されたバイト数だけ加算されます。

次のプログラムを実行したときの結果を予想しなさい。そして実際にプログラムを走らせ、予想と比較しなさい。またどうしてそのような結果になるかを説明しなさい。

 fo = open("sample.txt","w+")
 str = "abcdefghijkl"
 p str.length
 fo.write(str)
 p fo.pos
 fo.rewind
 p fo.pos
 p fo.read(3)
 fo.pos = 6       # fo.seek(6) としても同じ
 p fo.read(3)
 p fo.pos
 fo.seek(18)      # fo.pos = 18 としても同じ
 p fo.pos
 p fo.read(3)
 fo.close
 p fo.pos

課題2. ランダムアクセスファイルを扱うためのクラス

ここでは課題1で学んだことを基礎として、ランダムアクセスファイルを扱うためのクラスの定義について考えていきます。

ランダムアクセス・ファイルを扱うクラスをRafとします(Random Access File)。 このインスタンスを作るときに考えるべきものは、少なくともファイル名、レコードの大きさ、そして新規に作るかどうかの選択、となるでしょう。そこで、次のクラスの定義をベースとしましょう。

class Raf      # ランダムアクセスファイルを扱うためのクラス
  def initialize(fname,rsize,create=false)
     @file = fname           # ファイル名
     @recordSize = rsize     # レコードのサイズ
     if (create == true)
         @fo = open(@file,"w+")       # 読み書き両用で新規にファイルを作る
     else
         @fo = open(@file,"r+")       # 読み書き両用で既存のファイルを開く
     end # if
  end    # def of initialize
  def close   # ファイルのクローズ
     @fo.close
  end    # def of close
end  # class
以下はこれを用いたインスタンス作成の例です。
 rafOne = Raf.new("sample.txt",5)     # 既存のランダムアクセス・ファイルに対して、レコードサイズを5としてアクセス 
 rafTwo = Raf.new("test.txt",8,true)  # 新規にランダムアクセス・ファイルを、レコードサイズを8として作成

課題2-1.

ランダムアクセスファイルから読み込むためのメソッド read と、ファイルに書き出すためのメソッド write を作りましょう。以下のプログラムを適切なコードで穴埋めし、実行して、このデータでは正しく動くことを確かめなさい。

  def initialize(fname,rsize,create=false)
     @file = fname           # ファイル名
     @recordSize = rsize     # レコードのサイズ
     if (create == true)
         @fo = open(@file,"w+")       # 読み書き両用で新規にファイルを作る
     else
         @fo = open(@file,"r+")       # 読み書き両用で既存のファイルを開く
     end # if
  end    # def of initialize
  def seek(num)   # numはレコード番号(0が先頭)
     @fo.pos = num*@recordSize    # ファイルポインタの移動
  end    # def of seek
  def read(num)   # num番目のレコードの読み込み
                           # ファイルポインタの移動
      return               # 読みこんだデータを返す
  end    # def of read
 def write(num,data)   # num番目のレコードとしてdataを書き出す
                             # ファイルポインタの移動
                             # ファイルに書き出し
      return data  # 書きだしたデータを返す
  end    # def of write
  def close   # ファイルのクローズ
     @fo.close
  end    # def of close
end  # class

obj = Raf.new("sample.txt",5,true)
p obj.write(0,"abcde")
p obj.read(0)
p obj.write(3,"01234")
p obj.read(3)
obj.close

課題2-2.

writeメソッドの定義を洗練させることを考えます。writeメソッドによってファイルに書き込まれるデータは基本的には文字列だけではなく、数も書き込めると使い道が広がります。また、文字列であっても、レコードのサイズより長さが短かったり、長かったりする場合も問題になりそうです。特に、長い場合は1レコードでは収まりませんので、次のレコードにまたがることを許すか、それとも1レコードに収まるように加工(1レコードのサイズからはみ出る部分を削除)するか、方針を決める必要があります。さらに、データの長さが長い場合には、ユーザーに「警告」メッセージを出すことも考えないといけません。

writeメソッドを修正し、データとして数を許すようにすること、また文字列にした場合に長い場合は「はみ出た部分を削除し」てファイルに書き出すこと、またそのような場合に警告メッセージを出すようにしなさい。以下はテスト用のコードです。

 obj = Raf.new("test.txt",5,true)
 p obj.write(0,123)
 p obj.write(1,"abcdefgh")
 p obj.write(2,3.14159265)

課題2-3.

ここまで述べてきたランダムアクセスファイルは一つのレコードがひとつの情報を表すようなものでした。 しかし、前に扱ったアドレス帳ファイルでは、ファイルに氏名とメールアドレスを一行に書きこんでおき、Rubyのプログラムではそれを読み込んでハッシュとして扱う、というものでした。

つまり、一つのレコードの中に複数の情報を書き込んだり、一つのレコードから複数の情報を読み出したりしなければなりません。この情報それぞれをフィールドと呼ぶことにしましょう。これを用いて今回の課題のテーマを述べるとすれば、一つのレコードが複数のフィールドから構成される場合の処理方法、ということになります。

ここでは具体的にアドレス帳ファイルを例とします。アドレス帳ファイルでは氏名とメールアドレスという二つのフィールドからレコードが作られます。そこで考えるべきは、それぞれのフィールドのサイズです。ここでは氏名を表すために32バイト(日本語文字として16文字分)、メールアドレスとして28バイトの領域をとり、レコードとしてはあわせて60バイトとすることとしましょう。

このように考えた場合、必要な操作として、1個のレコードをフィールドに分ける(decodeとします)関数と、フィールドの情報をあわせて1個のレコードをつくる(encodeとします)関数が必要になることがわかるでしょう。ここで、decodeはレコードの文字列を引数に取り、氏名のフィールドの値を1番目の要素、アドレスのフィールドを2番目の要素とする配列を返すものとします。またencodeは逆に、氏名のフィールドの値を1番目の要素、アドレスのフィールドを2番目の要素とする配列を引数にとって、1レコードに相当する文字列を返す関数とします。

このように定義された関数decodeとencodeを作りなさい。

課題2-4.

課題2-3に引き続き、アドレス帳ファイルをランダムアクセスファイルで扱うことを考えます。

ここで扱うアドレス帳ファイルの例を以下とします。

address_list.txt

必要ならば課題2-1から課題2-3までに作ったクラスと関数を用いて、アドレス帳の先頭のレコード(これを1番目のレコードとします)と3番目のレコードを読み込み、それぞれの氏名とメールアドレスを表示するプログラムを書きなさい。また、4番目のレコードとして 氏名が豊田梨花、アドレスがrika@gmail.comの情報を書き込みなさい。そして、ファイルの内容を確認しなさい

これ以降の課題は「発展課題」ですので、時間がない場合はやらなくても構いません。

発展課題1.

ランダムアクセスファイルを使用する場合、「ファイルの中身をプログラムにすべて取り込むことはしない」というのが原則です。そのため、以前の課題で扱ったアドレス帳ファイルの扱いでは、一旦すべてのデータをファイルからRubyのハッシュ表にコピーすることにより効率よく検索ができましたが、ランダムアクセスファイルを使う場合にはそうはいきません。

この課題では、「氏名」から効率よく「アドレス」を探索する方法を考えましょう。それには二分探索法を用います。つまり、ランダムアクセスファイルでは、氏名により降順にソートされているものとするのです。またランダムアクセスファイルのレコードの数はファイルの先頭のレコードに書かれているものとします。(このようにランダムアクセスファイルでは、先頭のレコードは特別な情報をもたせていることがあります)。

そこで必要になるのは、氏名をキーとして二分探索し、氏名からアドレスを返すメソッドです(名前を find としておきます)。これは該当するデータを返しますが、該当するデータがない場合には nil を返すものとします。そのようなfindメソッドを定義し、適当なデータを作って正しく動くことを確かめなさい。

発展課題2.

最初に、ランダムアクセスファイルは、ファイルを「配列」のように扱う技術であると書きました。つまり、配列を使った技法がいろいろ使えます。ここでは、ランダムアクセスファイルをハッシュ表として扱うことを考えましょう。ハッシュ表とは、キー(ここでは「氏名」を考えている)からハッシュ関数と呼ばれる関数によりある範囲の整数を求め、これを配列のインデックスとみなす、という方法です。ハッシュにすることにより、二分探索よりもより効率的に「氏名」から「アドレス」を検索することができるようになります。

ただハッシュで注意しなければならないのはコリージョン、つまり異なるキーに対してハッシュ関数が同じ値を返す場合のことです。これには、再ハッシュ、もしくは線形探索探索(キーの番地から順に空き要素を探して入れる)、ということが考えられます。 どちらの方法でもよいですから、ランダムアクセスファイルをハッシュ表として扱うためのプログラムを実現しなさい。それには、キー(「氏名」)からアドレスを検索することと、氏名とアドレスの組みをランダムアクセスファイルに書き込むためのメソッド(関数)の実現が少なくとも必要です。

プログラミングIIIのホームに戻る