Windows上のPerlで日本語を扱う
2025/01/13
Perlを使い始めた頃、マルチバイト文字はデータに無かった
  • Perlを使い始めたのは2003年以降...回路設計データの網羅的処理と自動生成を目的に使い始めました。OSもSunOS, Solaris, LinuxとUNIX系だったため、2byte文字は全く含まれていないという状況でした。

  • スクリプトソースだけで動かせるPerlは、CVS等Version管理ツールとの相性も良く、とても便利な言語だと感じました。CPANのライブラリを使えることもあり、すぐにメイン言語をC++からPerlへ変えたことを思い出します。

  • ここまで見るとPerlは文句を付け難い言語ですが、その生い立ちから言って、マルチバイト文字を含むデータの処理は後付けなせいか扱いが混乱気味(*1)です。

  • ネットで検索するといろいろ出てきますが、Perlのメイン戦場はWebサーバー(大体Linux)のため、そもそもSTDIN/STDOUTがCP932(Shift_JIS)のWindows上で「どうすべきか」を説明しているサイトは少ないように思えます。「やり方は一つじゃない」ですが、さすがにちょっと探しにくい...。

  • そんな訳でちょっとまとめてみることにしました。下記がまとめです。以降のセクションは説明になります。
    • スクリプト文字コードはUTF-8にする
    • スクリプトには use utf8; プラグマを置く
    • STDOUT,STDERREncode::encode(...) で変換
    • ファイル出力は open(ファイルハンドル, ">:encoding(文字コード)",..) で変換
    • ファイル入力は Encode::Guess::guess_encoding(データ) で判定後 Encode::decode(...) で変換

マルチバイトの適切な処理にはPerl内部文字列への変換が必須
  • Perlでマルチバイト文字を処理するには、Perl内部文字列への変換が必須です。これはスクリプト内で記述している文字列も対象であり、変換しないと正規表現による抽出等がまともに動作しません。

  • 内部文字列変換にはEncode::decode()を使用しますが、スクリプト内部の文字列ですら変換が必要なのは面倒過ぎますね。そのための救済措置として use utf8; プラグマがあります。

  • UTF-8の文字コードで記述されたスクリプトに、use utf8; を置くと、Perlパーサーはスクリプト内の文字列をPerl内部文字列に自動変換してくれます。するとマルチバイト文字を利用した正規表現文字処理もうまくいくようになります。
  • #!perl -w
    use utf8;
    
    my $str = '0123456日本語abcdefg';
    $str =~ s/日本語/==2byte==/;
    print $str,"\n";
    
    >test.pl
    0123456==2byte==abcdefg
    
  • スクリプト文字コードをUTF-8にするだけでうまくいくケースもあり(誤解に繋がりやすい)ですが、たまたま問題が出なかっただけです。異なる文字列データで正常に動作するかはわかりません。UTF-8で記述したスクリプトには use utf8; が必須なのです。

STDOUT,STDERRの処理
  • WindowsのSTDOUT(標準出力),STDERR(標準エラー出力)は文字コードがCP932なので、UTF-8文字コードのPerlスクリプトから直接マルチバイト文字を出力すると当然文字化けします。

  • この対策として、スクリプト冒頭部で「binmode STDOUT,':encoding(cp932)'; を記述する」等がよく紹介されていますが私は推奨しません。Encode::encode() を利用すべきです。

  • この理由ですが、binmodeは重ね効きしてしまう(*2)からです。あるスクリプトファイルでbinmode STDOUTを設定後、別のファイルで再度binmode STDOUTを設定すると(してしまうと)それも効いてしまい、多重変換で壊れるからです。

  • そうなってくると、出力時に変換する方がコード管理としては楽になるのですが、非常に面倒なので、標準出力/標準エラー出力関数を作るのが提案です。関数名はC++を参考にして下記のようなcout(), cerr()等どうでしょうか。引数の置き方は括弧の有無を含めて、print と同じです。
  • sub cout {
        my @list = @_;
        
        foreach my $buf (@list) {
            my $buf_cp = Encode::encode('cp932', $buf);
            print STDOUT $buf_cp;
        }
    }
    
    sub cerr {
        my @list = @_;
        
        foreach my $buf (@list) {
            my $buf_cp = Encode::encode('cp932', $buf);
            print STDERR $buf_cp;
        }
    }
    
  • 尚標準入力(STDIN)も Encode::decode()すれば良いですが、コンソール入力って...滅多に使わないですよね。

ファイルハンドルの処理
  • ファイル出力は、open関数の第2引数として :encoding(文字コード) を指定することができます。
  • my $fh; # ファイルハンドル
    open($fh, ">:encoding(文字コード)", ファイル名); # 出力
    
  • ファイル入力ですが...ファイルの文字コードが既知の場合は良いのですが、Windows 10以降では文字コードが混在する傾向にあります。Windows 10のあるRevisionから、メモ帳(notepad)のデフォルト文字コードがUTF-8になったからです。

  • つまり...読んでから判定する必要があります。最初にファイルから読む際は、変換せずに "<:raw"で読み、その結果を推定してからデコードする流れです。常に文字コード推定入るので面倒です...仕方無いですけど。これも関数化推奨です。
  • my $fh; # ファイルハンドル
    open($fh, "<:raw", ファイル名) or die;
        my @src_raw = <$fh>; # とりあえずrawデータを取り込む
    close($fh);
    
    my $str_raw = join('', @src_raw); # 結合文字列化
    
    my $o_guess = Encode::Guess::guess_encoding($str_raw); # 結合文字列から文字コード推定
    
    my @src; # 内部文字列変換データを保持するリスト
    for (my $i=0; $i<@src_raw; $i++) {
        $src[$i] = Encode::decode($o_guess->name(), $src_raw[$i]);
    }
    
  • つまり、Windows上でテキストファイルを処理するには、文字コード情報とセットで扱う必要があるのです。Windowsで日本語データをプログラムから扱う場合、入門者にとって大きな壁となるでしょう。

日本語処理は難しい
  • Perlに限らず、どんなプログラミング言語でも日本語の処理は入門者+αレベルの知識を要求されます。

  • とりあえず標準入出力をどうにかしているPythonは「さすが」と思いましたが、プログラミング入門者に「文字コードはUTF-8にして下さい」と話すのは、プログラミングの本質から離れすぎていて難しいと感じます。

  • 少しコードを書けるようになって、いざ実際の(日本語を含む)データを扱ったら、文字化けしてわけわからない結果になる訳で、そりゃ心が折れますよね。英語以外の言語の影響はこんなところでも出てくるんだなと思わされました。
Notes
  • Python等後発の言語は、このあたり最初から方法論が確立されています。
  • こちらのサイトに説明がありました。
Copyright(C) 2025 Altmo
本HPについて