WindowsでPerl 5.8/5.10を使うモンじゃない

長い間に,私はPerlを使うときに次のようなテンプレートを使うようになっていた。

#!perl
# utf8でセーブ
# ---------------------------------------------------
# @ARGV = map { decode('cp932',$_) } @ARGV ;
# ---------------------------------------------------
# opendir(D,encode('cp932',"表")) ;
# @nodes = map { decode('cp932',$_) } readdir(D) ;
# closedir(D) ;
# --------------------------------------------------
# mkdir(encode('cp932',"表"),0777) ;
# --------------------------------------------------
# open(OUT,encode('cp932',">表.txt")) or die "open:$!\n" ;
# --------------------------------------------------
# use Devel::Peek;
# Dump($str) ;
# --------------------------------------------------
#
use utf8;
use Encode;
# use open IO => ":encoding(cp932)";
# binmode(STDIN,":encoding(cp932)") ;
# binmode(STDOUT,":encoding(cp932)") ;
binmode(STDERR,":encoding(cp932)") ;

これを見て,私はrubyを勉強始めた。


Perl5.8/5.10で日本語処理を行う方法とその問題点についてまとめたいと思う。

1. 日本語コードを意識するところ

Perlスクリプトで,文字コードを意識しなければならないケースは以下のようなものである。

  1. スクリプトソースコード
  2. ファイルとの入出力
  3. 標準入力(コンソール入力)・標準出力(コンソール出力)・標準エラー出力(コンソールエラー出力)
  4. システムコールまたはライブラリ関数へのパラメータ,入力データ,返り値,出力データ
  5. コマンドのパラメータ(引数)

他の言語では,「2.ファイルとの入出力」と「4.システムコール・ライブラリ関数...」は同じものであるが,Perl5では特別な扱い(PerlIO)をしていると考えた方が良いと思う。

で,この扱いの違いが,Windows(あるいは,ShiftJISがシステム標準の日本語コードのOS)で大きな混乱の元になっていると私は思う。

2. PerlのソースはUTF8で書かなければならない

use utf8;

日本語処理したい場合,Perl5ではutf8を使わなければならない。utf8で無い場合("no utf8;"と宣言する),文字列はバイナリコード列として評価されるということになっているけど,そんなことをしたら,Perlで日本語の正規表現とか使えなくなってしまう。それって,Perlで日本語を扱う意味があるのだろうか?

use encoding('ShiftJIS') ;

上のように書けば,ソースコードをShiftJISで記述できるが,薦められない。次のように動作し,一部の処理ではうまくいくが,そのためにわかりにくいところで文字化けが発生したりして,却って混乱を招くだけだから。

  1. ソースコードをUTF8に文字変換する。
  2. 次のコードを頭に追加する
    binmode(STDIN,":encoding(sjis)") ;
    binmode(STDOUT,":encoding(sjis)") ;
  3. そのコードを実行する。

文字変換をして実行するので,処理にオーバヘッドがかかる。「1.スクリプトソースコード」の文字列の出力は問題ないが,「9.コマンドのパラメータ(引数)」は意図的に変換しなければならない。あまり役に立たないと思うし,推奨もされていない。次のコードを実行してみると,わかると思う。

#! perl
# test_sjis.pl - ShiftJISでセーブする
use encoding('sjis') ;

print "テスト:表\n" ;
print @ARGV,"\n" ;

C:\TEMP> perl test_sjis.pl あ
テスト表
"\x{0082}" does not map to shiftjis at sjis.pl line 6.
"\x{00a0}" does not map to shiftjis at sjis.pl line 6.
\x{0082}\x{00a0},

意外と使えないのでがっかりする。確かに,ソースコードの行数が減る場合もあるけど,1.で書いたように意識しなければならない箇所は変わらず,意識をさせない分,問題なさそうで問題が出る箇所が見つけにくくなるので使わない方が良いと私は思う。

ついでに言うと,標準エラーは変換されないので,binmodeで文字コードを指定しなければならない。これも混乱の元だと思う。

