rubyをはじめとするスクリプト言語が良く使われる分野はテキスト 処理です.まずは雰囲気をつかむため,rubyの最初の例題として, テキストの中から正規表現に該当する行を捜し出すコマンド「grep」 を実装してみましょう.
grepは以下のようなコマンドです.
grep pattern file...
fileが省略された時には標準入力から行の検索を行います.
これをrubyで(簡単に)書くとこうなります.
$pat = ARGV.shift while gets print if /#{$pat}/ end
たった4行ですが,これでも立派なプログラムです.rubyはインタ プリタですので,作ったプログラムをコンパイルなどのステップ無 しにいきなり実行できますし,引数で指定したファイルからの読み 込みや,正規表現を使った検索などいろいろな便利な機能が最初か ら組み込まれていますので,プログラムの作成が「お手軽」です.
もし,同じプログラムを例えばCで書けば正規表現の検索部分を除 いても結構な分量になることでしょうし,よほどプログラムに長け た人でない限り,完成までかなりかかりそうです.この「簡単さ」 がrubyの長所のひとつです.
では,ちょっと実行してみましょう.
ruby grep0.rb ruby /usr/dict/words ruby
ちゃんと動作しているようです(良かった,良かった).
それでは,rubyのことが分かるように,このプログラムをもうちょっ と詳しく見てみましょう.
grepのようなファイルから読みだし,1行毎に処理して結果を出力 するようなプログラムは基本的に以下のような構成になります.
while 一行読み込み 読み込んだ行を処理 処理結果出力(もしあれば) end
rubyにはこのような機能は最初から組込みになっています.例えば, 「一行読み込み」にはgets,結果出力にはprintという関数が用意 されています.
先のプログラムもこの形式に従っています,詳しくみてみましょう.
1 $pat = ARGV.shift 2 while gets 3 print if /#{$pat}/ 4 end
1行目はコマンドライン引数からパターンを取り出して,変数$pat に代入しています.rubyのコマンドライン引数はARGVという配列に 入っていますので,その最初の要素を取り出しています. ARGV.shiftというのは,配列の最初の要素を取り除く操作です.
2行目の「while gets」というのは,配列ARGVに入っているコマン ドライン引数で指定されたファイルから1行ずつ取り出すためのあ る種の決まり文句です.ARGVに複数のファイル名が入っている時に は最初のファイルを読み終ると次のファイルの先頭から読みはじめ ます.
3行目でパターンに合う行があれば,printしています./#{$pat}/ という表現は,読み込んで来た行と$patで表される正規表現の比較 を行うという意味です.
`#{$pat}'というのは`#{'から`}'までの間の式を正規表現式 (/.../)の中に埋め込むという意味です.この式の埋め込みは文字 列や正規表現の中で有効です.
4行目の「end」は2行目の「while」と対応するものです.whileに よる繰り返しはここまでであることを示しています.
なんとなく,動作が分かりましたか? プログラムが短いのは良いん ですけど,少し読みにくい感じがしますね.実はrubyは書きやすい ようにいろいろな省略を許しています.そのせいで,こんなに短く 記述できるわけですが,逆に読みにくくなるのは仕方が無いですね. では,省略無しで同じプログラムを書いてみましょう.
1 $pat = ARGV.shift 2 while $_ = gets() 3 if $_ =~ /#{$pat}/ then print $_ end 4 end
あんまり読みやすくなっていないような気もしますが,省略されて いた「$_」という変数が表に出ることで動作が想像しやすくなった かも知れません.
ではこのプログラムを更に詳しく説明しましょう.
1行目は同じですね.
2行目はgetsが関数であることがはっきり分かります.getsは1行読 み込んで来て,変数 $_ に代入します.またファイルの終りが来る と偽を返しますので,whileループが終了します.
3行目はずいぶんみかけが変わっています.まず,ifの形式が変わっ ています.rubyではifは二通りの形式をとります(意味は同じです). ひとつは前のプログラムで現れたような後置形式です.もうひとつ はこのプログラムで見られるような前置形式で,こちらはendで終 ります.ここには出て来ませんが,前置形式にはelse節という,条 件が成立しなかった時に実行する文を指定できます.後置形式では これはできません.
次にifの条件の部分(ifとthenの間)も変わっています.条件部分に 現れた正規表現式は,変数 $_ との比較の省略と見なされます.今 回のプログラムはここを省略せずに書いています.=~ は文字列と 正規表現を比較する演算子です.
最後にprintに引数が指定されています.printの省略された時には 変数 $_ の値を出力するようになっています.
4行目も前と同じですね.
では,このプログラムの実行速度を見てみましょう.
% time ruby /tmp/grep0.rb ruby /usr/dict/words ruby 5.89user 0.25system 0:06.17elapsed
この程度の作業にi486DX4 75MHzで6秒以上ってのはずいぶん遅い気 がしますね.普通のgrepと比較してみましょう.
% time grep ruby /usr/dict/words 0.03user 0.06system 0:00.13elapsed
うーん,やはり遅いなあ.rubyが遅いのはある程度仕方の無い部分 もあります.rubyはインタプリタ言語ですから,コンパイル型の言 語で記述されたgrepのような専用プログラムと比較するとどうして も遅くなります.とはいえ,いくらなんでも遅すぎる気がするので, ちょっと高速化してみましょう.
以下のプログラムが高速化したgrep1.rbです.
1 $pat = /#{ARGV.shift}/ 2 while gets 3 print if $_ =~ $pat 4 end
1行目と3行目が変わったところです.つまり,正規表現を毎回生成 するのをやめて(//はデフォルトでは毎回正規表現オブジェクトを 生成します),変数に代入した正規表現を再利用するようにしまし た.これだけでずいぶん高速になります. ifの条件式は正規表現式でなければ,変数$_との自動比較は行われ ませんから,今度は必ず`=~'を使って比較しなければなりません.
% time ruby /tmp/grep1.rb ruby /usr/dict/words ruby 2.26user 0.12system 0:02.39elapsed
大体実行時間が半分になりました.それでもまだまだgrepよりも遅 いですけどね.
では,この「遅い」grepになんの意味があるんでしょう? 確かにす ごく簡単に作れましたけど,普通のgrepより何倍も遅いのでは使い 道が無いような気もします.
このプログラムの存在意義はこの4行がプログラムである点です. つまり,その気になれば,簡単に機能を拡張できるわけです.Cで 記述された専用プログラムであるgrepではこうはいきません.せい ぜいオプションで機能を若干変更できる程度です.
例として,マッチした部分を反転するgrepを作ってみましょう.
1 st = "\033[7m" # 反転開始エスケープシーケンス 2 en = "\033[m" # 反転終了エスケープシーケンス 3 4 $pat = /#{ARGV.shift}/ 5 while gets 6 if $_ =~ $pat 7 # マッチした部分の前後にエスケープシーケンスを埋め込む 8 gsub! $pat, "#{st}\\&#{en}" 9 print 10 end 11 end
ちょっとだけ複雑になりましたが,これだけでマッチした部分を反 転させることができるようになりました.自分の望む機能を簡単に 実現するための道具としてrubyはもってこいです.
少々実行速度が遅くても,さっとプログラムして,さっと実行させ て結果を得れば,最終的にはずっと速く結果を得ることができるこ とは多々あると思います.そういう場面こそがrubyをはじめとする スクリプト言語のもっとも有効な場面です.実行速度が本当に問題 になるようなツールが必要とされる場合は,それなりに時間をかけ て高速なプログラムを作れば良いわけです.
では,ここで学んだことをまとめておきましょう.
matz@caelum.co.jp Last modified: Wed Jan 28 12:16:41 JST 1998