(9) 06/12の授業内容:ファイルの入出力

出席課題

テーマ

最近読んだ本、最近観た映画、最近気になることのいずれかについて数行で熱く語る。

制限時間は5分。出席点は2点。提出要領は下記の通り。

  • 提出先:naoya@e.koeki-u.ac.jp
  • メールのSubject:学籍番号-ruby09
  • 本文の構成:1行目で学籍番号、氏名を記載する。2行目以降は自由。

Tips:emacsでの日本語入力のオンオフはCtrl-oです

Tips:Mewによるメールの送り方はMewコマンドを参照

openメソッド

前回のプログラムは、getsメソッドを使ってファイルから1行ずつ読み込み特定の処理をするものであった。ファイルの指定は、ktermでプログラムを実行する際に

% ruby mining.rb regi.txt

のように指定した。これにより指定した任意のファイルを自由に読み込むことが可能になる。

一方で、このような指定方法にはデメリットもある。ファイル名を間違えたり、指定し忘れるとプログラムが正しく動かなくなる。また、常に同じファイルを用いるのであれば、いちいち指定するのが煩わしい。

前回はregi.txtを読み込んで分析をするプログラムであった。分析は目的により異なるため、同一のプログラムを継続的に使用しないこともある。それゆえプログラム実行時にファイル名を指定する方式であってもあまり問題にならない。一方、regi.txtを作成するプログラムを考えてみる。これはレジスターの履歴を単にファイルに出力するだけであり、なおかつデータは時系列で1つのファイルに蓄積されていくため、ファイル名は常に固定である。このような場合レジスターの電源を入れる(=プログラムを実行する)たびに販売履歴の出力先として毎回同じファイルを指定するとなるとなんとも面倒である。

こうした問題に対処するために準備されているのがopenメソッドである。

openメソッド

基本構造

open("ファイル","モード")do |変数|
    ファイルを読み込んで行う処理
end

ファイル:ファイル名を指定する。ファイルとプログラムは同一のディレクトリに置く。

モード:指定したファイルを開いて読み込むのか、指定したファイルを作成して書き込むのか、既存のファイルを開いて追加書き込みするのか等、モードを指定する。以下の6種類から選ぶ。

  1. r:読み込み専用。
  2. r+:読み込み/書き込用。
  3. w:書き込み専用。指定したファイルを新規作成する(既存のファイルを指定した場合は自動的に削除され、新規作成される)。
  4. w+:読み込み/書き込み用で、その他はwと同じ。
  5. a:追加書き込み専用。指定ファイルが存在しない場合は新規作成し、既存ファイルを指定した場合はデータの末尾に続けて書き込みを行う。
  6. a+:読み込み/追加書き込み専用で、その他はaと同じ。

変数:printfやprintメソッドの表示先、getsメソッドの読み込み元を指定する際に使用する。任意の名前でよい。

openメソッドを使ったファイルへの書き込み

register.rbを改良し、もう少し現実的なデータマイニングができるようにしてみよう。まずは年代を入力できるように改良してみよう(register_final.rb)。このプログラムでは、実行すると結果をディスプレイに表示するのではなく、ファイルに出力する。出力する際のファイル名はpos.outである。

#!/usr/koeki/bin/ruby

product = [] #商品名
amount = []  #金額
sum = 0      #合計金額
item = 0     #配列のインデックス
tax = 1.05   #消費税率(5%に固定)
tc = 1       #transaction count(取引回数)

open ("pos.out", "a") do |pos|

#会計の繰り返し
  while true
    STDERR.print "いらっしゃいませ\n"
    STDERR.print "年代を入力(1:若年、2:中年、3:老年、9:終了):"
    age = STDIN.gets.chomp!.to_i
    if age == 9
      then break
    end

#商品名、金額の入力とお会計
    while true
      STDERR.print "商品名を入力(終了はq):"
      product[item] = STDIN.gets.chomp!
      if product[item] == "q"
        then break
      end
      STDERR.print "金額を入力:"
      amount[item] = STDIN.gets.chomp!.to_i
      sum += amount[item]
      item += 1
    end

    sum *= tax
    STDERR.print "\n"
    STDERR.printf ("お会計%d円になります\n", sum)
    STDERR.print "支払い金額を入力:"
    pay = STDIN.gets.chomp!.to_i
    STDERR.printf ("%d円お預かりいたします\n", pay)
    change = pay - sum
    STDERR.printf ("%d円のお返しになります\n", change)
    STDERR.print "どうもありがとうございました\n\n"

