Home PerlでUTF8文字コード使用する際の文字化け問題と解決方法

PerlでUTF8文字コード使用する際の文字化け問題と解決方法

●● 改訂追記 (2016/2/9) ●●
①~⑦で現時点で明瞭かつ確言できる注意点留意点をメモ書きしておく。
⑧以下の旧版にグダグダ書き綴った事柄は経験的ではあるけれど誤りを含む未整理なメモ。特に画面(web)出力をdecode文字列のみとせよ。という記述は誤りである。

●● 問題を解決する際の最善の方法 ●●

①パッケージ利用宣言(use)で以下を宣言する

    use   Encode;
    use   utf8;

・スクリプトを記述するファイルは文字コードを UTF8 で指定し保存されるべきである。

②入力データはふつうencode文字列であるので内部処理前に必ず decode 処理を行う。
(例) my $myDeKey  = decode_utf8($myEnKey);        #入力文字列 $myEnKeyをデコードして$myDeKeyに保存する。

③出力データはふつうencode文字列とすべきであるから出力前に必ず encode 処理を行う。
(例) my $myEnKey  = encode_utf8($myDeKey);        #内部処理文字列 $myDeKeyをエンコードして$myEnKeyに保存する。

④スクリプトの内部処理はすべてdecode文字列に統一し処理する。煩雑ではあるが出力はすべて encode 文字列に変換した後に行う。

・スクリプト内で定義される定数文字列はすべてdecode文字列である。
 ・文字化けはencode/decode文字列の混在出力が原因である。だからdecode/encode文字列を不注意に混在連結させてはならない

・内部処理途中のdecode文字列のまま表示orファイル出力しない。必ずencodeし直したあとで出力する。

 ・間に合わせ的にとりあえず出力した文字列がブラウザで正常表示されたとしても以下の潜在的問題があることを忘れてはならない。
  ・ssh端末でdecode文字列を表示させるとWide文字列に遭遇したという警告が多数表示され結果、狭い表示枠がノイジーで読み辛くなる。
  ・apache HTTPサーバのログがssh端末同様にWide文字列使用の警告で埋め尽くされる。無駄の極み。

・内部処理途中のhashはdecode文字列を使用する。decode未処理=encode文字列に対する文字列操作こそが文字化けの原因。
 ・UTF8では純粋なASCIIだけの文字列はdecode/encode処理と無関係であるかのごとく文字化けしない。理由はそれが1BYTEコードであるから。
 ・ただしtie付けされたhashで decode文字列をキー使用すると日本語コード(3-byte)が原因でクラッシュする。

・CPAN由来の外部ファイルを独自に読み込むようなPackageは処理結果をencode文字列として返すことが多い。


⑤混乱を避けるために encode / decode 文字列それぞれの区分を明瞭明確に示す変数記法を定義使用する。

・もちろんスクリプトの記法は好みの問題であるから任意・マイルールでかまわない。
・私はEncodeされた文字列は $myEnxxxxxxと記述し Decodeされた内部処理変数を $myDexxxxxxxxと記述することを常としている。

⑥ encode / decode は多用しない

・encode / decode 処理は意外とオーバーヘッドが大きい。処理ループでこまめに多用すると処理速度が遅くなる。
・処理の遅速化を回避するために encode / decode 処理は入力と出力の時に一括して一回限りで済ませることが望ましい。

⑦サンプル・コード

