home      
clear   青空文庫の※を漢字に変換

青空文庫の作品をメモ帳などで開き、全選択、コピーして、下のテキスト枠に貼り付けてください。
 clear    

   
青空文庫の作品は、第3水準、第4水準漢字をすべて「※」に置き換えて、[#]タグの中に文字コード情報を記す形となっています。
(例) 惝 ⇒ ※[#「りっしんべん+淌のつくり」、第3水準1-84-54]
このページは、この※と[#]タグをもとの漢字に戻すための変換プログラムです。

では、具体的にどういうことが出来るのか。
試しに、この説明文の冒頭をテキスト枠にコピーして、変換ボタンを押してみてください。
惝 ⇒ ※[#「りっしんべん+淌のつくり」、第3水準1-84-54] が、
惝 ⇒ 惝 に変換されていると思います。
はい、こんなことです。

変換後は、説明文を表示しないすっきりしたページ構成になります。もう一度この説明文に戻りたいときは、ページ右上の「青空文庫の※を漢字に変換」をクリックしてください。

青空文庫リーダー(ソフト)にはこの※を内部的に変換して表示してくれるものもあります。PageOne や OyaziViewer などがそうです。それでも、元のテキストの※を漢字に変換してutf-8で保存しておけば、何かといろいろ便利なこともありそうです。二十年来愛用している smoopy で軽快にさくさく読めるようになるのも、うれしいことです。


○ 変換できないものについて
変換後のテキストを※で検索すると、変換できずに残っていることがあります。
[#]タグの中に句点番号の無いものは、当然のことながら、変換できませんん。また、Unicode の文字コードが記されているものも、ここでは変換できません。Unicode は件数としてはごく少数ですので、私は windows の IMEパッドから文字コードを探して置き換えるようにしていますが、そのうちプログラムで変換できるようにしたいと思っています。

Unicode も変換できるようになりました。
説明文の下の方に、出来るようになるまでの悪戦苦闘ぶりを記しています。笑い話です。


○ 例外について
[#二の字点、1-2-22]は「〻」に該当する句点番号ですが、ここでは例外措置として「々」に変換します。
また、[#コト、1-2-24]は「ヿ」ですが、ここでは「コト」とします。

ヿは「事」の草書体の一部で、片仮名の一つだそうです。 2音節の仮名もあるんですね。
例:躊躇シテイタヨウナ事柄ヲモアエテ書キ留メルヿニシタ。谷崎潤一郎 鍵

1-1-52 と 1-1-53 の《 》はそれぞれ〈 〉とします。
これらは普通に扱える文字の範囲内ですが、青空文庫ではこの括弧を《ルビ》として使っているため、底本の中に現れる《 》は「※」になっていることがあります。

○ ページの作りについて
ページ右上の「青空文庫の※を漢字に変換」をクリックすると aozorara.html を表示し、「clear」は aozorara.cgi を表示します。

aozorara.cgi は漢字変換プログラムの本体となるもので、説明文を省略した単純なページ構成となっています。
aozorara.html は入口のページのようなものです。変換フォームが先頭に来て、説明文がその下に続きます。

上記2つ以外に、index.html もあります。ここでは必要のないファイルですが、設置しない訳にもいかないので、白紙にしておくよりは有効利用した方がいいだろうと言うことで、説明文を先頭に置く作りにしてみました。

ファイル名は最初、aozora.html にしていたのですが、なにか、青空文庫さんに対して申し訳ないような、おこがましいような気がして、aozorara って変な名前になってしまいました。


○ 変換プログラムについて
プログラムを大幅に改良しました。置き換える文字の検索手順という根本からの改良です。
これまで、区点番号対照表と正規表現を使って総当たりで探っていく方法を採っていました。4500枚のフィルターで濾過するような変換方法だったのですが、それをたった2枚のフィルターだけで処理できるようになったのです。Unicodeフィルターと区点番号フィルターの2枚です。

根本からの改良と言っても、対照表を配列に格納して、そこから文字を取り出すという、ごくありふれた検索方法ではあるのですが、今までここに思い至らなかったのです。わたしの人生、こういうことの連続です。ま、とにかく、これがうまくいきました。

まず、区点番号は5桁の整数値にしておきます。それを bango[i] に代入し、moji[i] には対応する漢字を入れます。[#]タグから取得した区点番号も整数値に変換して、bango[i]に対して検索するものです。

検索ルーチンは、昔、MSXという玩具のようなパソコンでプログラムの勉強をしていた時に覚えたやり方です。もう四半世紀も前に学んだことが今またよみがえって役に立っている、なんかうれしいですね。
その四半世紀前に覚えたやり方のスクリプトを書いておきます。

sub search{
 $gomen = $_[0].$_[1].$_[2];
 $kuten = $_[1];
  $high = @bango;
  $low=0;
  $i = int(($high + $low)/2);
  for($j=0;$j<15;$j++){
   if($bango[$i] == $kuten){
    return $moji[$i];
    }
   if($bango[$i] < $kuten){
     $low = $i;
    }else{
     $high = $i;
    }
   $i = int(($high - $low)/2) + $low;
  }
 return $gomen;
}

処理速度も向上しました。そこで、変換対象をJIS漢字表すべての約11,700字にまで拡張しました。拡張と言っても、普通に扱える第2水準以下の漢字、ひらがな、半角英数字などをカバーするような下方拡張ですので、青空文庫の作品を扱う上ではあまり意味があるとも思えません。でも、すべてをカバーするというのは、やはり、ちょっとうれしいです。

2枚のフィルターです。
$str =~ s/(※[#.*?U\+)([0-9A-F]{4,5})([^]]*?])/&#x$2\;/g;
$str =~ s/(※[#.*?)([1-2]\-\d{1,2}\-\d{1,2})([^]]*?])/&search($1,$2,$3)/eg;

ほんとうは「#.*?」を「#[^0-9]*?」として、ここは数字を除外したいのですが、[^0-9] も \D も効かないのです。不思議。
Unicode の正規表現には括弧が多すぎるようですが、これは、ま、おまじないのようなものです。


○ プログラムについて  これは古いプログラムの記録です
変換は、文字コードと該当する漢字の対照表をもとに、ひとつひとつ総当たりで探り当てるという原始的な方法を採っています。漢字3695文字、記号など845文字、そのすべてについて正規表現で置き換えて行きます。のんびりとしたスクリプトです。cgiを読み込むだけでも、こののんびりした感じが滲み出しているように思われます。結果が出せれば、方法は拙くてもいいではないか、と思うことにしました。

最初は、区点番号からJIS文字コードを取得して、JISコードからutf-8に変換する方法を模索しました。まあ、順当な発想です。
区点番号は1から始まる10進数、JISコードは21から始まる16進数ですので、区点番号に16進数の20(10進数の32)を足して、16進数に変換すればJISコードが求められるということが分かって、これはいけると思ったのですが、その後がまったく駄目でした。どうやってもうまくいきません。

対策をネット検索しているうちに、漢字対照表を使う方法もあるということが分かりました。対照表と正規表現で変換できると言うこと、特にこの対照表はありがたいものでした。
いくつかの試行錯誤はありながらも、これはうまくいきました。めでたし、めでたしです。


○ 問題噴出か
基本的には、めでたしなのですが、まだまだ問題は噴き出してきます。

その中でもっとも悩ましかったことは、変換対象が同じ行に複数ある場合に、いくつか跨いで変換してしまうことでした。跨いだ部分は消えてしまいます。正規表現の最少マッチが効かないようなのです。最大マッチでもなく最少マッチでもない、それがまた悩ましいです。「?」があれば最少マッチになるんじゃないのか。悩ましい。

スクリプトの記述、
 $str =~ s/※[#.*?第3水準1-14-11[^]]*?]/伃/g;

これを[#[^]]*?第 としたり、[#[^第]*?第 としたり、いろいろやってみても駄目で、もっともっと、正規表現をさらに工夫すればいいのでしょうけれど、もうお手上げです。
正規表現の正しい記述法は確かにある筈なのですが、一方では、特定の条件下では現行の記述のままでも正しくなる筈なのですから、その条件を作ることにしました。1行につき変換対象が1つであれば問題はない訳です。

そこで、「※」の前に改行を加えることにしました。実際は、元に戻せるように、「※」を「改行=&=※」に変換して、最後に「改行=&=」を削除します。
スクリプトの最初に $str =~ s/※/\n=&=※/g;
スクリプトの最後に $str =~ s/\n=&=//g;

これで、問題解決です。
対症療法的、あるいは場当たり的ですが、発想の転換と呼ぶことにします。


○ ことのはのとこ
そもそもこの変換プログラムは、文学作品検索のデータ処理のために作ったものです。「ことのはのとこ」のデータファイルに使っています。

今回、このスクリプトをcgiに作り直しました。windows上のperlではファイルから1行ずつ変数に読み込んでいましたが、cgiでは小説をまるごと1つの変数に代入して処理しています。
太宰治の全作品を1本にまとめた7メガバイトを超えるファイルでも試してみました。変換に120秒ぐらいかかりましたが、無事、完了しました。120秒の間、画面は止まったままでしたので、無事というのが実感でした。
7メガはさすがに大きすぎるように思いますが、1メガ程度でしたら軽快に処理できるようです。

先に述べた悩ましい問題は、このcgiに作り直しているときに発見しました。発覚したというべきかな。ことのはのとこのデータは修正前のプログラムで処理しています。谷崎潤一郎の春琴抄や吉野葛には、跨いで消された文字がいくつもありそうです。作り直しですなあ。


○ Unicode型漢字変換、作りました
Unicodeから漢字に変換するスクリプトを作ったのですが、先の変換から漏れた※は変換できませんでした。青空文庫の作品では、第3第4水準にも記載されていない漢字をUnicodeで表しているようなのです。
工作員の好みで区点番号になったりUnicodeになったりしているのではなかったのですね。一つの作品の中にこれらが混在している訳が分かりました。

文字コードが記されているのですから、対照表に拠らず、直接そのまま変換できそうなものなのですが、これも難しいのです。chr関数が漢字にも対応していれば、次の式でとりあえずは変換できるのですが、chr関数はASCII文字だけのようです。
 $str =~ s/※[#.*?、U\+([0-9A-F]{4,5})、[^]]*?]/chr($1)/e;

ただし、chr関数が漢字に対応していたとしても、これでうまく変換できるとは、まだまだ到底思えません。utf-8は3バイト文字の筈ですが、青空文庫のUnicodeは16進数4桁、ときどき5桁です。3バイトは6桁必要です。どうなっているのか、ううん、分かりません。文字コードの勉強を一から始めなければならないと言うことなのでしょうか。気が遠くなる思いです。


○ 少し勉強しました
区点番号が JIS EUC SJIS にとっての文字一覧表であるように、Unicode は utf-8 utf-16 にとっての文字一覧表だと見ることが出来る、このような関係なのだそうです。
と言うことは、utf-8 は Unicode から直線的に変換可能だと言うことです。具体的には、Unicode 2バイト(16bit)を 4bit 6bit 6bit に分けて、それぞれに16進数の E0 80 80を足せば utf-8 3バイトになるのだそうです。ちょっとややこしそうですが、一旦文字列として分割するなりなんなりして関数を作っておけばいい訳で、こちらはなんとかなりそうです。
問題は、どうやって perl でこの文字を表示するかですが、何か、お間抜けな見過ごしがあって、なあんだこんなことか、という笑い話のような解決法があるんじゃないかとも思うのですが、今はそれが見えないのです。


○ 一歩前進です
utf-8 コードから文字を表示する手掛かりを見つけました。
例として、「俱」の Unicode は 4FF1 で、utf-8 は E4BFB1 です。
 $str =~ s/※[#.*?、U\+4FF1[^]]*?]/\xE4\xBF\xB1/g;
のように、utf-8 を直接記述すれば、該当する文字「俱」に置換できます。

Unicode から utf-8 に変換する関数 uni_utf も作りました。

 $str =~ s/※[#.*?、U\+([0-9A-F]{4})[^]]*?]/&uni_utf($1)/eg;

これでうまくいく筈でしたが、結果は、「俱」ではなく、\xE4\xBF\xB1 に置き換わるのでした。でも、もう一歩というところまで来ているように思います。それでも、この一歩が分からない。
 
Unicode から utf-8 に変換する関数のスクリプトを書いておきます。

sub uni_utf{
  $uni = substr($_[0],-6,2);
  if($uni ne "U+"){         # 4桁でないとき
    return &uni_utf4($_[0]);
  }
  $uni = substr($_[0],-4,4);
  $u = hex($uni);        #16進数を10進数に
  if($u < 128){         # 7F 以下では1バイト
    return substr($uni,-2,2);
  }
  if($u < 2048){         # 07FF 以下では2バイト
    return &uni_utf2($u);
  }
  $u = sprintf("%b", $u);    #10進数を 2進数に
  $u = "0000$u";        #0001が1になるのを補正

  $u1 = substr($u,-16,4);
  $u2 = substr($u,-12,6);
  $u3 = substr($u,-6,6);

  $u1 = oct('0b' . $u1);    # 2進数を10進数に
  $u1 += 224;
  $u1 = sprintf("%X", $u1);   #10進数を16進数に

  $u2 = oct('0b' . $u2);
  $u2 += 128;
  $u2 = sprintf("%X", $u2);

  $u3 = oct('0b' . $u3);
  $u3 += 128;
  $u3 = sprintf("%X", $u3);

  $utf = $u1.$u2.$u3;
  return $utf;
}

2バイトに変換する &uni_utf2($u) では、5bit 6bit に分けて、10進数の 192 128 を足します。あとは同じような処理です。

5桁(実は6桁)の Unicode の場合、utf-8 は4バイト文字になります。こちらは、3bit 6bit 6bit 6bitに分け、240 128 128 128 を足して同じ処理です。

utf-8 に1バイト文字から4バイト文字まであるとは知りませんでした。3バイト前提で書いたスクリプトを修正しました。2バイト変換も急場凌ぎのようにして別の関数を呼び出しています。ま、プログラムは拙くても、結果が出せればそれでいいのです。


○ 出来ました。問題解決!完成です
数値文字参照で問題解決です。
なあんだこんなことか、と拍子抜けするような問題だったのですが、見落としではなく、知らなかったのですね。5桁の Unicode について調べていて、たまたま発見した、見付けてしまったのです。

Unicode を utf-8 に変換する必要もありません。Unicode をそのまま表示できます。

 $str =~ s/※[#.*?、U\+([0-9A-F]{4,5})[^]]*?]/&#x$1\;/g;

区点番号型のスクリプトにも上の1行を追加しました。たった1行でいいのですね。悪戦苦闘の痕跡がちょっと寂しい。

でも、めでたし、めでたしです。



かめゐ
okame @ kameique.com