3. 文字列にUTFフラグがあるということ

Perl 5.8で,UTF8の強力な正規表現がサポートされた。非常に役にたつのだが,そのために,文字列にUTF8フラグというものが付くようになった。UTF8の文字列を処理するには,文字列のコードがUTF8とするだけでなく,UTF8フラグが付いていなければならない。

use utf8がない。UTF8フラグはつかない use utf8がある。UTF8フラグがつく
#! perl
# no_utf8.pl - UTF8でsave

use Devel::Peek;

$str="strings" ;
Dump($str) ;

$str="文字列" ;
Dump($str) ;
#! perl
# use_utf8.pl - UTF8でsave
use utf8 ;
use Devel::Peek;

$str="strings" ;
Dump($str) ;

$str="文字列" ;
Dump($str) ;
SV = PV(0x1da070) at 0xe6900
REFCNT = 1
FLAGS = (POK,pPOK)
PV = 0x1d7698 "strings"\0
CUR = 7
LEN = 8
SV = PV(0x1da070) at 0xe6900
REFCNT = 1
FLAGS = (POK,pPOK)
PV = 0x147ca8 "\346\226\207\345\255\227\345\210\227"\0
CUR = 9
LEN = 16
E:\WORK\perltp>use_utf8.pl
SV = PV(0x32a070) at 0x236918
REFCNT = 1
FLAGS = (POK,pPOK)
PV = 0x327698 "strings"\0
CUR = 7
LEN = 8
SV = PV(0x32a070) at 0x236918
REFCNT = 1
FLAGS = (POK,pPOK,UTF8)
PV = 0x284248 "\346\226\207\345\255\227\345\210\227"\0 [UTF8 "\x{6587}\x{5b57}\x{5217}"]
CUR = 9
LEN = 16

$strという変数だけをみただけでは,UTF8フラグが付いているかどうかわからないのだ。それは,Perlスクリプトの規模が大きくなれば,UTF8フラグを把握するのが難しくなり,至る処に,Dump()を使う羽目になる。

それを避けるために,内部では,UTF8コードになるように,コーディングをするように私はしたが,思ったように動作しているかどうか,確認するための時間はかかっている。

3. WindowsのShiftJISコードは,cp932

ShiftJISと言っても,OS固有の文字とかあって,1つの規格があるわけでない。WindowsとUNIXのShiftJISは一部異なっている。Windowsで丸付き数字とか,扱う必要が出てくるので,WindowsのShiftJISの正式名称「cp932」を使う必要がある。

 use open IO => ":encoding(cp932)";
 binmode(STDERR,":encoding(cp932)") ;
 open(IN, "<:encoding(cp932)", "sjis.txt");

こんな感じに。

4. ファイルの入出力は簡単になった

確かに,ファイルの入出力とその処理は非常に簡単になった。もし,扱うファイルのコードがわかっているのであれば,頭に,"use open"のプラグマを入れるか,open時にコード指定を入れれば良いだろう。

#! perl
use utf8;
use open IO => ":encoding(cp932)"; # 全てのファイル入出力がWindowsのShiftJIS
use open IN => ":encoding(cp932)"; # 全てのファイル入力がWindowsのShiftJIS
use open OUT => ":encoding(cp932)"; # 全てのファイル出力がWindowsのShiftJIS
# または,個別のopen時に
open(IN, "<:encoding(cp932)", "in.txt");
open(OUT,">:encoding(eucjp)", "out.txt");

簡単なコード変換(ShiftJISからUTF8へ変換)

#! perl
# sjis_to_utf8.pl
use utf8;
use open IN => ":encoding(cp932)"; # 全てのファイル入力がWindowsのShiftJIS
use open OUT => "utf8"; # 全てのファイル出力がWindowsのShiftJIS

# -- 以下はサンプル --
open(IN,"in_sjis.txt") ;
open(OUT,">out_utf8.txt") ;
while (<IN>) {
    print OUT ;
}

