後藤謙太郎 <URL:http://www.notwork.org/~gotoken/mag/cmagazine/>
註: この文書は『C MAGAZINE』2000年8月号 に掲載された記事の元となるものに手を加えたものです.
記事中のプログラムを一つずつファイルにしたものもあります(→list)。
中田さんのおかげでNetscapeでも読めるようになりましたが,まだちょっとカッコ悪いです。「CSSの書き方がなっとらん、こう書け」というお便りは歓迎します。
間違いを見つけたら,gotoken@notwork.org宛に御連絡くださると喜びます。
Copyright(c) 2000 by GOTO Kentaro. All rights reserved.
はじめに,メソッド名の表記法,変数,文字列, レシピ集(ファイルの各行に何かする,一気に読んでから処理する, 文字数を数える,破壊的置換とチェーン,「日本語 改行 日本語」問題),おわりに,参考資料,謝辞
いわゆるRuby本[MI99]が出版されて以来,Rubyに対する関心はますます高まっているようです。この3月にはUsenetのニューズグループ comp.lang.ruby も開設され海外での評判も高まって来ました。しかし Rubyの紹介の多くはRubyがどんなモノかという極めて入門的,もしくは機能の一覧的な内容のモノが多く,そうかといってRuby本はいきなり本格的なので,もうちょっと中間的な解説が欲しいという話もちょくちょく耳にします。実際は,多くのWebサイトでさまざまなテクニックやチュートリアルを目にすることが出来るのですが,忙しくてそんなにあっちこっちぐるぐるみて回れないとか,定点観測したいという人のためにこの連載が企画されました。毎月読みきりでつれづれなるままに話題を提供していく予定ですので気が向いたときにおつき合いくださいね。
さて,今回は文字列にまつわるアレコレを紹介します。文字列といっても実際はファイルも扱います。しかし,今回は第1回でもあるので,その前にふたつの準備をしておきましょう。一つは,メソッド名の表記法,それから,わりと誤解があるようなのでRubyの変数についての説明を与えます。なお,当連載では Ruby 1.6 (次期安定版)がリリースされるまでは,Ruby 1.4.x の最新版を仮定します。自分の使っているRubyのバージョンは,次のようにして調べることが出来ます。
% ruby -v ruby 1.4.4 (2000-04-14) [i386-freebsdelf4.0]
例えば文字列クラス String のオブジェクトには each というメソッドがありますが,同名の each は多くのクラスが持ちそれぞれ独自性を発揮しています。そこで文書中で String の each について言及したい場合には,String#each と記します。一般に,ある特定のクラス Foo のメソッド bar を次のように綴ります。
Foo#bar
ただしこの記法はあくまで人が読むためだけのものであり,Ruby のプログラム中に書いても,# 以降がコメントとして無視されるだけです。一方, String#each を意味するつもりで, String.each と書いている人をしばしば見掛けますが,これだとRubyプログラム中では「String というオブジェクトに対する each メソッドの呼び出し」という全く異なる意味を持つためあまりよい書き方ではありません。これはRubyではクラス自体も一つのオブジェクトであるという性質から来るもので,このようなオブジェクトとしてのクラスが持つ特異メソッドは特にクラス
メソッドと呼ばれています。
当連載でもこの記法を使いますが,これはRubyのメーリングリスト等でも広く使われている習慣です。きちんと区別すると質問内容などをより円滑に伝えることが出来るので覚えておくとお得です。
Ruby本の43頁にも載っているのですが,Rubyにおける変数はちょうどオブジェクトにつける名札に相当します。いいかえれば,変数への「代入」とはオブジェクトに名札をつけることにほかなりません。また変数から変数への「代入」はそのオブジェクトに別の名札をつけることになります。
リスト1で comic_artist と author_of_yasha は同一のオブジェクトにつけられた名札と考えることが出来ます。もし見た目が同じでも違うオブジェクトはやっぱり違うものです。つまり tv_director の指
すオブジェクトも見た目は"吉田秋生"ですが,直後の
String#replace による変更とその結果を出力から違いが分かります*1。
-- リスト1 変数はオブジェクトについた名札 -- comic_artist = "吉田秋生" author_of_yasha = comic_artist tv_director = "吉田秋生" comic_artist.replace "よしだあきみ" p comic_artist #=> "よしだあきみ" p author_of_yasha #=> "よしだあきみ" p tv_director #=> "吉田秋生"
上の例はとってつけたようないわば説明のための例ですが,実際 gsub
と gsub! の違いのような,オブジェクトが変化するかどうかという場合にはこの仕組みを知らないとプログラムの動きを追うことが出来ないので,ぜひ覚えておきましょう。また,これを知っていればどのオブジェクトに何をさせているかといった視点でRubyプログラムを読むことができ,効率的なRuby
流メソッドの活用法を盗めるようにもなります。
Rubyで文字列を表すクラスはご存知のように String ですが,より正確にいえば String はCの文字列のように,文字列というよりはバイトの並びです。その証拠に,"ガンバ"[0]のように,文字列"ガンバ"
の0番目の要素を調べると,EUCなら 245 という整数が返ってきます。整数を1
文字からなる文字列に変換するには Fixnum#chr を使います。逆に
?a というリテラルは文字 a のASCIIコードを表す整数です。
"abc"[-1] #=> 99 "abc"[-1].chr #=> "c" ?c #=> 99
Stringも,次の文脈では文字の並びとみなすことが出来ます。
require "jcode" 後ただし,いずれの場合もJISコードの日本語文字列を文字の並びとしては解釈できません。いくつか補足しておきまます。
日本語のリテラルを扱うためには,Rubyインタプリタにその文字コードを読む心構えが出来ていなければなりません。この文字コードはまずコンパイル時に指定でき,"utf8","euc","sjis","none" (指定無し)のなかから選ぶことが出来ます。自分の使っているRubyのコンパイル時指定を知るには,次のようにするのが手っ取り早いでしょう。
% ruby -e 'p $KCODE'
また,実行時に -K オプションを使って指定することも出来ます。例えば,-Ku でUTF8です。プログラム中で$KCODE = "u" とすると以下で触れる正規表現のデフォルトの解釈などが変化しますが,リテラルの解釈はプログラムを読む前に指定する必要があるのでこの方法ではうまくいきません。実はプログラムの1行目に次のように書くことでプログラムからも指定できます。
#! /usr/bin/env ruby -Ku
これはUNIXのスクリプトにはつきものの指定ですが,Rubyインタプリタはこの行を解釈してから残りを読むので,UNIXのスクリプト実行機構を使ってない場合でもこれでうまくいくのです。他のオプションも同様ですが,特に問題になるのはこの -K だけでしょう。ただしWebなどで広く公開したい場合は ASCII 以外の文字をリテラルやコメントに書かない方がよいとは思います。
正規表現を特定の文字コードのもとで解釈するには /.../e のように正規表現リテラルの最後にコードの先頭の一文字をオプションとして与えます。
リスト2はこの違いを見るための小さな実験です。
-- リスト2 正規表現の文字コードを制御する --
require "nkf"
ji = NKF.nkf("-e", "じ")
p ji.scan /^./e #=> ["じ"]
p ji.scan /^./n #=> ["\244"]
ここではマッチの対象となる文字列 ji を確実にEUCにするためにnkfモジュールを使っています。このように,正規表現が文字コードに反応するといっても文字列はあくまでバイト列なので,JISをEUCとしてマッチするような暗黙の変換はありません。なお,正規表現の文字コードのデフォルトは
$KCODE で決まります。
さて,長々と説明が続いたので,ここいらでいくつかプログラムを見ていくことにします。
テキストファイルの各行に何かをするというのはとても基本的な作業です。例えば,次のような慣用句があります。
while gets .... end
この慣用句は1行読む gets が結果を $_ にもセットするという性質を使ったもので,.... でその行を参照するにはこの $_ を使うわけです。しかし,暗黙の代入にはなんとなく分かりにくさがあるので次のほうが少し読みやすいように思います*2。
while line = gets .... end
この gets という「関数」は ARGF という定数で参照される仮想ファイルを読むので次の方がより明示的で分かりやすいでしょう。
ARGF.each_line do |line| .... end
なお,ARGFはIO相当なので,each_line を each と書いても同等です。
リスト3は引数で与えられたファイルの各行に対して,"<",">", "&" の3文字をそれぞれ "<",">","&" で置き換えます。ただし,ファイルの文字コードを自動判別して,出力はデフォルトでJISコード,さもなくば -O オプションの引数で出力の文字コードを指定します。
-- リスト3 HTML特殊文字のエスケープ --
#! /usr/bin/env ruby
require "getopts"
require "nkf"
class String
def nkf(opt) # nkf をStringのメソッドに
NKF.nkf(opt, self)
end
end
HTML_SPECIAL = { # 実体参照表
"<" => "<",
">" => ">",
"&" => "&"
}
getopts("i", "O:j")
opt = "-" + $OPT_O[0,1]
$-i = "" if $OPT_i
ARGF.each do |line|
print line.nkf("-e").gsub(/<|>|&/){
HTML_SPECIAL[$&]
}.nkf(opt)
end
オプションとして -i を与えれば,それぞれのファイルが上書きすることも出来ます。本題とは関係ないですが String#nkf をNKF.nkf
で定義しています。これによってprintの引数が「line を
nkf("-e") して,gsub して再度 nkf したもの」と読めます。
また gsub の代入値をマッチした文字列に応じて変えるには,リスト3
のようにブロックつきで呼びます。マッチしたパターンやパターン中のカッコに対応して,第2引数中で "\\&" や "\\1" などを使うことは出来ますがこの例のようにマッチの結果を別のメソッドに渡して動的に置換する値を作ることは出来ません。もっとも,この場合は each の中身を,リ
スト4のように置換パターンを第2引数にハードコードしても良いでしょう。しかしこの問題では最初に "&" を置換するように注意が必要です。
"&" より先に "<" を "<" に置換してしまうと,
"&" をさらに置換することになり,結局 "&lt;" となってしまいます。
-- リスト4 HTML特殊文字のエスケープ(gsub!を使って) --
ARGF.each do |line|
line = line.nkf("-e")
line.gsub!(/&/, "&")
line.gsub!(/</, "<")
line.gsub!(/>/, ">")
print line.nkf(opt)
end
なお,1文字ごとに単に固定された1文字に置換をするにはString#trを使うに越したことはありません。たとえば,標準入力に対して rot 13/47 という簡単な暗号化を施すフィルタはリスト5のようになります
[ruby-list:6517]。
-- リスト5 rot13.rb: 簡単な暗号 Rot 13/47 --
#!/usr/bin/env ruby
# rot 13/47
class String
def rot1347
tr "A-Za-z\M-!-\M-~", "N-ZA-Mn-za-m\M-P-\M-~\M-!-\M-O"
end
end
STDIN.each do |line| print line.rot1347 end
ファイルの行数を数えるという問題はときどき出て来ます。これまでの文脈にもっとも素直な解は,各行読むごとにカウンタをインクリメントすることでしょう(リスト6)。
-- リスト6 wc0.rb: 行数を数える -- #! /usr/bin/env ruby nr = 0 ARGF.each do nr += 1 end puts nr
しかし,本当に行数を数えるだけでよいなら次の巧妙な方法がもっとも速いようです[ruby-list:14491]。
ARGF.read.delete("^\n").size
つまり,ARGFを一行ずつ処理するのではなく,一気に読んでしまい,改行文字 "\n" の個数を数えるわけです。ただし,リスト6とこの方法は大きな違いが一つだけあります。すなわち,最後の行が "\n" で終っていなければ後者は(wc(1)のように)それを行として数えません。もしリ
スト6 と同じ数え方にするなら次の通りです。
ARGF.read.scan(/$/).size
上の例はコストを理由にファイルの内容を一気に読んでいますが,一般に複数行にまたがる処理や2パス以上を必要とする処理の場合も一気読みはもちろん有効です。HTMLのタグとそれ以外に分解するにはリスト7のようにします。
-- リスト7 HTMLをタグとテキストとコメントに分解 --
compat = '<!--.+?-->' # コメント
tagpat = '<(?:\s|[^>])+>' # タグ
txtpat = '(?:[^<>]|\s)+' # テキスト
pat = /#{compat}|#{tagpat}|#{txtpat}/p
token = html.scan(pat)
ここで html はHTMLファイル全体を読みとった文字列,また正規表現のオプション //p は改行を通常文字とみなすためのものです。この
pat のような複雑な正規表現を書く際はこのように部品を組み立てるように書くと把握しやすくなります。またこの場合,各部品を表す文字列リテラルはバックスラッシュを多用するのでダブルクオートでなく,シングルクオートで囲むのが良いでしょう。
文字列に含まれる文字の数を数えるにはいろいろな方法がありますが,日本語の文字を含む場合にも使えるもっともてっとり早い方法は jcode を使うことです(リスト8)。
-- リスト8 jcode.rbのjlength -- require "jcode" "7文字なのです".jlength #=> 7
Ruby 1.4.4 では String#jlength の定義はリスト9のようになっています。このなかで現れる split(//) は一文字ごとの配列に分解する方法の一つです。
-- リスト9 jlength の定義(Ruby 1.4.x のもの) --
class String
def jlength
self.split(//).length
end
end
しかしその後 gsub を使った方が速いという理由から Ruby 1.5.x では
リスト10 のようになっています。
-- リスト10 jlength の定義(Ruby 1.5.x のもの) --
class String
def jlength
self.gsub(/[^\Wa-zA-Z_\d]/, ' ').length
end
end
一文字ごとの分解を使うと文字ごとにアクセスすることが出来るので,日本語を扱う場合は各文字に関するイテレータ each_char は便利かも知れません(リスト11)。Ruby 1.5.x に添付の jcode.rb には含まれています。
リスト11に出てくる scan(/./) も一文字ごとの配列に分解する方法の一つです。
-- リスト11 String#each_char --
class String
def each_char
if iterator?
scan(/./) do |x|
yield x
end
else
scan(/./)
end
end
end
イテレータについて不慣れな方もいるかも知れないので,使い方を示しておきましょう。リスト12は文字の出現頻度を求めるものです。
-- リスト12 jchc.rb: 文字の出現頻度を数える --
#! /usr/bin/env ruby
require "nkf"
class String
def each_char
if iterator?
scan(/./e) do |x|
yield x
end
else
scan(/./e)
end
end
end
TOTAL = "# total" # 総計表示のプレフィックス
freq = {}
sum = Hash.new(0) # デフォルト値0のハッシュ
maxfn = (ARGV + TOTAL).collect{|fn| fn.size}.max
ARGV.each do |fn|
# 各引数(ファイル名)fnについての繰り返し
freq[fn] = Hash.new(0) # ファイル fn での頻度
NKF::nkf("-e", open(fn).read || "").each_char do |c|
# ファイル fn の各文字 c についての繰り返し
# 「|| "」 は入力が空ファイル対策
freq[fn][c] += 1
sum[c] += 1
end
freq[fn].sort{|i,j| j[1] <=> i[1]}.each do |c,v|
# 頻度の高い順に結果表示
printf "%s: %4s %5d\n",
fn.ljust(maxfn), c.inspect, v
end
end
if ARGV.size > 1
# 複数のファイルからなるときは総計も表示
sum.sort{|i,j| j[1] <=> i[1]}.each do |c,v|
printf "%s: %4s %5d\n",
TOTAL.ljust(maxfn), c.inspect, v
end
end
ところで each_char を String の組み込みメソッドにしてはどうかという提案がときどき上がりますが,将来実現するであろうRubyのM17Nを考慮して採用されていません[ruby-list:22818]。RubyとM17N/I18N については発展途上中のサーベイ[Tak] がありますので,関心のある方には一読をお勧めします。文字とはなにかというのは難しい問題です。
これまでもたびたび出ましたが文字列の置換には []= を使った代入,
tr,tr! および sub の一族を場合に応じて使いわけます。
sub の一族とはsub,gsub およびそれらの破壊的な版である sub!,gsub! のことです。gsub と gsub! はレシーバが書き変わるかどうかが違うわけですが,返す値も違います。すなわち,
gsub! は書き換えるべきものが何もなかったとき,nil を返します。このことはメソッドをその後に続けることが出来ないことを意味します。例えば,文字列line のあるパターン pat にマッチする部分を全て別の文字列 str で置き換えて,それから全体を大文字に変換するという作業を考えてみます。すぐに思い付く次の例は危険です。
line.gsub!(pat, str).upcase! # 危険
なぜなら,gsub! が nil を返した場合は,nil が
upcase を理解できないために例外が発生するからです。実際は次のいずれかのように書く必要があります。
# gsub! の場合 line.gsub!(pat, str) line.upcase! # gsub の場合 line = line.gsub(pat, str).upcase
"!" で終る名前を持つメソッドはいずれも同様なのですが,これらは実行効率と書きやすさが競合の関係にあるわけです。つまり,gsub! は新たにオブジェクトを生成しないので,やや高速ですが,いっぽう
gsub を使えば,一行で書いてしまうことが出来る代わりにオブジェクト生成を引き起こします。どちらが良いかは場合に応じて判断する必要があるでしょう。この例の場合はチェーンしない方が分かりやすいように思います。あんまり長いメソッドチェーンは分かりにくい場合や想像以上の効率の悪さの原因となることもあるので気をつけた方がよいのでしょうが,一時的な変数を使わずにメソッドチェーンで書けるのはRuby の快楽のひとつですよね。
最後に少し実用的な例として,HTMLファイル中から日本語と日本語の間の改行を取り除くという問題を考えます。多くの HTML ブラウザは日本語の文字間の改行を無視せずに空白として扱います。それ自体は正しいのですが,表示の際にこの空白を空白として残すために,変な所に空白が入って見栄えが悪くなっているものをしばしば見掛けます。しかし編集する際に1段落を長い1行で書くのは結構めんどくさいという問題もあるので,そういうことは自動化しようというわけです。
さてリスト13がその解の一例です。String のメソッドとして定義した
kill_nlsp_in_jchars が中心的な役割を果たします。流れとしては,まずリスト7の技法でHTMLをタグとそれ以外に分解し,token という配列に代入したあと,token の各要素のうちPRE要素内とタグの両方を除く全ての部分に対し kill_nlsp_in_jchars を破壊的に適用し,最後に
join して print しています。
String#kill_nlsp_in_jchars では,一旦EUCに変換してから改行と行頭の空白を削除して,もとの文字コードに戻してから返しています。また,ここではgsub! が nil を返すことを積極的に利用して,繰り返し
gsub! を適用しています。これは,面倒なパターンを書くかわりにしばしば用いられる技法の一つです。また破壊版のメソッドを定義するには,
String#replace を使っています。
-- リスト13 jhtmljoin.rb: HTMLから日本語,改行,日本語の改行を消す --
#! /usr/bin/env ruby -Ke
require "kconv"
class String
def kill_nlsp_in_jchars
code = Kconv.guess(self)
if code == Kconv::UNKNOWN
self # コードが不明ならそのまま返す
else
str = Kconv.toeuc(self) # 一旦EUCに
# 日本語EUCの日本語部分
euc = '[\xa1-\xfe][\xa1-\xfe]'
pat = /(#{euc})\s*\n\s*(#{euc})/n
begin
modified = str.gsub!(pat){$1+$2}
end while modified
Kconv.kconv(str, code) # 元のコードに
end
end
def kill_nlsp_in_jchars! # 破壊的な版
replace kill_nlsp_in_jchars
end
end
compat = '<!--.+?-->' # コメント
tagpat = '<(?:\s|[^>])+>' # タグ
txtpat = '(?:[^<>]|\s)+' # テキスト
pat = /#{compat}|#{tagpat}|#{txtpat}/p
token = (ARGF.read || "").scan(pat)
pre = false # PRE要素内か
token.each do |tok|
if tok =~ /<\s*pre\b/pi
pre = true
elsif pre and /<\s*\/\s*pre\b/pi =~ tok
pre = false
end
if !pre and /\A</ !~ tok
tok.kill_nlsp_in_jchars!
end
end
print token.join
今回はHTMLをおもな例題にとり文字列についてのいろいろな話題を取り上げました。この他にもRubyホームページ<URL:http://www.ruby-lang.org>の FAQやMLトピックスにはさまざまなtipsが載っていますので一度は御覧になることをお勧めします。またこの記事に登場したプログラムリストは <URL:http://www.notwork.org/~gotoken/mag/cmagazine/> で公開していますので,必要に応じて御活用ください。
まつもとゆきひろ,石塚圭樹,『オブジェクト指向スクリプト言語Ruby』,アスキー出版局 (1999)
TAKAHASHI Masayoshi, "M17N/I18N for Ruby", <URL:http://www.inac.co.jp/~maki/ruby/ruby-i18n.html>
WATANABE Hirofumi,"Re: ROT13/47", <URL:http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/6517>
WATANABE Hirofumi,"Re: count lines", <URL:http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/14491>
Yukihiro Matsumoto, "Re: String#each_byte", <URL:http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/22818>
Rubyの作者であるまつもとゆきひろさんをはじめとするRubyコミュニティのみなさんに感謝します。blade を立ち上げ,維持されている原さんには特に感謝します。bladeなしではこの原稿は書けませんでした。また,Rubyに関する連載の機会を与えて下さり,毎号でお世話になっている C MAGAZINE 編集部の有馬さんにも感謝します。
Rubyがもっともっと広まりますように :-)
Rubyどうでしょう
著者: 後藤謙太郎
御意見,御感想,御批判の宛先: gotoken@notwork.org
*1
ちなみに吉田秋生さんといってもTBS系のドラマ『恋の神様』の演出もされたのは "よしだあきお" さんで,テレビ朝日系のドラマ『YASHA』の原作者 "よしだあきみ" さんとは別の方というのは割かし有名な話,と思ったのですが心配なので補足 ^^;;
*2もっとも組み込み関数のなかには
$_を特別扱いするものがありスクリプト言語としては使いこなせると便利かも知れません