csvファイルを読み込みDBハッシュに登録保存するコードのサンプル例。

  use Encode;
  use utf8;
 
  my ($myEnReadData, $myEnReadData, $myDeKey, $myDeKanaData, $myEnKanaData, $myDeLcKey, $myEnLcKey);
 
  tie %MyHash,DB_File, $gDBFilePath, O_RDWR|O_CREATE, 0666, $DB_HASH;##データベースファイルを新規作成する
  if (!open(FILE,$gFilePath)) { print "file open error : $gFilePath"; exit; } ## タブ切りcsvファイルをオープンする
  @gData = <FILE>;                       ## ファイル内容を一気に配列に読み込む
  close  FILE;                          ## データファイルを閉じる
  foreach $myEnReadData (@gData)
  {
    $myDeReadData      = decode_utf8($myEnReadData);    ## csvファイルの行データを一括デコードする
    ($myDeKey, $myDeKanaData ) = split(/\t/,$myDeReadData);    ## csvデータをカラム単位で抜き取る
    $myDeLcKey       = lc($myDeKey);            ## キーデータを小文字統一する
    $myDeKanaData    =~ tr/ァ-ン/ぁ-ん/;          ## データのカタカナをひらがなに変換する
    $myEnLcKey        = encode_utf8($myDeLcKey);      ## キーをエンコードする
    $myEnKanaData    = encode_utf8($myDeKanaData);  ## データーをエンコードする
    $myDeMessage    = "キーワード:$myDeLcKey データ:$myDeKanaData<BR>\n"; ## メッセージ・データーを構成する
    $myEnMessage    = encode_utf8($myDeMessage);  ## メッセージをエンコードする
    print "$myEnMessage";    ## エンコードされたメッセージを出力する
    $MyHash{$myEnLcKey}    = $myEnKanaData;        ## dbデータベースファイルに記録する
  }
  untie %MyHash;                          ## dbデータベースをフラッシュし保存終了する
  chmod   0666, $gDBFilePath;                    ## ファイル属性を666とする



⑧以下旧版記述

■■ 前書き ■■
大手サイトのホームページでも当初はシフトJISやEUCで記述されるのが普通だったが、今ではほとんどがUTF8に移行している。
10年も前の5.0.x + jcode.pl の頃と違って今時のPerlはUnicode UTF8に対応している。
Apacheベースのperl-CGIを記述する際、「UTF8/LFのみ」でコード保存すればUTF8の移行は単純に完了する。
今時のperl CGI記述は、UTF8で記述保存するのがふつうだろう。そしてjcode.plはもはや過去の遺物。
Encodeモジュールが一般化している現在ではperlは5.8以降が標準で use Encode; use utf8が普通に使える。

以下はversion 5.8.8で検証確認した現象と対策。perlの新バージョンや将来の改版で事態が異なることが起こるかもしれません。


■■ use Encode; と use utf8; の使い分け ■■
use Encodeだけのコードで、substr や tr などの標準perl関数を用い任意の文字列を抜き出したり置換したりすると文字化けなど問題がドカッと現れてくる。


■■ 問題の現れ方は、"use utf8"の有無で大きく異なる。 ■■
単純に use Encode; だけで済ませ標準関数を利用しようとすると、指定した文字列で切り出しや変換が上手くできない。
これはふつうunicode UTF8文字は1文字3バイト構成なので、標準関数は文字 UTF8の文字1個を3個の文字とみなして処理を行ってしまうことに起因する。
文字列は英数字混在であり得るから、単純に1文字3バイトで指定するようなみなし処理は解決にはならない。
substrで1文字切り出しが1バイト切り出しと同義であるような場合、文字化けというより単にデータとは言えないゴミを表示しているだけに過ぎない。
それにそもそも特殊文字は3バイトではない場合もありえる。


■■ 文字操作のない単純なCGIなら ■■
文字列を文字単位で置換したり抜き出したりしないのであれば、use utf8; 指定せず利用しない方がよい。
use utf8; 指定しなければ文字化けは生じない。だから単純なCGIならプレーンな記述で十分。


■■ どうしても文字操作が必須なCGIなら ■■
文字操作が多重的で頻繁であるような場合、substrやtrなどの標準関数を用いたいのであれば、use utf8 を記述したほうがよい。


■■ use utf8無用のプレーン記述にこだわる ■■
use utf8; をメインソースで用いないプレーン記述こだわる手法もある。
それは標準関数を decode_utf8 や encode_utf8 と組み合わせてラップ関数を書いて使用局面でだけフラグの付け外しを行うという手法である。
この場合、ラップ関数は別ファイルで記述し require文でインクルードする。そのファイルでは use utf8を使用する。
このよな実装では foreach文の中で文字処理をループさせたりすると、ラップ関数それ自体が無駄な処理を多重生成する。といった欠点・無駄が生じる場合もある。