簡単なコード変換 その2(ShiftJISからEUCへ変換)

#! perl
# sjis_to_euc.pl
use utf8;
open(IN, "<:encoding(cp932)", "in_sjis.txt");
open(OUT,">:encoding(eucjp)", "out_euc.txt");
while (<IN>) {
    print OUT ;
}

上記コード内で,文字処理を行う場合,UTF8でスクリプトを記述すれば,問題なく処理できると思う。

5. 標準入力・標準出力・標準エラーの指定

一般的なファイルの入出力と標準入力・標準出力・標準エラーの違いは,スクリプト実行時にopenされているか否かの違いで,binmodeで文字コードを指定すれば,自由にコード変換可能である。binmodeは,一般的なファイル入出力のディスクリプタにも使用できるので,特に標準入出力のみを意識する必要はない。

#! perl -w
use utf8;
binmode(STDIN,":encoding(cp932)") ;
binmode(STDOUT,":encoding(cp932)") ;
binmode(STDERR,":encoding(cp932)") ;

#! perl -w
use utf8;
binmode(STDIN,":encoding(cp932)") ;
binmode(STDOUT,":encoding(cp932)") ;

$\="\n" ;
while (<>) {
    chomp ;
    s/表/申/ ;
    print ;
}

6. 関数・システムコールは文字列(UTFフラグ無し)

よく考えてみなくても,当たり前のことなんだが,ファイル入出力以外はコード変換されない。関数の引数は,システムで使用されている文字コードで関数の返り値は,UTFフラグの付いている文字列になる。

以下の例は,文字化けしたファイルが作成される。

#! perl
use utf8;
use open IO => ":encoding(cp932)"; # 全てのファイル入出力がWindowsのShiftJIS
binmode(STDIN,":encoding(cp932)") ;
binmode(STDOUT,":encoding(cp932)") ;
binmode(STDERR,":encoding(cp932)") ;

mkdir("表",0777);
open(IN,"<表.txt") or die "open(表.txt):$!\n";

正解は,

#! perl
use utf8;
use Encode;
use open IO => ":encoding(cp932)"; # 全てのファイル入出力がWindowsのShiftJIS
binmode(STDERR,":encoding(cp932)") ;

mkdir(encode('cp932',">表.txt"),0777);
open(IN,encode('cp932',"<表.txt")) or die "open(表.txt):$!\n";

7. コマンド引数も文字列(UTFフラグ無し)

付け足しではあるが,コマンド引数も文字列(UTFフラグ無し)である。

#! perl
use utf8;
use Encode;
binmode(STDOUT,":encoding(cp932)") ;

@a = map { decode('cp932',$_) } @ARGV ;

print "ARGV:@ARGV\n" ;
print "CONV:@a\n" ;

8. Windowsで何が問題になるのか

1章から7章までを合わせると,冒頭のテンプレートができあがるというわけだ。これは,実際のプログラミングでは非常にデバクしにくいし,移植性(portability)が全くなくなるコードになってくる。実際コードを書いてみよう。

例: 正規表現を指定して,指定したディレクトリ配下のファイルから取り出すコードを書いてる。

コマンド形式: diregrep {パターン} {ディレクトリ}

次のようなテスト環境を用意する。

C:\TEMP\TP> tree /F dt
フォルダ パスの一覧: ボリューム S0P4N_V64070823
ボリューム シリアル番号は 42C9-943A です
C:\TEMP\TP\DT
├─alphabet
│      alpha.txt
│      sjis.txt

└─日本語
       alpha.txt
       sjis.txt

英数字のみの環境では次のコードで十分問題ないだろう。(このサンプルは,良いコードでは無いが,説明には十分ということなので,勘弁してほしい)

#!perl -w

&grepdir(@ARGV) ;

