[CG実習 >  Rubyによるプログラミングの基礎 >  標準入出力]

標準入出力

これまでのプログラムでは,実行する際に,必要なデータをキーボードから入力していました. しかし,実用的なプログラムでは,しばしば,大量のデータを処理する必要があります. たとえば,この実習で毎回利用しているRubyのインタプリタrubyは, 与えられたRubyプログラムのファイルを読み込んで,解釈しながら実行しています. また,WWWブラウザは,HTML等の形式でファイルに書かれたWWWページを読み込んで, それを解釈した結果を表示しています. データは,このようにファイルとして与えられたり, プログラムの中で動的に生成されたりします.

ここでは,ファイルから大量のデータを入力する, あるいはファイルへ大量のデータを出力するために, 標準入出力を利用する方法について解説します.

もくじ

  1. 例題
  2. プログラムの仕様
  3. ファイルを介した入出力 --- 標準入出力の利用
  4. プログラム
  5. 標準入出力のリダイレクト
  6. パイプ
  7. 標準エラー出力
  8. ハッシュ

例題

英文テキストのファイルを読んで,そのファイルに現れる単語を頻度の高い順に並べて, 頻度とともに出力する.

プログラムの仕様

プログラムの流れは,おおよそ次の通りになるでしょう.

  1. ファイルを1行(文字列として)読み込む.
  2. 読み込んだ行(文字列)を単語に分解する.
  3. 単語の頻度を更新する(初めて現れた単語は頻度が1になるようにする).
  4. ファイルを全て読み終わるまで,1〜3を繰り返す.
  5. 単語を頻度の高い順に並べて,頻度とともに出力する.

ファイルを介した入出力 --- 標準入出力の利用

例題のプログラムでは,まずファイルの単語を読み込んでいく必要があります. 入力されたデータが多ければ,結果として出力される単語の頻度データも大量になります. その場合,出力されるデータを単に画面に表示させただけでは, データをきちんと見ることができなくなる危険性が高くなります. そこで,出力もファイルに保存することが考えられます.

もちろん,Rubyのプログラムでファイルを介した入出力を扱うことは可能です. しかし,そのためには,まずファイルを「開いて」,開いたファイルからの読み込み, あるいは開いたファイルへの書き込みを行って,最後にファイルを「閉じる」, という一連の操作が必要となります. つまり,これまでの入出力とは違う手続きが必要となるわけです.

じつは,これとは,別の方法で,ファイルを介した入出力を実現することができます. その方法では,プログラムでファイルを利用していることを意識する必要はありません. これまでと同様に,キーボードからデータを読んで, 画面にデータを出力するようにプログラムを書いておくだけで, ファイルとのデータのやりとりができます. その方法とは, 標準入出力リダイレクト(redirect)するというものです.

さて,標準入出力とは何でしょうか. これまでのプログラムでは,print,getsというメソッドをデータの入出力に用いていました. printは画面にデータの表示を行い, getsはキーボードからデータを読み込むメソッドとして利用してきました. じつは,正確には, 関数的メソッドprintは,標準出力(standard output)へデータを書き出すメソッドです. また,関数的メソッドgetsは標準入力(standard input)からデータを読み込むメソッドです. 通常は,標準出力は画面(コマンドを起動したターミナルの画面)と結びつけられていて, 標準入力はキーボードと結びつけられています. そのため,printで画面にメッセージが出力され, getsでは,キーボードからデータを入力することになっていたわけです Rubyでは,標準入力を$stdin,標準出力を $stdoutという特別な変数で表します. これらがprintやgetsのレシーバだったわけです (細かいことをいうと,getsに関しては,もう少し複雑な仕掛けがあります. 気になる人は,Rubyの組み込み定数ARGFについて調べて下さい).

標準入出力とキーボード

これまでのプログラムのprint,getsの記述を次のように書き換えても, プログラムは同じように動作します.

  $stdout.print 
  $stdin.gets   