■■ UTF8の2つのモード(文字化けの最大原因) ■■
use utf8; 指定をしたことで生じるUTF8文字化けは、perlがUTF8を扱うにあたって、2つのモードを使い分けていることによる。
そのモードの1つはperl固有の内部フラグ有りflagged/デコーデッドなUTF8文字列。もう一つは外部データ的な内部フラグ無しunflagged/エンコーデッドなUTF8文字列。
use utf8; 指定を行わず文字処理で標準関数を用いたりしなければ表示上文字化けは生じない。use utf8; を指定すれば、標準関数でUTF8文字列を指定通り正しく処理できる。
この標準関数が利用できるという利点を享受するためには、UTF8文字列はdecode_utf8によるフラグ有り flagged UTF8/デコーデッド文字列に(decode)変換することが必須。


■■ UTF8のモードの切り替え ■■

$myFlaggedDecodedWord = decode_utf8($myUnflaggedWord);    (UTF8文字列を標準関数でも使えるperl内部処理用にするフラグを付ける decode 変換を行う)

$myUnflaggedEncodedWord = encode_utf8($myFlaggedWord);    (UTF8文字列を外部ファイルやDBに書き出してもふつうに読めるフラグ無し・外しのencode変換を行う)

ここではフラグ付加がdecode(デコード)。フラグ外しがencode(エンコード)である。
わたし的な語感では、加工変換がエンコードで解除変換がデコードという語感なのだが、真逆。戸惑いを増やすポイント。

■■ 埋め込みリテラル文字列の扱い1 ■■
perlには flagged UTF8 と unflagged UTF8 の二つのモードがある。たとえば、

my $myData = "あいうえお";

と書いた場合、文字列変数の$myDataのフラグはどうなっているのだろうか?

①use Encode; のみが指定されたコードでは、この $myData はフラグなし。unflagged/エンコーデッドな変数。
②use utf8; が指定されたソースコードで記述された $myData はフラグ付き。 flagged/デコーデッドと見なされる。

この①と②の扱いの違いが混乱の原因のひとつ。たとえば単純に use utf8; 記述を追加すると今まで動いていたコードも動作がヘンになる。

■■ print文の埋め込みリテラル文字列の扱い2 ■■
print文のUTF8文字列の扱いは少々複雑である。

①print文の引数が単独文字列であれば、フラグありなしに関わらず正しく表示する。
②print文の引数がフラグありなし文字列が結合・混在する複文字列の場合、フラグなし unflagged/エンコーデッドな文字列が文字化けする。
③print文の文字列に英文字の埋め込み文字定数を含む場合、フラグの有無に関わらず文字化けは生じない。
④print文の文字列に日本語などUTF8の埋め込み文字定数を含む場合、フラグなしのunflagged/エンコーデッドな文字列が文字化けする。

①から④までの現象から、次のことが導き出せる。

[要点] use utf8; 指定でコードを書く際、フラグありのflagged/デコーデッドな文字列(変数)では文字化けしない。
    文字化けする文字列は大抵 フラグなし unflagged/エンコーデッドな文字列である。


従って、use utf8; 記述を利用し、標準関数でUTF8文字列を処理する場合は、
内部的な文字変数は全て "decode_utf8" 関数でフラグありのflagged変数に変換する。さもなければ文字化けの原因となる。
だから、use utf8; を用いていないプレーンなコードに単純にuse utf8; を付加しただけだと、
そもそもが unflagged UTF8文字列の処理前提で書かれているコードであるが故、大量の文字化けが生じる可能性が高い。

上述の文字化けが生じる際の文字コードの条件は、本ページの終部に付加したサンプルコードを実際に試していただければ検証可能。


