「美文書第5版」のp.373のスクリプト

「美文書第5版」のp.373のスクリプト

- Z. R. の投稿
返信数: 7

ギリシャ・ロシア文字を「欧文扱い」にするスクリプトで、サポートページにも掲載されている次のスクリプトについての話です。

#!/usr/bin/perl
# -*- coding: utf-8; mode: cperl; -*-
# Cyrillic and Greek to ^^HEX format
use utf8;
binmode(STDIN, ":utf8"); binmode(STDOUT, ":utf8");
while (<STDIN>) {
  utf8::decode($_);
  foreach my $chr (split(//, $_)) {
    if ((($chr ge "\x{0400}") && ($chr le "\x{04ff}")) || # Cyrillic
        (($chr ge "\x{0370}") && ($chr le "\x{03ff}")) || # Greek
        (($chr ge "\x{1f00}") && ($chr le "\x{1fff}"))) { # Greek Ext
      utf8::encode($chr); # UTF-8 encode
      foreach my $bchr (split(//, $chr)) {
        print(sprintf("^^%x", ord($bchr)));
      }
    } else {
      print($chr);
    }
  }
}

7行目の decode は不要だと思います。これがあると入力が二重にデコードされてしまいます。

Z. R. への返信

Re: 「美文書第5版」のp.373のスクリプト

- 安田 功 の投稿
ZR さん,こんばんは。

文責・安田です。ご指摘ありがとうございます。
こういう点も細かく見ていただき恐縮いたします。

基本的なことですが,utf8::decode($_); があると,ZR さんの
環境では「二重にデコードされ」る問題で動作しない,
ということでしょうか?
ちなみに,私はこのコードを Mac OS X Snow Leopard, Tiger,
Windows 7/XP Cygwin 環境で試験しております。

もとより,私は Perl を自分の目的に適う程度でしか使えませず,
このコードが最適解であるとはまったく思っておりません。

ですが,昔,UTF-8 が Perl でやっとまともにサポートされた
ころ(5.8.0 あたり?),

 (1) use utf8;
 (2) binmode(xxx, ":utf8");
 (3) utf8::decode($str);

の 3 点セットがないと,IO,$str 中文字の length 取得,
正規表現操作がうまく動作しなかった記憶があり,
以来,おまじないのように書いております。
あくまで私の記憶ですので,外しているかも知れません。
Perl の版に関するこの曖昧な記憶について,Perl の発展に
詳しい方のコメントがいただければと存じます。

たしかに,最近の Perl-5.8.8 以降では,Unicode サポートが
完成し,標準 IO が UTF-8 になっており,(1) が不要になった
だけでなく,(2), (3) いずれかが指定されていれば,UTF-8
外部ファイルの UTF-8 文字単位の操作ができるように
なっています。

その意味では (1) および (2) or (3) は冗長に過ぎないかも
知れません。でも,まあ,上のような次第でご覧のコードに
なっております。
もちろん,Perl の版を幅広くカバーするというような
積もりも目論みもありません。
でも,最新 Perl でも動作そのものに支障はないと考えています。
デコードが二重になされて誤動作するわけではないと思います。
もし気持ち悪いなら (3) を外していただければと思います。

ところで,ZR さんのご指摘とは少し外れますが,
(3) utf8::decode($_); はまずくないか?ということについて,
いま現在の私の再考するところとして,
LaTeX のマクロファイルなど ptexenc が通すファイル群が
必ずしも UTF-8 8 bit 文字とは限らないので,
(3) ではなく,むしろ (2) を削るのがより適切な改善では
ないでしょうか。

というわけで,コードはむしろ次がよろしいかと存じます。

#!/usr/bin/perl
# -*- coding: utf-8; mode: cperl; -*-
# Cyrillic and Greek to ^^HEX format
binmode(STDOUT, ":utf8");
while () {
utf8::decode($_);
foreach my $chr (split(//, $_)) {
if ((($chr ge "\x{0400}") && ($chr le "\x{04ff}")) || # Cyrillic
(($chr ge "\x{0370}") && ($chr le "\x{03ff}")) || # Greek
(($chr ge "\x{1f00}") && ($chr le "\x{1fff}"))) { # Greek Ext
utf8::encode($chr); # UTF-8 encode
foreach my $bchr (split(//, $chr)) {
print(sprintf("^^%x", ord($bchr)));
}
} else {
print($chr);
}
}
}

安田 功 への返信

Re: 「美文書第5版」のp.373のスクリプト

- 安田 功 の投稿

すみません,プログラムコードをそのまま書いちゃいましたので, ぐじゃぐじゃになりました。<pre> タグ付きで再投稿します。

#!/usr/bin/perl
# -*- coding: utf-8; mode: cperl; -*-
# Cyrillic and Greek to ^^HEX format
binmode(STDOUT, ":utf8");
while (<STDIN>) {
    utf8::decode($_);
    foreach my $chr (split(//, $_)) {
        if ((($chr ge "\x{0400}") && ($chr le "\x{04ff}")) || # Cyrillic
            (($chr ge "\x{0370}") && ($chr le "\x{03ff}")) || # Greek
            (($chr ge "\x{1f00}") && ($chr le "\x{1fff}"))) { # Greek Ext
            utf8::encode($chr); # UTF-8 encode
            foreach my $bchr (split(//, $chr)) {
                print(sprintf("^^%x", ord($bchr)));
            }
        } else {
            print($chr);
        }
    }
}
安田 功 への返信

Re: 「美文書第5版」のp.373のスクリプト

- Z. R. の投稿

安田さん、こんばんは。

「二重デコード」されて不正な結果になる例を示します。これに元のフィルタを適用して組版すると〈Â〉が落ちてしまいます。

% もちろん文字コードは UTF-8
\documentclass[a4paper]{article}
\usepackage[utf8]{inputenc}
\begin{document}
(G·TEAU)
\end{document}

この〈·〉の箇所のバイト列は「二重にデコード可能」という性質をもつことに注意します。

(a) C3 82 C2 B7
↓ UTF-8 デコード
(b) C2 B7 → Unicode 文字列としてみると〈·〉
↓ UTF-8 デコード
(c) B7 → Unicode 文字列としてみると〈·〉

仮に〈·〉だけ((a)のバイト列)が書かれた行が処理されると以下のようになります。最初のpLaTeXソースでもこれと同じことが起こっています。

  • <STDIN> で読込が行われた後、STDIN に :utf8 layer が指定されているので、$_ の内容は(b)になる。
  • utf8::decode($_) が行われると $_ の内容は(c)になる。
  • 結果的に(c)がSTDOUT(これも :utf8 layer 指定)に書き込まれる。標準出力に書き込まれるバイト列は(b)になる。
  • 結果のファイルの文字コードはUTF-8なので、これは文字列として(c)が書かれていることになる。

注意すべきは、Perl(5.8 以降)の文字列には「エンコーディング」の情報を持たないので、結局Perlでは「文字列」と「バイト列」が言語仕様上は区別されないということです。例えば、$s = "\xc2\xb7" とした場合、$s を「Unicode文字列」として見ると〈·〉であり、$s を「UTF-8バイト列」と見るとそれの表す文字列は〈·〉となります。(念のため言っておくと、これは $s の「UTF-8フラグ」の値とは無関係。)どちらであるかを認識しているのは人間だけなので、注意を怠ると二重のデコードが発生してしまいます。

そのように考えると

(1) use utf8;
(2) binmode(xxx, ":utf8");
(3) utf8::decode($str);

の間には「冗長性」(=あっても意味は不変)はないと思います。つまり、(2)と(3)が両方あるのは片方だけあるのとは意味が異なり、また(1)(Perl5.8.0以降では「スクリプトのエンコーディングがUTF-8であること」を意味する)はそもそも入出力とは無関係だからです。

ちなみに、正常なUTF-8のバイト列でないものを扱う場合は、(2)と(3)は動作が違います。(2)は警告を出して "\0" に置き換えますが、(3)は $str に変更を加えず、偽を返します。結局、元のスクリプトでは、二重デコード可能でない場合(非ASCII文字がある大部分の場合)に(3)は偽を返すことになります。

Z. R. への返信

Re: 「美文書第5版」のp.373のスクリプト

- 安田 功 の投稿
ZR さん,こんばんは。
詳しい説明をありがとうございました。
おっしゃるところがよくわかりました。
いつもいつも適切な事例をあげていただき敬服いたします。

整理すると,(2) binmode() と (3) utf8::decode() はどちらか一方,
ということですね。私が改訂案としてあげたコードならよさそうですね。
サポートページにも訂正をあげておきます。

ただ,utf::decode(); の影響ですが,「二重デコード」が起こるのは
input がいわゆる latin1 だけからなるストリングの場合に限られるのでは
ないでしょうか?
試験してみると,U+0100 以上の文字を含むテキスト断片の場合,
「·」のような latin1 も含めて何度 utf8::decode() を掛けようが
「二重デコード」は起こらないようです。

また,
>「二重デコード」されて不正な結果になる例を示します。これに元のフィルタを適用して組版すると〈Â〉が落ちてしまいます。
ですけど,この「元のフィルタ」は,そういう意味で,美文書掲載の
ptexfilter のことではないですよね。
これはキリル,ギリシア文字を選択的に処理するので,「二重デコード」の
問題は(運良く)発生しないはずだと思いますが,いかがでしょうか。私が
「基本的なことですが,utf8::decode($_); があると,ZR さんの環境では
「二重にデコードされ」る問題で動作しない,ということでしょうか?」
と書いたのはそういう意味です(原理的なことではなく)。
私もそうはいってもラテン文字,漢字を含む Unicode テキストで試験した
上で書きましたので,ここで外すと読者様にヒラにお詫びしなくてはなりませんね。

それでは。
安田 功 への返信

Re: 「美文書第5版」のp.373のスクリプト

- 安田 功 の投稿
ZR さん,
latin1 だけからなるテキストの場合に起こる,ご指摘のバグ訂正版を,
サポートページに今夜中にアップしておきます。
ありがとうございました。

安田 功 への返信

Re: 「美文書第5版」のp.373のスクリプト

- 安田 功 の投稿
Z.R.さん,こんばんは。

バグ訂正版を『美文書第5版』多言語章サポートページにアップしました。
おかげさまで,早いうちに訂正できました。

Utf82TeX にも同じバグがありましたので,こちらもあわせて訂正して,
utf82tex-1007 として公開しました。

よい勉強になりました。ありがとうございます。

安田 功 への返信

Re: 「美文書第5版」のp.373のスクリプト

- Z. R. の投稿

改訂版のアップロードお疲れ様でした。

>「二重デコード」が起こるのは input がいわゆる latin1 だけからなるストリングの場合に限られるのではないでしょうか?

これについては、お察しの通りです。本当に蛇足ながら解説しておくと:

(a) C4 82 C2 B7
↓ UTF-8デコード
(b)102 B7 → Unicode文字列〈Ă·〉
× UTF-8デコード不能
(c)

UTF-8で書かれたテキストがLatin-1以外(U+0100以上の符号値)の文字を含むということは、すなわち(b)のPerl文字列が0x100以上の符号値を含むことになります。このようなPerl文字列はそもそも「バイト列と看做す」こと自体ができないので、これが「2度目のUTF-8デコード」を通す可能性はありません。先に書いた通り、この場合はutf8::decode()は偽を戻り値とします。

従って、「二重でコード」が起こるのはテキストがLatin-1の文字(U+0000~00FF)のみを含む場合に限られます。ただし、件のスクリプトは行ごとにデコードを行っているので、「Latin-1のみを含む行」が必要条件であることに注意を要します。実際、私が返信で挙げた例も、全体としてはLatin-1でない文字(先頭コメント行の日本語)を含んでいます。

>この「元のフィルタ」は,そういう意味で,美文書掲載の ptexfilter のことではないですよね。

私が実験に用いたのは、サポートページにある修正前のptexfilterでした。ただ美文書掲載のものもコメント部分が違うだけのように見えますが…