先ほども述べた通り, printは,標準出力へデータを出力し, getsは,標準入力からデータを入力しています. しかし,これら標準入出力が,つねに画面とキーボードに結びつけられているわけではありません. これらの対応関係は変えることができます. 対応関係を変えることをリダイレクトといいます. 標準入力を別のファイルにリダイレクトしておけば, 標準入力から読み込むことで,キーボードから読み込む代わりに, ファイルからデータを読み込むことができます. また,標準出力を別のファイルにリダイレクトしておけば, 標準出力へ書き出すことで,画面に出力する代わりに, ファイルへデータを書き出すことができます.

標準入出力のリダイレクトの例

このようにリダイレクトした場合でも,プログラムでは,あくまでも, 標準入力からデータを入力し,標準出力へデータを出力していることにはかわりありません. つまり,リダイレクトを利用することで,プログラム自体を一切変更することなく, ファイルを介して入出力が実現できるわけです. なお,一般にデータの入力元や出力先となるオブジェクトのことを, 入出力ストリームといいます.

標準入出力のリダイレクトはシェルのコマンドラインで実現することができます. その方法について,例題に沿って説明します. 同時に,リダイレクトに関連したトピックとして, パイプについても説明します. さらに,今回の例題を扱うのに便利なハッシュという仕組みについて解説します.

プログラム

例題のプログラムをRubyで記述すると, 以下のようになります. なお,各行の左端の数字は説明のために付けた行の番号で, プログラムには,この行番号は含まれません.

ws.rb
[行番号つきプログラムを別のウィンドウで開く] [行番号なしのプログラム]

このプログラムは,たとえば次のように実行します.

$ ruby ws.rb <  input.txt > output.txt
$ less output.txt

この例では,input.txtというファイルを読んで, 結果をoutput.txtというファイルに記録するようにしています. なお,その次のlessコマンドは,output.txtの内容を見るために利用しています (lessの使いかた).

標準入出力のリダイレクト

上の実行例で示したように,標準入力,標準出力をファイルにリダイレクトするには, 次のように「<」あるいは「>」に続いてファイルを指定します.

$ command < file # commandの標準入力(stdin)のfileへのリダイレクト
$ command > file # commandの標準出力(stdout)のfileへのリダイレクト

リダイレクトの効果は,一時的なもので,リダイレクトを行ったコマンドのみで有効です. 次のコマンドでは,ふたたび,標準入力はキーボード,標準出力は画面に対応付けられます.

パイプ

さきほどの実行例では,ws.rbが出力する結果をいったんoutput.txtというファイルに格納して, 次にそれをlessで見るという2ステップの処理を行っていました. さて,結果をずっと保存したいのであれば, もちろんファイルに格納しておく必要があります. しかし,一時的に結果を調べたいだけで,保存しておく必要がないのであれば, わざわざファイルを作る必要はありません. そのような場合には, 次のようにすると,ws.rbが出力する結果をファイルに格納することなく, 1ステップで,そのまま直接lessに送り込むことができます.

$ ruby ws.rb <  input.txt | less

この仕掛けを可能にしているのが,Unixのパイプという仕組みです. 上のコマンドラインの「|」がパイプを表しています. パイプはコマンドをつなぐものです.パイプでコマンドをつなぐと, パイプの前のコマンドの標準出力が,次のコマンドの標準入力へリダイレクトされます. これは,複数(三つ以上も可)のコマンドを標準入出力でつないで,データを順に処理する パイプラインを作る仕組みであると考えると分かりやすいでしょう. まず最初のコマンドにデータが読みこまれ, その処理の結果が次のコマンドの標準入力へ流し込まれる, またそのコマンドの処理の結果がさらに次のコマンドの標準入力へ流し込まれるというように, パイプの中をデータが順に通っていくにつれ, 各コマンドで順にデータ処理が行われていくわけです.

パイプの動作例