■■ 文字列変数のUTF8フラグの有無を識別する簡易な方法 ■■
上述の①から④の現象から、任意の文字列変数に格納されている文字コードのUTF8のフラグの有無は
「あ」のような全角文字1文字を混在させることで簡単に判別できる。

 print "$myString";       ## 文字列変数単独では UTF8フラグの有無と無関係で正常表示される。
 print "あ $myString";      ## 全角文字が混在すると UTFフラグの無い unflagged な文字列であれば、必ず文字化けする。


■■ 外部ファイルのデータ ■■
外部ファイルのデータは全て、フラグなしのunflagged/エンコーデッドデータでなけばならない。
さもないと、テキストファイルでは文字化けだらけのデータであったり、
データベースであれば外部からアクセス不能なキーを有するDBとしたりして、問題をさらに厄介にする。
use utf8; 指定のないプレーンなコードでは全てがフラグなしのコードとして扱われるからこの外部ファイルデータ的な問題は一切生じない。
プレーンなコードこそが外部ファイルへの悪影響を最小にするので、use utf8; などを指定しないことが推奨されるべきである。

use utf8; なコードのフラグ有り内部処理用途のUTF8文字列はテキストファイル化すると文字化けるから、必ずencode_utf8変換してから出力すること。 逆に、外部ファイルから読み込まれるUTF8文字データはフラグ無しなので、早期にdecode_utf8関数でフラグ付けをすること。

(フラグ無しUTF8文字列もフラグ有りUTF8の文字列と混在させなければ正常表示する。これによって問題がさらに厄介に見えることがある。)


■■ 文字列化けするUTF8文字列はフラグ無しのundecoded/encodedなUTF文字列だけ! ■■
結局、文字列化けするUTF8文字列はフラグ無しのundecoded/encodedなUTF文字列だけ!なのである。
文字化けは外部ファイルやURLなどで受け取った文字列に含まれるデータなど、外部由来のUTF8文字列だけなのである。
内部処理的には、文字化けしない、decode_utf8でデコードされたフラグ有りのUTF8で統一しなければならない。


■■ URL埋め込みの文字はフラグ有り flagged UTF8のままでよい。 ■■
以下のように、URL埋め込みのUTF8(全角)文字列はエンコード不要である。
というか、ブラウザ画面上で文字化けしなければ正常。出力全てがフラグなしであればそれはそれで正常なリンクURLを構成する。

ウィキペディアで埋め込み文字列のままのフラグ有りUTF8「文字化け」を調べる
ウィキペディアでencode_ut8変換されたフラグ無しUTF8「文字化け」を混在したURLで調べる


■■ 結論 ■■

①文字処理で標準関数を使用する必要がなければ use utf8; 指定は不要である。
② use utf8; を使用する場合、文字列変数は全てフラグありUTF8とすること。
  外部読み込みデータを格納する文字列変数は decode_utf8を用いてUTF8の内部フラグ付けを徹底して行うこと。
③ use utf8; を使用する場合、外部ファイル、DBへ出力する場合は、encode_utf8でフラグ外しを行うこと。
④ use utf8; を使用する場合、文字列を画面表示する場合は、フラグ付けされた文字列のみを用いること。
⑤ use utf8; を使用する場合、文字列変数のフラグの有無が分からなくなったら、全角文字1文字を付加して表示させてみる。
  文字化けすれば、その文字列変数はフラグ無し。


■■ おまけ1 split関数について ■■

foreach文で多用される split関数は、\t (tab)や , などAscii記号で区切り解除する場合が多いので、
フラグのありなしとは無縁のまま正常動作することが普通。


■■ おまけ2 平仮名・カタカナ変換について ■■

以下のような「平仮名カタカナ」変換は use utf8; を使わないと面倒だろう。

my $myWord =~ (tr/ァ-ン/ぁ-ん/); ## ひらがな変換
my $myWord =~ (tr/ぁ-ん/ァ-ン/); ## カタカナ変換


■■ おまけ3 require文で読み込むライブラリについて ■■