#商品名、金額をファイルに書き出し
    i = 0        #配列のインデックス(2回目以降は0にリセット)
    while i < item
      pos.printf ("%d \t %d \t %s \t %d\n", tc, age, product[i], amount[i])
      i += 1
    end

    tc += 1      #取引回数を1増やす
    sum = 0      #合計をリセットする
    item = 0     #配列のインデックスをリセットする
  end
end

このプログラムを実行した例を以下に示す。黄色の部分が実行中にキーボードから入力したものである。なお、ktermでの日本語入力の切り替えはShift-Space(シフトキーを押しながらスペースキー)である。

irsv{c106000}% ruby register_final.rb[Return]
いらっしゃいませ
年代を入力(1:若年、2:中年、3:老年、9:終了):1
商品名を入力(終了はq):みかん
金額を入力:200
商品名を入力(終了はq):りんご
金額を入力:400
商品名を入力(終了はq):q

お会計630円になります
支払い金額を入力:1000
1000円お預かりいたします
370円のお返しになります
どうもありがとうございました

いらっしゃいませ
年代を入力(1:若年、2:中年、3:老年、9:終了):3
商品名を入力(終了はq):
金額を入力:98

商品名を入力(終了はq):q

お会計102円になります
支払い金額を入力:200
200円お預かりいたします
97円のお返しになります
どうもありがとうございました

いらっしゃいませ
年代を入力(1:若年、2:中年、3:老年、9:終了):9

プログラムの実行結果を見ると、なんとも中途半端な終わり方をしているように見える。このプログラムでは、詳細は後述するが、購入データをpos.outというファイルに出力している。では、pos.outにどのような情報が書き込まれているのかを見てみよう。

catはktermで使用するコマンドで、ファイル内を表示するものである。catの後ろに中味を見たいファイル名を指定し[Return]を押す。catと同じようにファイル内を表示するコマンドとしてlessコマンドがある。catコマンドは上下のスクロールができないのでファイル内のデータが多い場合には、上のほうはktermの画面の外に消えてしまい見ることができない。lessコマンドはkterm内で自由にスクロールをすることができる。lessコマンドを使用した場合、見終わったらqを押す。なお、ファイルを削除したい場合はrmコマンドを使用する。rm pos.out[Return]でpos.outが削除される。一旦削除したファイルはどんなことをしても復活することはできない。削除するときはよく確認すること。

irsv{c106000}% cat pos.out[Return]
1     1     みかん       200
1     1     りんご       400
2     3     卵            98

pos.outを見ると、1行につき、2つの数字、商品名、金額の4つのデータが書かれていることがわかる。最初の数字は取引回数を表し、1は最初のお客様、2は2番目のお客様ということになる。次の数字は年代で1は若年、3は老年となる。1人のお客様が2つの商品を購入するとデータは2行分になる。ここには書かれていないが5つ購入すれば5行分のデータになる。

なお、もう一度プログラムを実行し、適宜入力をして終了をしてから再度pos.outの中味を表示してみると、以下のようになる。

irsv{c106000}% cat pos.out[Return]
1     1     みかん       200
1     1     りんご       400
2     3     卵            98
1     2     大根         128
1     2     ほうれん草   100
1     2     たまねぎ     198
2     1     しょうゆ     398
2     1     さんま        58
3     3     卵            98
3     3     牛乳         128
3     3     食パン       100
3     3     大根         128

先ほどのデータに新しいデータが追加されていることがわかる。ただ一番左端の数字を見ると、2回目の通し番号がつながっておらず、また1から開始されているという問題がある。

このプログラムについて、まず全体的な構造を確認し、続けて詳細に見ていこう。このプログラムではwhile-endが3回登場している。そしてそれらをopen-endがさらに取り囲んでいる。まずは構造を図示してみよう。

open ("pos.out", "a") do |pos|
(open-endで囲むことでこの間で行う処理の結果をファイルに出力できる)
while true
  STDERR.print "いらっしゃいませ\n"
  STDERR.print "年代を入力(1:若年、2:中年、3:老年、9:終了):"
                          :
(以下の2つのwhile-endを繰り返し行う)
(これにより複数のお客さんに対応できるようになる)
while true
  STDERR.print "商品名を入力(終了はq):"
                 :