ここで挙げた例では,まずws.rbでは, (ファイルinput.txtにリダイレクトされた)標準入力からデータを読み込んで, その結果を標準出力へ書き出します. ところが,この標準出力は,パイプによって, 次のコマンドlessの標準入力へ送り込まれます. lessはコマンドライン引数がなければ,標準入力のデータを処理するようになっています. したがって,この例の場合は,パイプによって送り込まれたws.rbの出力結果を処理することになるわけです.

Unixでは,パイプを利用して,コマンドを組み合わせることで, 実にさまざまな処理を実現することができます.

標準エラー出力

これまでの例題でも見てきた通り, プログラムの実行時に画面にメッセージを出力することがあります. メッセージは,入力を促したり, エラーの発生やプログラムの進行状況などをユーザに伝えるために利用されます.

メッセージは, リダイレクトやパイプによって出力するデータとは別々に扱うべきです. データを保存するファイルにメッセージを一緒にリダイレクトしたり, パイプでメッセージを次のコマンドに送ったりしても意味がありません. ところが,データとメッセージをどちらも同じ標準出力によって出力したとすると, これらの区別ができなくなります. リダイレクトすると,データとメッセージがともにファイルに保存され, パイプを利用すると,データとメッセージの両方が次のコマンドへ送られてしまうことになります.

そのため,メッセージを出力するストリームとして,標準出力の他に, 標準エラー出力(standard error)が用意されています. 通常,標準出力,標準エラー出力はともに画面と結びつけられているため, 区別がつきませんが, リダイレクト,あるいはパイプを用いたときは,両者に違いが現われます. リダイレクト,あるいはパイプでは,標準出力の対応付けのみが変更され, 標準エラー出力は,あいかわらず画面と結びつけられたままになります. これによって,データとメッセージの出力先を振り分けることができます. つまり,パイプでデータをやりとりすることを想定して データは標準出力に送って,メッセージは標準エラー出力に送ればよいわけです.

これまでの例題でも, 本来メッセージは標準エラー出力に送った方がよいのですが, 説明がややこしくなりますので,標準出力を用いていました. なお,Rubyで,メッセージを標準エラー出力へ送るには, $stderrのメソッドとしてprintを起動します.


  $stdout.print "メッセージ\n" # 標準出力へ書き出す
  print "メッセージ\n"         # レシーバを省略した場合(上と同じ)
  $stderr.print "メッセージ"   # 標準エラー出力へ書き出す

なお,ここでは述べませんが,標準エラー出力も必要であれば,リダイレクトできますし, パイプに送り込むこともできます.

ハッシュ

例題のプログラムでは,単語の頻度を調べようとしてします. したがって,プログラムでは,たとえば,次のようにしてデータを保持するのが自然でしょう.

  count["the"] = 21 #  "the"の頻度=21
  count["is"] =  16 #  "is"の頻度=16
   :
   :

これは,いってみれば,整数でない添字をもつ配列のようなものです. Rubyでは,まさにこういった機能をもつオブジェクトがあります. それがハッシュ(hash)です. 連想配列とよぶ場合もあります. ハッシュを用いることで,整数だけでなく任意のオブジェクトをキーとして, キーに対する値として任意のオブジェクトを対応付けて記録しておくことができます. ハッシュは次のように定義します.


  {"key"=>"value",1=>0,[1,2,3]=>true}  

ハッシュの要素は,「キー=>値」として定義します. この例に示す通り,キーと値は,一種類のオブジェクトである必要はありません. ハッシュの要素は,次のように,配列と同様に参照します.


  h = {"key"=>"value",1=>0,[1,2,3]=>true}  
  p h["key"] # ==> "value"
  p h[1]     # ==> 0

要素の追加も,この形式で行うことができます.


  h = {"key"=>"value",1=>0,[1,2,3]=>true}  
  h[1.2] = [0,0,1] # キー1.2に値として配列[0,0,1]を対応付ける
  p h              # ==> {"key"=>"value",1=>0,[1,2,3]=>true,1.2=>[0,0,1]}  