exit 0 ;
# ---------------------------------------
sub grepdir($$){
  my($pat,$dir) = @_ ;
  my($node) ;
  opendir(D,$dir) ;
  my @nodes = grep (!/^\./, readdir(D)) ;
  closedir(D) ;
  # -------------------------------------
  foreach $node (@nodes) {
    my $path="$dir/$node" ;
    print "!!$path!!\n" ;
    if ( -f $path ) {
      grepfile($pat,$path) ;
    }
    elsif( -d $path) {
      &grepdir($pat,$path)
    }
    else {
      print STDERR "skip:$path\n" ;
    }
  }
}

sub grepfile($$){
  my($pat,$file) = @_ ;
  open(IN,$file) or die "Error:open($file):$!\n" ;
  while (<IN>) {
    chomp ;
    print "$file:$_\n" if (/$pat/) ;
  }
}

これを実行させると,次のようになる。単純に動きそうで動かない。

E:\WORK\perltp>grepdir.pl aaa dt
!!dt/alphabet!!
!!dt/alphabet/alpha.txt!!
dt/alphabet/alpha.txt:aaa
!!dt/alphabet/sjis.txt!!
dt/alphabet/sjis.txt:aaa
!!dt/日本語!!
!!dt/日本語/alpha.txt!!
dt/日本語/alpha.txt:aaa
!!dt/日本語/sjis.txt!!
dt/日本語/sjis.txt:aaa
E:\WORK\perltp>grepdir.pl 表 dt
!!dt/alphabet!!
!!dt/alphabet/alpha.txt!!
Trailing \ in regex m/表/ at E:\WORK\perltp\grepdir.pl line 36, <IN> line 1.

直さなければならないところは,以下のようなところになる。

  1. 「表」などの日本語の文字列検索を行うためには,内部コードはutf8でなければならない。
  2. @ARGVの文字は,文字列(UTFフラグ無し)なので,utf8に変換する。
  3. diropen()やopen()で指定する文字列は,文字列(UTFフラグ無し)だが,表示や/$pat/での比較はutf8である。

ということで,数箇所,文字コードの変換が必要になってくる。

#!perl -w
use utf8; # 文字列をutf8として扱う
use Encode; # 文字コード変換 encode()/decode()ライブラリ
use open IO => ":encoding(cp932)"; # ファイルはSJISとして扱う
binmode(STDOUT,":encoding(cp932)") ; # 標準出力は,SJIS
binmode(STDERR,":encoding(cp932)") ; # 標準エラーも,SJIS
@ARGV = map { decode('cp932',$_) } @ARGV ; # コマンド引数をutf8とする。"/$pat/"のため
&grepdir(@ARGV) ;
exit 0 ;
sub grepdir($$){
    my($pat,$dir) = @_ ;
    my($node) ;
    opendir(D,encode('cp932',$dir)) or die "diropen($dir)\n" ; # 引数は,文字列(UTFフラグ無し)
    my @nodes = map { decode('cp932',$_) } grep(!/^\./, readdir(D)) ; # 内部は,基本的にutf8文字列になるように
    closedir(D) ;
    # --------------------------------------------------
    foreach $node (@nodes) {
        my $path="$dir/$node" ;
        my $Bpath=encode('cp932',$path) ; # -f/-d のファイルシステム比較では,システムの文字コード=ShiftJISで,文字列(UTFフラグ無し)
        print "!!$path!!\n" ; # printでは,UTF8→ShiftJISの変換が暗黙に行われている
        if ( -f $Bpath ) {
            grepfile($pat,$path) ; # 内部のインタフェースは,UTF8文字列で行う
        }
        elsif( -d $Bpath) {
            &grepdir($pat,$path) 
        }
        else {
            print STDERR "skip:$path\n" ; # Windowsでは普通ここには来ない
        }
    }
}
sub grepfile($$){
    my($pat,$file) = @_ ;
    my $Bfile=encode('cp932',$file) ;
    open(IN,$Bfile) or die "Error:open($file):$!\n" ; # open()の引数は,文字列(UTFフラグ無し)
    while (<IN>) { # ファイルからの読み込みで,ShiftJIS→UTF8への案目的に変換
        chomp ;
        print "$file:$_\n" if (/$pat/) ; # printでは,UTF8→ShiftJISの変換が暗黙に行われている
    }
}