end
(1人のお客さんについて商品名、金額の入力とお会計を行う)
while i < item
  pos.printf ("%d \t %d \t %s \t %d\n", ・・・・
                        :
end
(そのお客さんについて商品名、金額等をファイルに出力する)
end
end

この構造を前提に、新しく出てきた部分(プログラムの黄色の部分)について順に説明する。

open ("pos.out", "a") do |pos|

openメソッドを使って、pos.outというファイルを開いている。開くときのモードはaであるため、追加書き込み専用となる。はじめてこのプログラムを実行した際には、pos.outというファイルは存在しないため、新規作成される。しかし、2回目以降はpos.outは存在していることから、データの末尾に追加で書き込みが行われる。なおdo |pos|のposはファイルに書き込みを行う際の変数として指定している(後述)。

STDINとSTDERR

openメソッドを使用してファイルを開いているため、printやprintfを使って何かを表示する場合、

  • ディスプレイに表示する
  • ファイルに表示する(=ファイルに書き込む)

という2種類の表示先が存在する。このため、どちらに表示するかを指定する必要が生じる。同様にgetsメソッドは値を読み込むメソッドであるが

  • キーボードからの入力を読み込む
  • ファイルの内容を読み込む

という2種類の読み込み元がある。この場合も、どちらから読み込むのかを指定する必要がある。

キーボードからの入力を受け取る場合はSTDIN.getsのようにgetsの前にSTDINをつける。またディスプレイへの表示を行う場合はSTDERR.print, STDERR.printfのようにprintやprintfの前にSTDERRをつける。

pos.printf ("%d \t %d \t %s \t %d\n", tc, age, product[i], amount[i])

通常のprintfにpos.がついている。このposは、openメソッドの行で指定した変数名である。変数名.メソッドとすると、そのメソッドはファイルに対して行われる。ここではpos.printfとしてファイルに出力している。このプログラムでは使用していないが、pos.printも同様にファイルに出力し、pos.getsはファイルから1行読み込むことになる。

ファイルに出力する内容は、後ろの()で指定したものであり、ここでは4種類となる。tcはお客さんの通し番号、ageは年代、productは商品名、amountは金額となる。tc、age、amountは数字であるため%dを使用している。productは文字であるため、書式制御文字に%sを使用して文字列を表示できるようにしている。

openメソッドを使ったファイルからの読み込み

register_final.rbによりお客さんの購入データがファイルに出力できるようになったので、このデータを読み込んでデータマイニングを行うmining.rbもopenメソッドを使って書き直してみよう(mining_final.rb)。

#!/usr/koeki/bin/ruby

#初期設定

name = []     #商品名を代入する配列
price = []    #金額を代入する配列
cost = []     #仕入額を代入する配列

profit = 0    #利益額を代入する配列
n = 0         #配列に使用するインデックス
i = 0         #配列に使用するインデックス

#商品名と金額の読み込みと利益額の計算

open ("regi.txt", "r") do |mining|

  while line = mining.gets
    if /\S+\s+\S+\s+(\S+)\s+(\d+)\s+(\d+)/ =~ line
      name[n] = $1
      price[n] = $2.to_i
  	  cost[n] = $3.to_i
	  profit += $2.to_i - $3.to_i
	  n += 1
    end
  end

#個々の利益額の表示

  STDERR.print "- 商品名 ------- 利益 --\n"
  STDERR.print "\n"
  while i < n
    STDERR.printf ("%-10s \t %3d円\n", name[i],price[i]-cost[i])
    i += 1
  end

  STDERR.print "-------------------------\n"
  STDERR.printf ("利益(合計)     %5d円\n",profit)
  STDERR.printf ("利益(平均)     %5d円\n",profit/n)
end

mining.rbから変更した箇所は以下の通り。

open ("regi.txt", "r") do |mining|

前回はktermでプログラムを実行する際にregi.txtを指定していたが、今回はopenメソッドを使ってプログラム内で指定している。開く際のモードはrであり、読み込み専用となる。

while line = mining.gets

openメソッドの行で指定した変数名を使い、変数.メソッドで、ファイルに対してそのメソッドを適用できる。今回は変数名をminingにしているので、mining.getsとすることで、regi.txtから1行ずつ読み込みlineに代入する。

その他、プログラム内の様々な箇所でSTDERRが使用されているが、これはディスプレイに結果を表示するために使用している。

レポート課題

以下の3種類の課題から実施できそうなものを選んで実施する。採点は以下の通り行う。

  • 4番のみ:7点満点で採点
  • 1番のみ:8点満点で採点
  • 1番+2番:9点満点で採点
  • 1番+2番+3番:10点満点で採点
  1. 自分がこれまでに改良した中で最も高機能なregister.rbを対象に、今回の授業で取り上げたregister_final.rbの機能を追加する。ここで、今回の授業で取り上げた機能とは(1)年代を選択できるようにする、(2)複数人のお会計を行うために何度もプログラムをktermから実行するのではなく、1回の実行で実現できるようにする、(3)Openメソッドを用い、結果をファイルに出力する(出力項目はregister._final.rbと同一とする)の3種類であり、これら3種類全ての機能を追加する。
  2. 1番の改良を施したregister.rbを対象に、register_final.rbにおいてプログラムを実行するたびにpos.outでお客さんの通し番号が1からになってしまう問題を解決する。具体的に言うと、例えば1回目にプログラムを実行したときに3人のお客さんが買い物をし、そこで一旦終了したとする。もう一度プログラムを実行して今度は4人が買い物をしたとする。合計で7人が購入しているが、pos.outを見ると、一番左端の通し番号は1、2、3、1、2、3、4となってしまう。これが1、2、3、4、5、6、7になるようにプログラムを書き換える。ここでプログラムを実行するとは、ktermから%ruby register_final.rb[Return]と入力してプログラムを動かすことを指す。
  3. mining_final.rbを改良し、1番と2番で改良したregister.rbにより作り出されたファイルを読み込み、年代別の平均売り上げ個数、平均購入額をディスプレイに表示できるようにする。なお、可能であれば、register.rbとmining_final.rbを合体し、最後のお客さんの会計が終了すると同時に計算結果がディスプレイに表示されるようにする。2つのプログラムを合体するためには、単にmining_final.rbの1行目の#!/usr/koeki/bin/rubyを削除し、register.rbの下に貼り付ければよい(もちろん変数名やプログラムの内容については適宜修正する必要がある)。
  4. これまでに作成したプログラムの中から1つ選び、自分のwebページで公開する。

1番は既存のregister.rbにregister_final.rのb変更点を適用するものである。これまで作成してきたregister.rbはこの図の緑色と、青色の部分に相当する。オレンジ色の部分を追加して複数人のお会計が連続して行えるようにし、さらに赤色の部分を追加することで売り上げのデータをファイルに出力する必要がある。ファイルに出力するためには、青色の部分も適宜修正が必要となる。2番は、プログラムの中で1つ目のwhileに突入する前に、pos.outの中の一番最後のデータのtcがいくつであるかを調べておく必要がある。3番はmining_final.rbの改良だが、このプログラムはregi.txtを読み込むためのプログラムであるため、pos.outを読み込む形式に変更する必要がある。4番は1年生の後期で行ったhtmlの作り方を思い出してやってみること。

  • 提出先:naoya@e.koeki-u.ac.jp
  • 提出期限:6/18(日)23:59(1限履修者)·6/25(日)23:59(2限履修者)
  • メールのSubject:学籍番号-kadai07
  • 本文の構成:1行目で学籍番号、氏名を記載する。2行目以降は下記の構成とする

1番のみ・1番+2番・1番+2番+3番の場合

  1. 作成したプログラム
  2. プログラムの実行結果
  3. プログラムの説明
  4. 感想

4番の場合

  1. 作成したページのURL
  2. 感想

  • 採点基準(4番):期限内提出点(2点)、メールの体裁(1点)、htmlの構文(4点):htmlの構文はAnother HTML-lint gatewayで採点し、マイナスなら1点、50点以下なら2点、100点未満なら3点、100点なら4点とする。
  • 採点基準(4番以外):期限内提出点(2点)、メールの体裁(1点)、プログラム(2〜4点)、プログラムの説明(3点)
  • プログラムの説明(4番以外):変更箇所のみ説明すればよい。
  • 説明に関して、文章の意味がわかりづらい場合や、Webページを単にコピー&ペーストしたものは減点することがある。一度読み直してから提出すること。
  • 驚異的に良くできているレポートについては満点の8点を超える得点をつけることがある。
  • よくできていたレポートは、他の人の参考になるよう、本人が特定できないような形で掲載する。掲載してほしくない場合はメールでの課題提出時にその旨記載すること。

Tips:emacsでの日本語入力のオンオフはCtrl-oです

Tips:ktermでのプログラムの実行結果をメールに貼り付けるには、コピーしたい箇所をマウスで選択し、emacs(Mew)上でマウスの真ん中ボタンをクリックする

Tips:Mewによるメールの送り方はMewコマンドを参照