Perl プログラミング □ はじめに 以前、日本語の文字コードを変換するためのライブラリを紹介しま した。Perl で日本語を処理する機会はあまり多くありませんが、 この程度の機能でも必要になった時には結構役に立つものです。 今月はそれに関係があるようでもあり、ないようでもあるローマ字 仮名変換のためのライブラリを紹介しましょう。ローマ字仮名変換 と言えば日本語入力のインタフェースでみなさんお馴染みだと思い ます。 いずれ Perl で仮名漢字変換のプログラムを作ろうなどという大そ れたことを考えているわけではなく、ちょっとした必要性があって 作ってみたものです。日常的な Perl プログラムでこのような機能 が必要になるかどうかわかりませんが、あっても別に悪くはないで しょう。 それと、今回のプログラムはちょっと変わった作り方をしてありま す。このスタイルが応用できるとは限りませんが、こんなプログラ ミングの仕方もあるんだということを知ってもらえると、いくらか でも読者の皆さんのお役に立つのではないかと考えています。 □ ローマ字仮名変換ライブラリ ライブラリの名前は romkan.pl で、ローマ字仮名変換を実現する サブルーチン名は &romkan です。仕様は簡単で、 aiueo というローマ字文字列を あいうえお という仮名の文字列に変換します。たとえば、 print &romkan('aiueo'); というプログラムを実行すると、 あいうえお という出力が得られるようにします。 内部コードは EUC を使うことにします。しかし別のコードのデー タを欲しいこともあるので、2番目の引数で出力も字列のコード を指定できることにしましょう。 jis: JIS コード euc: EUC sjis: Shift-JIS の3種類を選択できるようにします。デフォルトは euc なので、 2 番目の引数がないか、あるいは undef の場合には EUC を返しま す。 また、平仮名ではなく片仮名に変換したいこともあります。ですか ら、3番目の引数で平仮名に変換するか片仮名に変換するかを指定 できるようにしましょう。3番目の引数が真の値を持つ時には片仮 名に変換するということにします。平仮名でよければ何も指定しな ければいいわけです。 まとめると &romkan の呼出形式は次のようになります。 &romkan(STRING, CODE, KATAKANA) CODE で出力文字列のコードを指定し、KATAKANA が指定指定してあ る場合には平仮名ではなく片仮名を返します。 STRING で与えられる文字列は、必ずしも仮名に変換できるとは限 りませんから、もし変換できなかった時には undef を返します。 文字列の一部だけが変換できるということもありますが、そのよう な場合でも undef を返すことにします。 □ romkan.pl の応用 romkan.pl を使ってどのようなプログラムを作ることができるでしょ うか。まず思いつくのはローマ字で書かれた文章を仮名に変換する ことです。たとえば次のようなプログラムでローマ字の文章の中で 変換できる部分だけを仮名に変えることができます。 while (<>) { s/\w+/&romkan($&)||$&/ge; } "\w+" はでアルファベットと数字が1つ以上連続する部分にマッチ し、それを "&romkan($&) || $&" という式を評価した結果に変換 します。$& はマッチした文字列であり、それを &romkan に与えた 結果が真だった場合にはその値、偽の場合には元の文字列のままに になるわけです。 これではアルファベットと数字の連続する部分だけを仮名に変換す るので、たとえば "ro-ma" という文字列は「ロ-マ」と変換されま す。これを「ローマ」と変換したければマッチするパターンを /[\w\-]+/ のようにすればよいでしょう。'-' は 'ー' と変換することにしま す。 筆者はいつも Nemacs の中で EGG を使って日本語入力をしている ので、たまに普通のコマンドに対して日本語を入力する必要が生じ ると、とたんに慌ててしまいます。こんな時に仮名だけでもいいか らローマ字で入力できるようになっていると助かります。 □ romkan.pl の実装 まずはローマ字をどのように仮名に変換するかを定義することから はじめます。$romkan_table という変数にローマ字とそれに対する 仮名を空白で区切って入れておきます。先頭のの部分を取り出すと 次のようになっています。 "a あ i い u う e え o お" このファイルは EUC で書かれているとは限らないので、まず EUC に変換します。これには fj.lang.perl に投稿された jcode.pl を 利用します。そのためにプログラムの最初に、 require('jcode.pl'); という文が入っています。 そして $romkan_table を空白で split した結果を連想配列 %romkan に代入します。こうすることで $romkan{'a'} の値を「あ」、 $romkan{'i'} の値を「い」という風に、ローマ字をキーとして仮 名を値とする連想配列を一度に初期化することができるわけです。 次は 'tte' などのように小さな「っ」を処理するための配列要素 を設定します。'tt', 'kk' などの値を $romkan{'xtsu'} の値、つ まり「っ」に初期化します。最後に 0 から 9 までの数字はそれ自 身の値をセットします。 さて、いよいよサブルーチン &romkan の定義ですが、これは直接 定義するのではなく、サブルーチンを定義する文字列を作成してか らそれを eval する方法を取ります。与えられた文字列と %romkan のキーとを1つ1つ比較する処理が必要であり、それにはキーの数 だけのパターンマッチを繰り返した方が速度的に有利だからです。 &romkan の処理の概要は次のようなものです。 sub main'romkan { local($_, $code, $katakana) = @_; local($kana) = ''; while (length) { # ----- ここから next if s/^a//i; next if s/^i//i; ... # ----- ここまでは自動生成 next if s/^([tk...])\1/\1/i; last; } continue { $kana .= $romkan{"\L$&"}; } return undef if length; $kana =~ s/\244(.)/\245$1/g if $katakana; &jcode'convert(*kana, $code, $pcode) if $code && $code ne $pcode; $kana; } この中の next if s/^a//i; next if s/^i//i; ... という部分を自動的に作成します。この中のどれかにマッチすれば、 マッチした部分を文字列の先頭から取り除き continue 文が実行さ れます。continue 文の中では $kana .= $romkan{"\L$&"}; のようにして $kana という変数に変換した結果が蓄えられていき ます。 どのパターンにもマッチしなければ最後の last によってループを 終了します。ループを抜けた後でまだ $_ に変換するための文字列 が残っていたとすれば、途中で変換に失敗したということですから、 すぐに undef を返します。 3番目の引数である $katakana がセットされていたら、変換した文 字列を片仮名に変換しなくてはなりません。なぜなら %romkan の 値はすべて平仮名で入っているからです。これは、 $kana =~ s/\244(.)/\245$1/g; という文で行います。EUC では平仮名の1バイト目は \244 で、そ れを \245 に変更すると対応する片仮名に変更することができるの です。この処理を行うために内部コードを EUC にしていると言っ てもよいでしょう。 最後に指定された出力コードが EUC でなかったら、そのコードに コード変換します。 お気付きだと思いますが、ここで eval したプログラムの中には日 本語文字はまったく出てきません。ですから、日本語化されていな い Perl でも問題なく処理できるわけです。日本語文字は $romkan_table の内容としてしか出てこないので、単なるデータと して処理されます。 □ アプリケーション romkan.pl romkan.pl の最後を見ると、 if (__FILE__ eq $0) { ではじまる妙なプログラムが入っていることに気が付くでしょう。 実はこれはデバッグ用に入れたメインプログラムで、romkan.pl が 直接実行されたときだけ、この if 文の条件が真になってその中の ブロックが実行されるようになっています。 この中では $/ を空文字列にセットすることによってパラグラフ単 位での読み込みを指示し、1パラグラフ読み込む毎に元の文章と仮 名に変換した文章を表示しています。ローマ字で書かれたメールを このプログラムにかけると、いくらか読みやすくなります。 最初の if 文について種明しをしましょう。 __FILE__ は、スクリ プトが入っているファイル名に変換され、__LINE__ はそのファイ ルの中での行番号になります。この例では __FILE__ は、 'romkan.pl' になるわけですが、通常の場合 / からのフルパスに なります。一方、$0 という特殊な変数には、スクリプトを実行し ているメインのファイル名が含まれます。 つまり、romkan.pl を別のプログラムから do や require などで 読み込んで使った場合には、__FILE__ と $0 の内容は一致しない ので、if 文の条件式が偽になります。ところが romkan.pl を直接 実行すると両者が一致し、その時だけテスト用のメインプログラム が実行されるわけです。もちろん、romkan.pl を読み込む前に $0 を変更して __FILE__ と一致するようにすることも可能です。しか し、偶然そんなことをする人はまずいないでしょう。 ライブラリを作った時には、それをテストするためのテストプログ ラムが必要になりますが、このようにライブラリ自身の中に埋め込 んでおくと便利です。C 言語ならば #ifdef DEBUG のようにして、 コンパイル時に main() を入れるかどうか指定するところですね。 □ おわりに 今回のプログラムは、何も連想配列を使ったり、サブルーチンを定 義する文字列を作ってから eval したりなどという面倒なことをし なくても、最初からべたべたコーディングしていって実現すること ができます。おそらく、その方が効率もいくらかいいでしょう。 しかし敢えてこのような面倒な処理をしたのは、主にプログラムの 開発を容易に行うためと、可読性を向上してメンテナンスを楽にす るためです。今回のような形式でローマ字と仮名の対応を記述して おけば、全体を簡単に見渡すことができますし、変更も容易にでき ます。これが、 next if s/^a/あ/; next if s/^i/い/; next if s/^u/う/; ... などのような文が200行近くも続いていたら随分と読みにくくなっ てしまうでしょう。(本当はその方が原稿の量が増えていいのです けど :-)) 今回の場合にはプログラムを作成する時間を考えるとどちらが有利 だったかは簡単には判断できません。しかし、メンテナンスの時間 も考えると、おそらく開発効率の向上に役だっていると思います。 他人のプログラムや仕事の仕方を見たときに、ちょっと工夫すれば もっと楽にできるのにな、と思うことがよくあります。それは、ちょっ とした開発の手順であることもあるし、適切な処理言語やツールを 使っていないことだったりもします。自分でやっているとなかなか それに気がつかないことも多いので、たまには他人に自分の作業の 進め方が正しいかどうか客観的に評価してもらうことが必要でしょ うね。特に Perl の場合には、いくらでも工夫する余地が残されて いることが多いです。もっともそのために開発効率が落ちてしまう ということもよく起こります :-)。 □□ ------------------------------------------------------------ romkan.pl ------------------------------------------------------------ #!/usr/local/bin/perl -s ;###################################################################### ;# ;# romkan.pl: ローマ字かな変換サブルーチン ;# ;# Copyright (c) 1993 by srekcah@sra.co.jp ;# Software Research Associates, Inc., Japan ;# ;###################################################################### ;# ;# SYNOPSIS ;# ;# $kana = &romkan($roma [, CODE [, KATAKANA]] ); ;# ;# DESCRIPTION ;# ;# Subroutine &romkan returns KANA string expressed by the first ;# argument. It returns undef when translation was failed. ;# ;# Second argument specifies the encoding of return string. It ;# is encoded in 'euc' by default. Use 'euc', 'sjis' or 'jis'. ;# ;# If the third argument is supplied and its value is ;# true, return string is expressed by KATAKANA rather ;# than HIRAGANA which is default. Use undef for ;# second argument if you don't want to specify the code. ;# ;###################################################################### ;# ;# SAMPLE: ;# ;# require('romkan.pl'); ;# while (<>) { ;# s/[\w\-]+/&romkan($&)||$&/ge unless 1 .. /^$/; ;# print; ;# } ;# ;###################################################################### package romkan; require('jcode.pl'); $pcode = 'euc'; $romkan_table = <<'__TABLE_END__' unless $romkan_table; a あ i い u う e え o お ka か ki き ku く ke け ko こ ga が gi ぎ gu ぐ ge げ go ご sa さ si し su す se せ so そ za ざ zi じ zu ず ze ぜ zo ぞ ta た ti ち tu つ te て to と tsu つ da だ di ぢ du づ de で do ど na な ni に nu ぬ ne ね no の ha は hi ひ hu ふ he へ ho ほ fa ふぁ fi ふぃ fu ふ fe ふぇ fo ふぉ pa ぱ pi ぴ pu ぷ pe ぺ po ぽ ba ば bi び bu ぶ be べ bo ぼ ma ま mi み mu む me め mo も ya や yu ゆ yo よ ra ら ri り ru る re れ ro ろ wa わ wi ゐ we ゑ wo を kya きゃ kyu きゅ kye きぇ kyo きょ gya ぎゃ gyu ぎゅ gye ぎぇ gyo ぎょ sha しゃ shi し shu しゅ she しぇ sho しょ zya じゃ zyu じゅ zye じぇ zyo じょ ja じゃ ji じ ju じゅ je じぇ jo じょ jya じゃ jyu じゅ jye じぇ jyo じょ tya ちゃ thi てぃ tyu ちゅ tye ちぇ tyo ちょ cha ちゃ chi ち chu ちゅ che ちぇ cho ちょ dya ぢゃ dhi でぃ dyu ぢゅ dye ぢぇ dyo ぢょ nya にゃ nyu にゅ nye にぇ nyo にょ hya ひゃ hyu ひゅ hye ひぇ hyo ひょ pya ぴゃ pyu ぴゅ pye ぴぇ pyo ぴょ bya びゃ byu びゅ bye びぇ byo びょ mya みゃ myu みゅ mye みぇ myo みょ rya りゃ ryu りゅ rye りぇ ryo りょ xa ぁ xi ぃ xu ぅ xe ぇ xo ぉ xwa ゎ xtsu っ xtu っ xya ゃ xyu ゅ xyo ょ n' ん n ん - ー __TABLE_END__ # まず EUC に変換 &jcode'convert(*romkan_table, $pcode); # 連想配列 %romkan をセットアップ %romkan = @romkan = split(/\s+/, $romkan_table); $consonants = 'ckgszjtdhfpbmyrw'; for ($consonants =~ /./g) { $romkan{"$_$_"} = $romkan{'xtsu'}; } for (0..9) { $romkan{$_} = $_; } # &main'romkan の定義 ;;; eval($sub_romkan = q% sub main'romkan { local($_, $code, $katakana) = @_; local($kana) = ''; while (length) { % . join('', grep(++$i%2 && ($_ = "\tnext if s/^$_//i;\n"), @romkan)) . q% next if s/^\d//; next if s/^([%.$consonants.q%])\1/\1/i; last; } continue { $kana .= $romkan{"\L$&"}; } return undef if length; $kana =~ s/\244(.)/\245$1/g if $katakana; &jcode'convert(*kana, $code, $pcode) if $code && $code ne $pcode; $kana; } %); ;###################################################################### # デバッグ用メインプログラム if (__FILE__ eq $0) { package main; print $romkan'sub_romkan if $debug; $/ = ''; while (<>) { print; s/[\w\-]+/&romkan($&)||$&/ge; print; } } ;###################################################################### 1;