CGIのutf-8改造で文字化けしたときの処方箋

2010.11

... そんなあなたへの処方箋   ※修正例は後半に

※use utf8 命令を使うにはPerlのバージョンが5.8以降であることが必要です

0. 序論/そもそも use utf8; は必要か

理由は1で説明しますが substr split などの文字列関数やテキストマッチングがシビアに使われていなければ、不要です

つまり簡易な掲示板CGI程度であれば、use utf8せずに、CGIスクリプトの文字コードをUTF-8Nに変更するだけで UTF-8 で読み書きできる掲示板になります。

※HTMLヘッダやHTTPヘッダを適切に書き換える作業は use utf8; を書かない場合でも別途必要です。→7. 基本的な補足
※Jcode等、日本語文字コードの相互変換を含むものについては未確認です。あくまでUTF-8のみで読み書きする場合です。

以下、本題 ---

1. use utf8; の意味

use utf8 とは、 このCGIスクリプト内で扱う全角文字は文字コードがUTF-8であり、且つ、扱う全角文字データのすべてにutf8フラグ(目印みたいなもの/以下単に「フラグ」と呼称)を付けてあるのでそのつもりで処理してね、とPerl側に教えてやる宣言のこと

この命令を宣言しておくことで、Perl側が substr、split、length等々の文字列を扱う関数やテキストマッチングを、そのフラグを目印に文字の境界を正しく判別して処理するようになる。 例えば a をともに正しく1文字と認識して処理できるようになる。

例: print substr("abあいcうdえお",3,5) → 「いcうdえ」

⇔逆に、use utf8; 宣言をしているのにもかかわらず、フラグの付いていない文字列を渡すと、(あるはずのものがないので) 文字の境界を正しく判別できずに処理を行ってしまい、文字化けが発生する。

2. use utf8 宣言した場合に文字化けのおこるポイント

a) フラグのない全角文字列を、文字列を扱う関数に通すと文字化けする。(∵文字の境目を正しく認識して処理できない為)

b) フラグはPerlスクリプトの中でだけ意味をもつ(ある意味)ゴミなので、表示時(print文発行時)にはフラグを剥がしておかないと文字化けする。(文字化けに混じって Wide character in print at ~ 云々というエラーが出る時もある)

3. 文字化けを防ぐために必要な処理

a) 外部から読み込んだ全角文字列にはフラグが付いていないので utf8::decode($str) 関数を通すことによってフラグを付けてやる必要がある。外部からの文字列とは、テキストファイルやデータベースから読み込む文字列、ブラウザのフォームから飛んでくる文字列などのこと。

 ⇔スクリプト内部で定義する文字列には自動的にutf8フラグが付くので上記のような処理は不要 (例:$str="あiuえお"; ← これだけでフラグもつく)

b) 2.のbで説明したようにアウトプット時にはフラグを剥がす必要がある

 ※文字列にフラグが付いているかどうかの確認は utf8::is_utf8($str) 関数で判別できます (フラグon/off → 戻り値が真/偽)

4. 具体的な手順

a) テキストファイルやデータベースから全角文字を含む文字列を読み込むときは、読み込んだ直後に utf8::decode 関数を通してフラグを付けてやる。(3.のa)

b) 掲示板など、<form>から来た全角文字列は uri escape(%E6%97%A5%E6%9C%AC%E8%AA%9E みたいな文字列)されているので、まずそれを uri unescape して元の全角文字列に戻してから、上記関数でフラグを付けてやる。クッキーも同様。(uri unescape する前にフラグを付けても意味がないことに注意)

c) アウトプット時は、その直前に個別に utf8::encode 関数を通してフラグを剥がす。 もしくは、use utf8; の次行に binmode(STDOUT, ":utf8"); と書いておけば自動的にフラグを剥がしてくれる*1。(後者の方が便利です)