require文で読み込むライブラリファイルに書かれてあるコードでは、
use utf8; はファイル毎にそれぞれ別途、指定しなければならない。
この点を失念していると埋め込み文字列のフラグの様態がファイル毎に異なることで文字化けする。
等々。またまた混乱の原因となる。かも。

■■ おまけ4 Wide character エラー(警告)について ■■

ssh端末ソフトなどでcgiモジュールを実行すると Wide character 警告が頻発することがある。

Wide character in print at test.cgi.utf8 line 1596.

この警告はuse utf8 指定されつつ同時にUTF-8コードで保存されたソースコードで発生する。
print文でリテラル文字列(埋め込み文字列)だけを表示させようとしても警告が出る。
use utf8指定がなければ警告は出ない。警告の原因は出力文字列がフラグ付きのperl内部的UTF8コードだからである。
このような警告を出したくないのであれば、encodeされた文字列だけprint文で出力すればよい。
ただし、encode_utf8処理が(Encode.pm内部エラーとかで)失敗するとスクリプトは簡単に異常終了するので要注意。

webサーバーのapacheではprint文の引数の文字コードがフラグ無しのみまたはフラグ有りのみであれば
文字化けなしで出力される。フラグの有り無し文字が混在した文字列を出力しようとした場合に文字化けが起こる。
ssh端末でもapache経由でも文字化けあるいは警告発生などがない無欠のuse utf8コードを望むのであれば、
print出力以前で encode(内部フラグ除去)する必要がある。
だけれども、それはコード量を無駄に増やしかつバグの原因でもあるから一長一短。
出力文字列をencode_utf8統一することは、必ずしも奨励されるべき手法ではない。かも。




■■ サンプルコード ■■

以下を実行してみると、UTF8文字列に文字化けが生じる文字列混在の条件が明瞭にわかるはず。お試しあれ。

use Encode;
use utf8;

sub test
{
$deMy1Key = "あア";
$deMy2Key = "いイ";
$deMy3Key = "うウ";
$deMy4Key = "えエ";

$enMy1Key = encode_utf8( $deMy1Key );
$enMy2Key = encode_utf8( $deMy2Key );
$enMy3Key = encode_utf8( $deMy3Key );
$enMy4Key = encode_utf8( $deMy4Key );

print "A $enMy1Key : $enMy2Key : $enMy3Key : $enMy4Key";
print "B $deMy1Key : $deMy2Key : $deMy3Key : $deMy4Key";
print "C $deMy1Key : $enMy2Key : $deMy3Key : $enMy4Key";
print "D $enMy1Key : $deMy2Key : $enMy3Key : $deMy4Key";
print "あ $enMy1Key : $enMy2Key : $enMy3Key : $enMy4Key";
print "い $deMy1Key : $deMy2Key : $deMy3Key : $deMy4Key";
print "う $deMy1Key : $enMy2Key : $deMy3Key : $enMy4Key";
print "え $enMy1Key : $deMy2Key : $enMy3Key : $deMy4Key";
print "Aあ $enMy1Key : $enMy2Key : $enMy3Key : $enMy4Key";
print "Bい $deMy1Key : $deMy2Key : $deMy3Key : $deMy4Key";
print "Cう $deMy1Key : $enMy2Key : $deMy3Key : $enMy4Key";
print "Dえ $enMy1Key : $deMy2Key : $enMy3Key : $deMy4Key";
}

実行結果

A あア : いイ : うウ : えエ
B あア : いイ : うウ : えエ
C あア : いイ : うウ : えエ
D あア : いイ : うウ : えエ
あ あア : いイ : うウ : えエ
い あア : いイ : うウ : えエ
う あア : いイ : うウ : えエ
え あア : いイ : うウ : えエ
Aあ あア : いイ : うウ : えエ
Bい あア : いイ : うウ : えエ
Cう あア : いイ : うウ : えエ
Dえ あア : いイ : うウ : えエ


Copyright(C) Satoshi Hanji 1997-2014. All Rights Reserved.