ファイル入出力


これまでのプログラムでは、データの入力を行なうときはgetsメソッドを利用した。getsは、プログラム起動時に引数を何も指定しないと端末(キーボード)から、引数にファイルを指定した場合そのファイルから1行ずつ内容を読む。

% ./test_grades.rb
   (gets はキーボードからデータを読み込む)
% ./test_grades.rb scores.txt
   (gets は scores.txt ファイルからデータを読み込む)

一方で、プログラムに与えるデータが予め決まっていて、常に一つの特定のファイルに保管されている場合などは、プログラムを実行する度にファイルを引数として与えなくても、そのファイルからデータを読み込む方法がある。


openメソッド

ファイルからデータを読み込む前に、先ずはファイルを開かなければいけない。そのために利用するのがopenメソッドである。openメソッドのは以下のように使われ、指定したファイルを指定したモードで開く。

open(ファイル, モード) do |変数|
  開いたファイルを使って行う処理 (たとえば、変数.gets によってファイルからデータを読む)
end

ファイル」には、オープンするファイルのパス名を指定する。プログラムが動作しているカレントディレクトリからの相対パスも、 ルートディレクトリからの絶対パスのどちらでも使用できる。オープンしたいファイルがカレントディレクトリ(プログラムと同じディレクトリ)に入っている場合は、ファイル名だけで十分。

「モード」には、ファイルに対して行いたい処理に応じて以下の文字列のうちのどれかを指定する。


ファイルからの読み込み処理

ファイルからの読み込みをするには、openメソッドに読み込みしたいファイル名と、読み込むためのモード指定 "r" を指定する。| | の中の変数の名前は、そのファイルへのアクセスのときに使いたい任意のものにする。

open("phonelist.txt", "r") do |file|
  while line = file.gets  # file.gets で phonelist.txt を1行読み line に代入
    …処理…
  end
end

とすると、カレントディレクトリにある phonelist.txt というファイルを読み込み専用モードで開き、開いた結果を変数 file に入れる。このファイルから1行ずつデータを読むには、file に備わっている gets メソッドを使う。つまり、file.gets とすることで、開いたファイルから1行ずつ読んだ結果が返ってくる。

前回のプログラムtotal_value.rbを改良し、openメソッドを使ってファイルを読み込みしてみよう。

 練習問題  total_value2.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

open("products.txt", "r") do |file|
  while line = file.gets
    if /(\S+)\s+(\d+)円\s+(\d+)/ =~ line
      product = $1
      price = $2.to_i
      quantity = $3.to_i
      total = price * quantity
      printf("%sの総価格: %d\n", product, total)
    end
  end
end

結果:

sime{c11xxxx}% ruby total_value2.rb
パンの総価格: 9900
おにぎりの総価格: 1800
コーラの総合格: 31800
ボールペンの総価格: 23880

ファイルへの書き出し

ファイルへの書き出しは、open メソッドに、書き込むためのモード指定 "w" または、"a" いずれかを(用途に応じて)指定する。たとえば、以下のような処理となる。

open("grades.txt", "w") do |outfile|
  …処理…
  outfile.print "……書き出す文字列……"
  …処理…
end

書き出しを行なう場合には、open したときの変数に備わっている書き出し用メソッドを使う。今までのプログラムで、端末にメッセージを出力するときに利用していた、print, printf, puts メソッドがそのまま使える。

たとえば、

x = 5*5
printf("結果は %d です\n", x)

とすると、

結果は 25 です

と出力されるが、

 練習問題  rslt2file.rb
x = 5*5
open("result.txt", "w") do |outfile|
  outfile.printf("結果は %d です\n", x)
end

とすると、カレントディレクトリの result.txt というファイルに

結果は 25 です

と出力される。また、オープンモードを "a" にした場合、つまり

 練習問題  rslt2file2.rb
x = 6*6
open("result.txt", "a") do |outfile|
  outfile.printf("結果は %d です\n", x)
end

とすると、result.txt ファイルの末尾に追加され、ファイルの中身が以下のようになる。

結果は 25 です
結果は 36 です

配列の中身をファイルに保存したい時は、ループのなかで出力する。

 練習問題  array2file.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

products = ["パン", "チョコレート", "おにぎり"]
prices = [99, 150, 120]

filename = "prices.txt"

open(filename, "w") do |outfile|
    outfile.puts "商品 価格"
    i = 0
    # ループの中で商品名と価格を出力する
    while i < products.length do
        outfile.printf("%s %d円\n", products[i], prices[i])
        i += 1
    end
end

実行後のファイルの中身:

商品 価格
パン 99円
チョコレート 150円
おにぎり 120円

標準入出力

printfなどの出力メソッドで特に出力先を指定しない場合は標準出力という特別なオブジェクトとなる。標準出力は STDOUT という組み込み定数に結び付けられている。標準出力は最初は端末(ターミナル)に関連付けられている。標準出力はRubyプログラムの起動時に変えられていることもある。たとえば、

% ./program.rb > output.txt