*1補足:c) クッキーについて (2011.8)
「binmode(STDOUT, ":utf8");と書いておけば自動的にフラグを剥がしてくれる」のは通常の出力に限ります。ここで注意を要するのはクッキーへの書き込みです。 クッキーには uri escape 処理した文字列を書き込みますが、このままだとフラグ(ゴミ)込みで uri escape されてしまいます。 これを避けるには、先に utf8::encode を通してから uri escape してください。 掲示板などでハンドルネームをクッキーに記憶させているような場合に注意が必要。

5. 以上簡単にまとめると

a) CGIスクリプト自体の文字コードを UTF-8N で保存する。これで問題が出なければ無理に use utf8; する必要はない。

b) 各CGIスクリプト冒頭に use utf8; と binmode(STDOUT, ":utf8"); を書く (複数のスクリプトをrequireしてる場合はそのすべてに書く)

c) 外部から読み込んだ全角文字列には、早い段階で utf8::decode() 関数を通してフラグを付けておく。

d) クッキーへの書き込みなど若干の例外処理が必要な場合がある。

6. 修正例

赤字青字がuse utf8のために追加した部分
---
#!/usr/bin/perl
use utf8;				# utf8モードを使うという宣言をする(4.c)
binmode(STDOUT, ":utf8");		# 自動でフラグを剥がすという指示(4.c)


## スクリプト内部で文字列を定義する場合
$str = 'あiuえお';			# 自動的にフラグの付いた文字列が入るので特別な処理は不要 (3.a)


## テキストファイルから文字列データを読み込む場合
open(READ,"<in.txt");
while(<READ>) { $buf .= $_ };
close(READ);
utf8::decode($buf);			# ファイルから読み込んだあと、他の処理に入る前にutf8フラグを付ける (4.a)

## <form>から飛んできた文字列の処理
if ($ENV{'REQUEST_METHOD'} eq "POST")
	{ read(STDIN, $buf, $ENV{'CONTENT_LENGTH'}); }
  else	{ $buf = $ENV{'QUERY_STRING'}; }

foreach ( split(/&/, $buf) ) {
    ($key, $val) = split(/=/);
    $val =~ tr/+/ /;
    $val =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack('H2', $1)/eg;	# uri escape されたものを元に戻して (4.b)
    utf8::decode($val);			# utf8フラグを付けてやる (4.b)
    $in{$key} = $val;
}

## テキストファイルへ書き込みする場合
open WRITE, ">out.txt";
print WRITE $buf;			# 何もせずそのまま書き込む ∵binmode(STDOUT, ":utf8");してある (4.c)
close WRITE;


## クッキーからの読み込み
foreach (split(/;/, $ENV{'HTTP_COOKIE'})) {
    my ($key, $val) = split(/=/);
    $val =~ tr/+/ /;
    $val =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack('H2', $1)/eg;	# uri escape されたものを元に戻してから (4.b)
    utf8::decode($val);			# utf8フラグを付けてやる (4.b)
    $cook{$key} = $val;
}

## クッキーへの書き込み
utf8::encode($str);			# フラグを剥がしてから(4.c補足)
$str =~ s/(\W)/'%'.unpack('H2', $1)/eg;	# uri escape する
$str =~ tr/ /+/;
print "Content-Type: text/html\n$str";		# クッキーに書き込み(httpヘッダの場合)
---

7. 基本的な補足

CGIスクリプトの文字コードをUTF-8Nに変更する場合、この書き換えは必須。(use utf8; 宣言をするしないに関わらず)

a) HTMLヘッダ部分の変更:

<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS"> などから
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> に変更する必要がある。

b) HTTPヘッダ部分の変更:

print "Content-Type: text/html;\n" などになっている場合は変更不要ですが
print "Content-Type: text/html; charset=Shift_JIS\n" などになっている場合は
print "Content-Type: text/html; charset=UTF-8\n" への変更が必要。

8. その他ヒント

「use utf8 全角チルダ」「use utf8 jcode」あたりで検索すると何か見つかるかも