結局のところ,内部の文字列がutf8文字列か文字列(UTFフラグ無し)か意識しなければコーディングできないのだ。

プログラミングの時にロジックに集中できず,絶えず変数の状態に気をつけなければならない。文字コードを常に意識しなければならいなんて,悪夢でしかない。

9. ではどうすれば良いのか

なぜ,Perlでプログラムを組むのが,こうも苦痛を伴う作業になったのだろうか。そもそもPerlの国際化サポートということ自体が,日本語Windowsのためでなかったのだ。Internationalization(国際化)とは汎用的なインターフェースの構築である一方,Localization(地域化)は特定の地域向けの特別なインタフェースの構築なのだ。従って,日本語Windowsに対して,Localizationの方がプラットフォームに対して特別な配慮をするものだから,使いやすいのは当然なのである。
UTF8が標準のLinuxであれば,Perl 5.8/5.10は非常によいツールだと思う。
しかし,開発言語は,使用されるプラットフォーム(OS)に対して最適なものを提供すべきで,言語使用を優先させるべきではないのだ。日本語Windowsでは,ShiftJISをうまく扱えるプログラミング言語が必要なんだ。

実際,JPerlであれば,grepdir.plのまま,"grepdir.pl 表 dt"が実行できるのだ。JPerlの方がむしろ移植性の高いプログラムができていたのだ。

PerlのUTF8のサポートで,「Perlで移植性の高いプログラムが書ける」,「1つのスクリプトを書けば,PerlとJPerlの2つの環境でテストしなくてもすむ」と思っていた。PerlIOのトリックは,非常に目を見張るものだった。これなら,ほとんど文字コードを意識する必要がない。「小飼 弾氏は,すごい。天才だ。」そう思っていた。

UTF8を使いこなすために,私は努力した。数日も経つと自分の意図通りに動作するかどうかわからなくなったプログラムを見ても,自分の知識不足でPerl5.8の機能を使いこなせていないための結果と思い,日本語を処理するために,どう工夫すれば良いのか探っていた。汎用的なインタフェースや国際化機能がとても魅力的だと思っていたからだ。それが地域固有の問題(Localization)を解決するものだと信じていたからだ。ShiftJISは嫌いだし,文字コードを意識するなど面倒でしかなかったが,それが無くなると思っていたかった。

しかし,それは私の認識違いでしかなかった。信じていたかった機能は無く,コピー&ペーストで切り貼りしたコードは動作せず,encode()/decode()がスクリプトの中で目立つようになり,Dump()の出力でで実行結果は埋められていった。

私は,Windowsを主に使っており,OSの頃からMSのOSを使い続けていて,HDDとかDVDのバックアップにShiftJISのファイル名が大量にたまっているのだ。だから,ShiftJISとはしばらく付き合わなければならない。
ファイルの整理に,Perlを使っていたし,インデックスの作成には欠かせない道具であったんだが,以前のコードがどう動くのかわからなくなっていた。

では,どうすべきか。運用でカバーするか。英数字のフォルダ名やファイル名のみとすることは可能だが,もう日本語のファイル名は大量に使っているものだし,何のために制限の不便さを受け入れなければならないのか。
JPerlを引き出すか。しかし,進化の止まったモノに魅力は感じないが,それもありだろう。
今のPerlにJPerl化するか。やり方はいろいろあるし,実現できる手段もあるだろうが,それに時間を割く価値はあるのか。Perl6が次に出てくるというのに。
もしかしたら,次のバージョンではもっと使いやすくなるかもしれない。だが,元々の発想が,「日本語Windows向け」ということがないのだから,期待をすべきではないだろう。
もうPerlにこだわる必要がないじゃないか。

私は,rubyの本を買って勉強し始めた。

Comments