とした場合は、標準出力は output.txt ファイルに関連付けられるようになる。このように標準出力をファイルに替えることをリダイレクションという。

もう一つの出力用オブジェクトとして、エラーや警告を出力する時に使われる標準エラー出力がある。STDERR という組み込み定数に結び付けられていて、最初は端末に関連付けられている。したがって、

STDERR.puts("100点以下の点数を入力してください!")

とした場合は、たとえ標準出力がファイルへリダイレクションされていたとしても、このメッセージの出力先は端末画面となる。

また、データを受け取るためのオブジェクトである標準入力STDIN という組み込み定数に結び付けられている。標準入力も最初は端末に関連付けられていて、キーボードから入力を受け取る。しかし、前回のようにRubyプログラムを起動するときに

% ./program.rb input.txt

とした場合、gets メソッドは input.txt からデータを読むため、標準入力から読めない。明示的に STDIN を指定し STDIN.gets とすることで読み取ることができる。

標準入出力について詳しく知りたい場合は、Unixひとめぐりページを参照してください。


外部コマンドとの入出力

open メソッドでは、ファイルだけでなく、

ことができる。この場合ファイル名の代わりに、| (パイプ)記号とコマンドを結合したものを指定すればよい。たとえば、

 練習問題  ls.rb
open("| ls -l", "r") do |filelist|
  while line = filelist.gets
    puts line
  end
end

とすると、ls -l コマンド(ディレクトリ内のディレクトリやファイルの一覧を表示するコマンド)を実行した結果を1行ずつ読み込むことができる。また、

 練習問題  bc.rb
open("| bc -l", "w") do |calc|
  calc.puts "9/2"
end

とすると、bc -l コマンド(ターミナルで数値計算を行うためのコマンド)を起動し、その入力として

9/2\n

を送出する(putsは末尾に改行文字を追加して出力する)。


ファイル入出力の処理例

 練習問題 

ファイル入出力の応用例として、決められた商品に対する、支払金額を計算するプログラムを作ろう。以下のような仕様のプログラムを作りたい。

  1. 商品名と単価を書いたファイルをデータファイルとして読み込む。
  2. 商品一覧をユーザに提示する。
  3. ユーザに商品番号を入力させる。
  4. 商品番号入力が終了したら合計金額を計算し、出力する。

プログラムの動作イメージ

以下のように、商品と価格の書かれたファイルがあったとする。

sushi.txt

サーモン        110円
イカゲソ        110円
えんがわ        110円
マグロ          110円
ネギトロ        110円
蛸              110円
赤貝            110円
穴子            110円
鉄火巻          110円
カッパ巻        110円
ハマチ          110円
サヨリ          180円
中トロ          180円
牛トロ          180円
寒ブリ          180円
イクラ          240円
タラバ          240円
大トロ          360円
ウニ            360円

プログラムを起動すると、このファイルを自動的に読み込み、その内容をメニュー番号とともに標準出力に出す。

0 サーモン        110円
1 イカゲソ        110円
2 えんがわ        110円
3 マグロ          110円
4 ネギトロ        110円
5 蛸              110円
6 赤貝            110円
7 穴子            110円
8 鉄火巻          110円
9 カッパ巻        110円
10 ハマチ          110円
11 サヨリ          180円
12 中トロ          180円
13 牛トロ          180円
14 寒ブリ          180円
15 イクラ          240円
16 タラバ          240円
17 大トロ          360円
18 ウニ            360円

ユーザがメニュー番号を順次入力し終えるとそれまで入力したメニューの価格を全て足した金額を出力する。

全部で2490円でございます。

会計プログラムの設計

上記の動作イメージから分かることは、プログラムのおおまかな流れとして

  1. 価格データファイル(sushi.txt)を読み、品目と金額を全て記憶する。
  2. 一覧を提示してユーザからの注文入力を繰り返す。
  3. 合計金額を出力する。

の3つがある。最後に合計金額を出力するので 2. のところで、つねに金額を足していなければならない。これらのことに注意して各段階の手順をプログラムにしていこう。

  1. 価格データファイルの読み込み

    まず、sushi.txtデータファイルから価格リストを読み込み商品名を配列変数に入れる部分を作ろう。さらにその値段部分を別の配列変数に入れておこう。ここでは商品名を入れる配列を menu 変数、価格を入れる配列を prices 変数としよう。するとプログラムの該当部分は以下のようになる。

    menu = []
    prices = []
    
    i = 0
    open("sushi.txt", "r") do |file| # sushi.txtを読み込みモードで開く
      while line = file.gets	 # ファイルから1行ずつ読む
        if /(\S+)\s+(\d+)円/ =~ line # 「[空白文字以外の文字1つ以上][1つ以上の空白文字][数字]円」 というパターンがあれば
          menu.push($1)		 # $1 で商品名を抽出して配列に追加
          prices.push($2.to_i)	 # $2 で価格を抽出して整数化して配列に追加
          printf("%3d %s\n", i, line.chomp)  # 商品番号と、読み込んだ行を出力
          i += 1
        end
      end
    end
    

  2. 注文の読み込み

    続いてユーザが対話入力で、商品番号を入れるループを作ろう。while true無限ループを作るので、何度でも注文できる。注文が終わったら "q" を入力するとループが終了する。

    while true  # 無限ループ
      print "御注文は(番号で入れてね、q で終了)?: "
      line = gets.chomp		# ユーザの入力を line に入れる
      if line == "q"		# "q" を入力したら
        break			#    終了
      end
      number = line.to_i		# 整数(メニュー番号)にする
      # メニュー番号に入っていない番号が入力された場合は再度入力させる
      if number < 0 || number >= menu.length
        puts "そんなメニューねぇでガス"
        redo
      end
      # 注文された商品名を表示し合計金額にその値段を足す
      printf("はあーい、「%s」一丁\n", menu[number])
      sum += prices[number]
    end
    

  3. 結果(精算金額)出力

    変数 sum に入っている合計金額を出力して終了する。

    print "おあいそでガス\n"
    printf("全部で %d 円でガス。まいどっ\n", sum)
    

