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

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

   
青空文庫の作品は、第3水準、第4水準漢字をすべて「※」に置き換えて、[#]タグの中に文字コード情報を記す形となっています。
(例) 惝 ⇒ ※[#「りっしんべん+淌のつくり」、第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 の《 》はそれぞれ〈 〉とします。
これらは普通に扱える文字の範囲内ですが、青空文庫ではこの括弧を《ルビ》として使っているため、底本の中に現れる《 》は「※」になっていることがあります。

○ 変換プログラムについて
プログラムを大幅に改良しました。置き換える文字の検索手順という根本からの改良です。
これまで、区点番号対照表と正規表現を使って総当たりで探っていく方法を採っていました。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;

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


○ 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 でこの文字を表示するかですが、何か、お間抜けな見過ごしがあって、なあんだこんなことか、という笑い話のような解決法があるんじゃないかとも思うのですが、今はそれが見えないのです。


○ おまぬけでした
chr関数は漢字も表現できます。引数を10進数に変換すればよかったのです。
$str = chr(hex($1)) とするだけでよかったのです。
これで漢字変換は出来たのですが、これは文字化けと隣り合わせの変換でもあります。
文字化けの起こる仕組みはよく理解できないのですが、文字化けが起こるのは対象となる1行だけです。ここはまたまた対症療法的に、変換対象の文字の前後に改行を入れて、1行1文字にしてみました。場当たり的な対策ですが、これで問題解決です。

変換スクリプトは次の1行です。
$_ =~ s/(※[#.*?U\+)([0-9A-F]{4,5})([^]]*?])/chr(hex($2))/e;

これはローカルな windows 上での解決法です。これを cgi に移植しなければなりません。送信されたデータを改行コードで区切って、ファイルに書き出せば、なんとかなりそうです。とにかく先が見えてきたって感じです。

Unicode から utf-8 に変換する関数も作ったのですが、これは必要なさそうです。


○ パール文字コードの勉強しました
use utf8; はおまじないではなかったのです。
意味も分からず使って、かえって文字化けを起こしたりして、use utf8 に嫌われているように思っていたのですが、use utf8 はスクリプト内の日本語をパールの内部文字列として認識するというだけの命令でした。文字化けが起こったのは、この内部文字列とファイルから読み込んだ外部文字列の混在によるものだったようです。

decode しない文字列は、パールにとっては文字なのか画像なのかも分からない二進数の羅列に過ぎないなのですが、それまでは、それがたまたまうまく行っていただけだったのです。パールに文字列として認識させるためには decode が必要なのでした。

とは言え、正規表現もある程度はうまく行くようですし、chr関数やsubstr関数を使わないのであれば、decode なしで済ませることも可能です。

可能ですが、先に述べた最小マッチが効かなかったのは、decode していなかったからだと思われます。
やはり、ファイルから読み込んだ文字列はまず decode する。ファイルなどに書き込むときには encode する。これが鉄則ですね。

use utf8;
use Encode qw/encode decode/;
$_ = decode("UTF-8",$_);
$_ =~ s/青空/文庫/g;
$_ = encode("UTF-8",$_);
print OUT $_;

これですべてよし、です。

 
○ 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行でいいのですね。悪戦苦闘の痕跡がちょっと寂しい。

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



かめゐ
okame @ kameique.com