これまでのプログラムでは実行する際に必要なデータをキーボードから入力していました. しかし実用的なプログラムでは,しばしば大量のデータを処理する必要があります. たとえば,この演習で毎回利用しているRubyのインタプリタrubyは,与えられたRubyプログラムのファイルを読み込んで解釈しながら実行しています. またWWWブラウザは,HTML等の形式でファイルに書かれたWWWページを読み込んで,それを解釈した結果を表示しています. データは,このようにファイルとして与えられたり,プログラムの中で動的に生成されたりします.
ここでは,ファイルから大量のデータを入力する,あるいはファイルへ大量のデータを出力するために,標準入出力を利用する方法について解説します.
もくじ
例題
プログラムの仕様
プログラムの流れは,おおよそ次の通りになるでしょう.
- ファイルを1行(文字列として)読み込む.
- 読み込んだ行(文字列)を単語に分解する.
- 単語の頻度を更新する(初めて現れた単語は頻度が1になるようにする).
- ファイルを全て読み終わるまで,1〜3を繰り返す.
- 単語を頻度の高い順に並べて,頻度とともに出力する.
ファイルを介した入出力 --- 標準入出力の利用
例題のプログラムでは,まずファイルの単語を読み込んでいく必要があります. 入力されたデータが多ければ,結果として出力される単語の頻度データも大量になります. その場合,出力されるデータを単に画面に表示させただけでは,データをきちんと見ることができなくなる危険性が高くなります. そこで,出力もファイルに保存することが考えられます.
もちろん,Rubyのプログラムでファイルを介した入出力を扱うことは可能です. ただしそのためにはファイルを扱うための準備の処理および後片付けの処理が必要となります.
じつは,これとは別の方法でファイルを介した入出力を実現することができます. その方法ではプログラムでファイルを利用していることを意識する必要はありません. これまでと同様に,キーボードからデータを読んで,画面にデータを出力するようにプログラムを書いておくだけでファイルとのデータのやりとりができます. その方法とは標準入出力をリダイレクション(redirection)するというものです.
さて,標準入出力とは何でしょうか. これまでのプログラムでは,print,getsというメソッドをデータの入出力に用いていました. printは画面にデータの表示を行い,getsはキーボードからデータを読み込むメソッドとして利用してきました. じつは,正確には関数的メソッドprintは標準出力(standard output)へデータを書き出すメソッドです. また関数的メソッドgetsは(通常は)標準入力(standard input)からデータを読み込むメソッドです. 通常は標準出力は画面(コマンドを起動したターミナルの画面)と結びつけられていて標準入力はキーボードと結びつけられています. そのため,printで画面にメッセージが出力され,getsではキーボードからデータを入力することになっていたわけです
Rubyでは,標準入力がSTDIN,標準出力がSTDOUTという定数に対応付けられています.
STDINにはgets,STDOUTにはprintというメソッドがあります. そこで,これまでのプログラムのgets,printを次のように書き換えることが可能です.
STDOUT.print STDIN.gets
さて標準入出力がつねに画面とキーボードに結びつけられているわけではありません. これらの対応関係は変えることができます. 対応関係を変えることをリダイレクションといいます. 標準入力を別のファイルにリダイレクションしておけば, 標準入力から読み込むことで,キーボードから読み込む代わりに, ファイルからデータを読み込むことができます. また,標準出力を別のファイルにリダイレクションしておけば, 標準出力へ書き出すことで,画面に出力する代わりに, ファイルへデータを書き出すことができます.
このようにリダイレクションした場合でも,プログラムではあくまでも標準入力からデータを入力し,標準出力へデータを出力していることにはかわりありません. つまり,リダイレクションを利用することでプログラム自体を一切変更することなくファイルを介して入出力が実現できるわけです. なお,一般にデータの入力元や出力先となるオブジェクトのことを入出力ストリーム(I/O-stream)といいます.
標準入出力のリダイレクションはシェルのコマンドラインで実現することができます. その方法について,例題に沿って説明します. 同時に,リダイレクションに関連したトピックとして, パイプ(pipe)についても説明します.
プログラム
例題のプログラムをRubyで記述すると以下のようになります.
[行番号つきプログラムを別のウィンドウで開く] [行番号なしのプログラム]
このプログラムは,たとえば次のように実行します.
$ ruby wf.rb < input.txt > output.txt $ less output.txt
この例では,まずwf.rbを実行して,input.txtというファイルを読んで,結果をoutput.txtというファイルに記録しています. つづくlessコマンドはoutput.txtの内容を見るために実行しています (lessの使いかた).
標準入出力のリダイレクション
上の実行例で示したように,標準入力,標準出力をファイルにリダイレクションするには次のように「<」あるいは「>」に続いてファイルを指定します.
$ command < file # commandの標準入力(STDIN)のfileへのリダイレクション $ command > file # commandの標準出力(STDOUT)のfileへのリダイレクション
リダイレクションの効果は一時的なもので,リダイレクションを行ったコマンドのみで有効です. 次のコマンドでは,もとの通り,標準入力はキーボード,標準出力は画面に対応付けられます.
パイプ
さきほどの実行例では,wf.rbが出力する結果をいったんoutput.txtというファイルに格納して,次にそれをlessで見るという2ステップの処理を行っていました. 結果をずっと保存したいのであれば,もちろんファイルに格納しておく必要があります. しかし一時的に結果を調べたいだけで,保存しておく必要がないのであれば,わざわざファイルを作る必要はありません. そのような場合には,次のようにすると,wf.rbが出力する結果をファイルに格納することなく,1ステップでそのまま直接lessに送り込むことができます.
$ ruby wf.rb < input.txt | less
この仕掛けを可能にしているのが,Unixのパイプという仕組みです. 上のコマンドラインの「|」がパイプを表しています. パイプはコマンドをつなぐものです.パイプでコマンドをつなぐと, パイプの前のコマンドの標準出力が,次のコマンドの標準入力へリダイレクションされます. これは,複数(三つ以上も可)のコマンドを標準入出力でつないでデータを順に処理するパイプラインを作る仕組みであると考えると分かりやすいでしょう. まず最初のコマンドにデータが読みこまれ,その処理の結果が次のコマンドの標準入力へ流し込まれる,またそのコマンドの処理の結果がさらに次のコマンドの標準入力へ流し込まれるというようにパイプの中をデータが順に通っていくにつれ各コマンドで順にデータ処理が行われていくわけです.
ここで挙げた例では,まずwf.rbでは, (ファイルinput.txtにリダイレクションされた)標準入力からデータを読み込んで, その結果を標準出力へ書き出します. ところが,この標準出力は,パイプによって, 次のコマンドlessの標準入力へ送り込まれます. lessはコマンドライン引数がなければ,標準入力のデータを処理するようになっています. したがって,この例の場合は,パイプによって送り込まれたwf.rbの出力結果を処理することになるわけです.
Unixではパイプを利用してコマンドを組み合わせることで,実にさまざまな処理を実現することができます.
標準エラー出力
これまでの例題でも見てきた通り, プログラムの実行時に画面にメッセージを出力することがあります. メッセージは,入力を促したり,エラーの発生やプログラムの進行状況などをユーザに伝えるために利用されます.
メッセージはリダイレクションやパイプによって出力するデータとは別々に扱うべきです. データを保存するファイルにメッセージを一緒にリダイレクションしたり, パイプでメッセージを次のコマンドに送ったりしても意味がありません. ところが,データとメッセージをどちらも同じ標準出力によって出力したとすると, これらの区別ができなくなります. リダイレクションするとデータとメッセージがともにファイルに保存され, パイプを利用するとデータとメッセージの両方が次のコマンドへ送られてしまうことになります.
そのため,メッセージを出力するストリームとして,標準出力の他に, 標準エラー出力(standard error)が用意されています. 通常,標準出力,標準エラー出力はともに画面と結びつけられているため, 区別がつきませんが, リダイレクション,あるいはパイプを用いたときは,両者に違いが現われます. リダイレクション,あるいはパイプでは,標準出力の対応付けのみが変更され, 標準エラー出力は,あいかわらず画面と結びつけられたままになります. これによって,データとメッセージの出力先を振り分けることができます. つまり,パイプでデータをやりとりすることを想定して データは標準出力に送って,メッセージは標準エラー出力に送ればよいわけです.
これまでの例題でも,本来メッセージは標準エラー出力に送った方がよいのですが,説明がややこしくなりますので,標準出力を用いていました. なおRubyでメッセージを標準エラー出力へ送るにはSTDERRのメソッドであるprintを起動します.
STDOUT.print "メッセージ\n" # 標準出力へ書き出す print "メッセージ\n" # レシーバを省略した場合(上と同じ) STDERR.print "エラーメッセージ\n" # 標準エラー出力へ書き出す
なおここでは述べませんが,標準エラー出力も必要であればリダイレクションできますし, パイプに送り込むこともできます.
その他の話題
ここでは,例題のプログラムで使っていて今回のトピックには直接関係のない文法事項を紹介します.
ハッシュの利用
例題では「単語」をキーとして「その頻度」を値とする単語頻度表をハッシュで表現しています.
プログラムの2行めでまず空のハッシュを用意しています. これは空の単語頻度表を用意したことを意味しています. つづいて3行めでは,ハッシュに存在しないキーに対する値を参照したときに返す値(デフォルト値)を0に設定しています. これは,これまでに単語頻度表に登録されていない新しい単語が現われたときは,その頻度を0として扱うことを意味しています. なお,このデフォルト値を設定しない場合は,存在しないキーを参照するとnilを返します.
9 dict = {} # 空のハッシュを生成
10 dict.default = 0 # ハッシュのデフォルト値を0にする.
プログラムでは,つづいて,getsで入力した行を処理するにあたって, まず文字列に対するsplitメソッドで, 文字列を空白文字(列)で分解して,単語の配列を得たのち, 配列の各要素(単語)をeachイテレータで処理しています. イテレータでは,9行めで各単語を全て小文字にしてから, 10行めでその単語の頻度を1増やしています. ここで,初めて登場する単語の場合には,上で解説した通り, 3行めの設定にしたがって, 右辺の「dict[word]」が0を返しますので,代入のあとは,頻度が正しく1となります.
12 # 標準入力から1行ずつ読みこむ
13 while line = gets
14 a = line.split # 単語に分解して配列にする
15 a.each do |word| # 各単語wordに対して
16 word = word.downcase # wordの大文字を小文字にする
17 dict[word] = dict[word] + 1 # wordの頻度を1増やす
18 end
19 end
データを全て読み終わって,頻度データを得たあとは,それをソートして結果を表示します.
21 # (ハッシュを配列に変換した上で)単語を頻度の高い順でソートして,
22 # 「単語 頻度」のリストを表示する.
23 dict.to_a.sort { |x,y|
24 -(x[1] <=> y[1])
25 }.each { |word,freq| print word," ",freq,"\n" }
26
ハッシュはソートすることができないため,ここでは, まず単語頻度表のハッシュを配列に変換してから, その配列の要素を頻度の高い順でソートしています. 最後にソートされた配列に関して,eachイテレータでその内容を表示しています.
まずハッシュをto_aメソッドで配列に変換します. 結果として,[キー,値]という配列を要素とした配列が得られます. この例では,[単語,頻度]という配列を要素をもつ配列になります.
次にこの配列の要素を,sortメソッドで,頻度の高い順に並べ替えます. この例のようにsortにブロックが渡された場合, ブロックで定められる評価基準にしたがって要素を比較して,並べ替えを実行します. ブロックパラメタは二つ用意します. たとえば,これらを例のようにx,yとしたとき, ブロックの最後の式の値を次のように定めておきます.
- x,yを比較して,xが前に来るなら,負の値
- x,yを比較して,yが前にくるなら,正の値
- x,yが等しければ,0
sortにブロックが与えられない場合には,配列の要素を「<=>」メソッドで比較します. この例では,頻度の整数値を「<=>」メソッドで比較した結果を逆にして並べ替えています. 通常,整数の「<=>」メソッドでは, 「x <=> y」で,xが小さいときに負の値,大きいときに正の値, xとyが等しければ0を返します. したがって,頻度の高い順に並べ替えるには, 「<=>」メソッドの値の符号を反転する必要があるわけです.
プログラムでは,こうして頻度の高い順にソートされた配列に対して,eachイテレータを起動して, 各要素を表示しています. 配列の要素が,[単語,頻度]という配列であるため, ブロックパラメタを二つ用意して処理しています.