以上を組み合わせることでプログラムが完成する。

sushi.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

sum = 0
menu = []
prices = []

i = 0
open("sushi.txt", "r") do |file|	# sushi.txtを読み込みモードで開く
  while line = file.gets		# ファイルから1行ずつ読む
    if /(\S+)\s+(\d+)円/ =~ line	# 「[空白文字以外の文字1つ以上][1つ以上の空白文字][数字]円」 というパターンがあれば
      menu.push($1)			# $1 で商品名を抽出して配列に追加
      prices.push($2.to_i)		# $2 で価格を抽出して整数化して配列に追加
      printf("%3d %s\n", i, line.chomp)	# 商品番号と、読み込んだ行を出力
      i += 1
    end
  end
end

while true  # 無限ループ
  print "御注文は(番号で入れてね、q で終了)?: "
  line = gets.chomp		# ユーザの入力を line に入れる
  if line == "q"		# "q" を入力したら
    break			#    終了
  end
  number = line.to_i		# 整数(メニュー番号)にする
  # メニュー番号に入っていない番号が入力された場合は再度入力させる
  if number < 0 || number >= menu.length
    puts "そんなメニューねぇでガス"
    redo
  end
  # 注文された商品名を表示し合計金額にその値段を足す
  printf("はあーい、「%s」一丁\n", menu[number])
  sum += prices[number]
end

print "おあいそでガス\n"
printf("全部で %d 円でガス。まいどっ\n", sum)

実行結果の例:

% ruby sushi.rb
御注文は(番号で入れてね、q で終了)?: 10
はあーい、「ハマチ」一丁
御注文は(番号で入れてね、q で終了)?: 13
はあーい、「牛トロ」一丁
御注文は(番号で入れてね、q で終了)?: q
おあいそでガス
全部で 290 円でガス。まいどっ

本日の課題

 基本課題 

以下のファイルscores.txtにさらに点数を追加したり、平均点数を計算したりできるプログラムtest_results.rbを作成せよ。
プログラムを起動すると、3つの選択肢が表示される:
1 - 全ての点数を出力
2 - 点数を追加
3 - 平均点数を計算
0 - 終了
ユーザーが0を入力しない限り、何度でも1・2・3を選べるように、無限ループとbreakを使うこと。

scores.txt

学籍番号 点数
C199000001 100
C199000002 80
C199000003 81
C199000004 84

実行結果の例:

{c11xxxx}% ruby ./test_results.rb
OPTIONS:
1 - 全ての点数を出力
2 - 点数を追加      
3 - 平均点数を計算  
0 - 終了
1
学籍番号 点数
C199000001 100
C199000002 80
C199000003 81
C199000004 84

OPTIONS:
1 - 全ての点数を出力
2 - 点数を追加
3 - 平均点数を計算
0 - 終了
3
平均点数: 86.25

OPTIONS:
1 - 全ての点数を出力
2 - 点数を追加
3 - 平均点数を計算
0 - 終了
2
学籍番号を入力してください
C199000005
点数を入力してください
100
点数が追加されました

OPTIONS:
1 - 全ての点数を出力
2 - 点数を追加      
3 - 平均点数を計算  
0 - 終了
1
学籍番号 点数
C199000001 100
C199000002 80
C199000003 81
C199000004 84
C199000005 100

OPTIONS:
1 - 全ての点数を出力
2 - 点数を追加
3 - 平均点数を計算
0 - 終了
3
平均点数: 89.00

OPTIONS:
1 - 全ての点数を出力
2 - 点数を追加
3 - 平均点数を計算
0 - 終了
0

本文は下記の通り記入してください.

氏名: 苗字名前
学籍番号: C11xxxxx

ソースコード:
...

実行結果:
...

 発展課題 

  1. test_results.rb を改良し、既存の点数を修正する機能を追加すること。
  2. sushi.rb を、最後に注文された商品の一覧と注文金額を記載された領収書を receipt.txt というファイルへ出力するように改良すること。

目次