極めよRuby道

第1回 変数とテキスト処理

後藤謙太郎 <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 は多くのクラスが持ちそれぞれ独自性を発揮しています。そこで文書中で Stringeach について言及したい場合には,String#each と記します。一般に,ある特定のクラス Foo のメソッド bar を次のように綴ります。

Foo#bar

ただしこの記法はあくまで人が読むためだけのものであり,Ruby のプログラム中に書いても,# 以降がコメントとして無視されるだけです。一方, String#each を意味するつもりで, String.each と書いている人をしばしば見掛けますが,これだとRubyプログラム中では「String というオブジェクトに対する each メソッドの呼び出し」という全く異なる意味を持つためあまりよい書き方ではありません。これはRubyではクラス自体も一つのオブジェクトであるという性質から来るもので,このようなオブジェクトとしてのクラスが持つ特異メソッドは特にクラス メソッドと呼ばれています。

当連載でもこの記法を使いますが,これはRubyのメーリングリスト等でも広く使われている習慣です。きちんと区別すると質問内容などをより円滑に伝えることが出来るので覚えておくとお得です。

変数

Ruby本の43頁にも載っているのですが,Rubyにおける変数はちょうどオブジェクトにつける名札に相当します。いいかえれば,変数への「代入」とはオブジェクトに名札をつけることにほかなりません。また変数から変数への「代入」はそのオブジェクトに別の名札をつけることになります。

リスト1comic_artistauthor_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      #=> "吉田秋生"

上の例はとってつけたようないわば説明のための例ですが,実際 gsubgsub! の違いのような,オブジェクトが変化するかどうかという場合にはこの仕組みを知らないとプログラムの動きを追うことが出来ないので,ぜひ覚えておきましょう。また,これを知っていればどのオブジェクトに何をさせているかといった視点で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も,次の文脈では文字の並びとみなすことが出来ます。

ただし,いずれの場合も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_lineeach と書いても同等です。

リスト3は引数で与えられたファイルの各行に対して,"<",">", "&" の3文字をそれぞれ "&lt;","&gt;","&amp;" で置き換えます。ただし,ファイルの文字コードを自動判別して,出力はデフォルトで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 = {  # 実体参照表
     "<" => "&lt;",
     ">" => "&gt;",
     "&" => "&amp;"
   }

   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#nkfNKF.nkf で定義しています。これによってprintの引数が「linenkf("-e") して,gsub して再度 nkf したもの」と読めます。

また gsub の代入値をマッチした文字列に応じて変えるには,リスト3 のようにブロックつきで呼びます。マッチしたパターンやパターン中のカッコに対応して,第2引数中で "\\&""\\1" などを使うことは出来ますがこの例のようにマッチの結果を別のメソッドに渡して動的に置換する値を作ることは出来ません。もっとも,この場合は each の中身を,リ スト4のように置換パターンを第2引数にハードコードしても良いでしょう。しかしこの問題では最初に "&" を置換するように注意が必要です。 "&" より先に "<" を "&lt;" に置換してしまうと, "&" をさらに置換することになり,結局 "&amp;lt;" となってしまいます。

-- リスト4  HTML特殊文字のエスケープ(gsub!を使って) --

   ARGF.each do |line|
     line = line.nkf("-e")
     line.gsub!(/&/, "&amp;")
     line.gsub!(/</, "&lt;")
     line.gsub!(/>/, "&gt;")
     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_charString の組み込みメソッドにしてはどうかという提案がときどき上がりますが,将来実現するであろうRubyのM17Nを考慮して採用されていません[ruby-list:22818]。RubyとM17N/I18N については発展途上中のサーベイ[Tak] がありますので,関心のある方には一読をお勧めします。文字とはなにかというのは難しい問題です。

破壊的置換とチェーン

これまでもたびたび出ましたが文字列の置換には []= を使った代入, trtr! および sub の一族を場合に応じて使いわけます。 sub の一族とはsubgsub およびそれらの破壊的な版である sub!gsub! のことです。gsubgsub! はレシーバが書き変わるかどうかが違うわけですが,返す値も違います。すなわち, gsub! は書き換えるべきものが何もなかったとき,nil を返します。このことはメソッドをその後に続けることが出来ないことを意味します。例えば,文字列line のあるパターン pat にマッチする部分を全て別の文字列 str で置き換えて,それから全体を大文字に変換するという作業を考えてみます。すぐに思い付く次の例は危険です。

line.gsub!(pat, str).upcase!  # 危険

なぜなら,gsub!nil を返した場合は,nilupcase を理解できないために例外が発生するからです。実際は次のいずれかのように書く必要があります。

# 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/> で公開していますので,必要に応じて御活用ください。

参考資料

[MI99]

まつもとゆきひろ,石塚圭樹,『オブジェクト指向スクリプト言語Ruby』,アスキー出版局 (1999)

[Tak]

TAKAHASHI Masayoshi, "M17N/I18N for Ruby", <URL:http://www.inac.co.jp/~maki/ruby/ruby-i18n.html>

[ruby-list:6517]

WATANABE Hirofumi,"Re: ROT13/47", <URL:http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/6517>

[ruby-list:14491]

WATANABE Hirofumi,"Re: count lines", <URL:http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/14491>

[ruby-list:22818]

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] Rubyどうでしょう
著者: 後藤謙太郎
御意見,御感想,御批判の宛先: gotoken@notwork.org


*1 ちなみに吉田秋生さんといってもTBS系のドラマ『恋の神様』の演出もされたのは "よしだあきお" さんで,テレビ朝日系のドラマ『YASHA』の原作者 "よしだあきみ" さんとは別の方というのは割かし有名な話,と思ったのですが心配なので補足 ^^;;
*2もっとも組み込み関数のなかには $_を特別扱いするものがありスクリプト言語としては使いこなせると便利かも知れません