例題では, 「単語」をキーとして「その頻度」を値とする単語頻度表をハッシュで表現しています. 2行めでまず空のハッシュを用意しています. これは,空の単語頻度表を用意したことを意味しています. つづいて3行めでは, ハッシュに存在しないキーに対する値を参照したときに返す値(デフォルト値)を0に設定しています. これは,これまでに単語頻度表に登録されていない新しい単語が現われたときは, その頻度を0として扱うことを意味しています. なお,このデフォルト値を設定しない場合は,存在しないキーを参照するとnilを返します.


 2  dict = {}        # 空のハッシュを生成
 3  dict.default = 0 # ハッシュのデフォルト値を0にする.

プログラムでは,つづいて,getsで入力した行を処理するにあたって, まず文字列に対するsplitメソッドで, 文字列を空白文字(列)で分解して,単語の配列を得たのち, 配列の各要素(単語)をeachイテレータで処理しています. イテレータでは,9行めで各単語を全て小文字にしてから, 10行めでその単語の頻度を1増やしています. ここで,初めて登場する単語の場合には,上で解説した通り, 3行めの設定にしたがって, 右辺の「dict[word]」が0を返しますので,代入のあとは,頻度が正しく1となります.


 5  # 標準入力から1行ずつ読みこむ
 6  while line = gets 
 7    a = line.split                  # 単語に分解して配列にする
 8    a.each do |word|                # 各単語wordに対して
 9      word = word.downcase          # wordの大文字を小文字にする
10      dict[word] = dict[word] + 1   # wordの頻度を1増やす
11    end
12  end

データを全て読み終わって,頻度データを得たあとは,それをソートして結果を表示します. それを行っているのが,16行め〜20行めです.


14  # (ハッシュを配列に変換した上で)単語を頻度の高い順でソートして,
15  # 「単語 頻度」のリストを表示する.
16  dict.to_a.sort { |x,y|  
17     -(x[1] <=> y[1])
18  }.each { |k,v|
19    print v," ",k,"\n"
20  }

ハッシュはソートすることができないため,ここでは, まず単語頻度表のハッシュを配列に変換してから, その配列の要素を頻度の高い順でソートしています. 最後にソートされた配列に関して,eachイテレータでその内容を表示しています.

まずハッシュをto_aメソッドで配列に変換します. 結果として,[キー,値]という配列を要素とした配列が得られます. この例では,[単語,頻度]という配列を要素をもつ配列になります.

次にこの配列の要素を,sortメソッドで,頻度の高い順に並べ替えます. この例のようにsortにブロックが渡された場合, ブロックで定められる評価基準にしたがって要素を比較して,並べ替えを実行します. ブロックパラメタは二つ用意します. たとえば,これらを例のようにx,yとしたとき, ブロックの最後の式の値を次のように定めておきます.

  1. x,yを比較して,xが前に来るなら,負の値
  2. x,yを比較して,yが前にくるなら,正の値
  3. x,yが等しければ,0

sortにブロックが与えられない場合には,配列の要素を「<=>」メソッドで比較します. この例では,頻度の整数値を「<=>」メソッドで比較した結果を逆にして並べ替えています. 通常,整数の「<=>」メソッドでは, 「x <=> y」で,xが小さいときに負の値,大きいときに正の値, xとyが等しければ0を返します. したがって,頻度の高い順に並べ替えるには, 「<=>」メソッドの値の符号を反転する必要があるわけです.

プログラムでは,こうして頻度の高い順にソートされた配列に対して,eachイテレータを起動して, 各要素を表示しています. 配列の要素が,[単語,頻度]という配列であるため, ブロックパラメタを二つ用意して処理しています.

[CG実習 >  Rubyによるプログラミングの基礎 >  標準入出力]