ページ間での文字化けを解消する

(1/1)
GETPOST で渡されたデータを処理する際、その中に日本語が入っている場合に文字化けを起こす場合がある。自分の環境で文字化けが起きなくても、異なるブラウザを利用していたり、異なるWebサーバにPHPスクリプトを移行したときに文字化けが発生することがある。
なぜそのようなことが起きるのか、原因と解決策をさぐる。

(2021年5月8日)PHP8対応

目次

典型的な文字化け

まずは上記からPHPスクリプトをダウンロードし、実行してみてほしい。ただし、セキュリティ対策が不十分なので、ローカルな環境で実行すること。

プログラムを実行すると、エンコード・タイプを3種類選べるようになっている。まず、一番上の UTF-8 を選択し、「Input Text」に何か日本語文字を入力し、[send] ボタンを押下してほしい。
「POST is passed」に表示される文字は化けていないだろうか。また、「Auto Converted」に表示される文字はどうだろうか。

次に、エンコード・タイプを、EUC-JP, Shift-JIS に切り換えて同様な操作をしてみてほしい。すべてのケースで文字化けが起きない環境というのは無いだろう。
また、もし複数のブラウザを利用できたり、異なるPHPサーバ環境が利用できるのであれば、それらでも試してみてほしい。環境が変わると、文字化けのケースも変化することを見ることができるだろう。

サンプル・プログラムの解説

0053:     $outstr1 = $_POST['param'];
0054:     $outstr2 = mb_convert_encoding($_POST['param'], $EncodingTable[$InternalEncoding][2], 'auto');

表示処理の関係で長くなっているが、このプログラムのコアは上記の部分である。
つまり、POSTで受け取った変数をそのまま表示($outstr1)と、関数  mb_convert_encoding  で自動変換して表示($outstr2)するだけである。$outstr1 が「POST is passed」に、$outstr2が「Auto Converted」に表示される。

HTMLフォームからのデータをPOST変数で受けるようなPHPスクリプトを書く場合、$outstr1 を処理対象にするのはNGである。ここで見てきたように、文字化けする可能性が高いからだ。
では、$outstr2 なら安全かというと、これでも不十分である。たとえば「Input Text」に「十分な機能」という日本語を入力すると、円マーク「¥」が付加されてしまう環境がある。(⇒「バックスラッシュと円マーク」)
この場合、関数  stripslashes  で円マークを取り除くのが定石なのだが、エンコードが Shift-JIS以外の場合には不具合が発生する。

なぜ、このような文字化けが発生するのか――じつは、複数の要因が絡んでいる。まずは要因を切り分けながら、各々の問題を明らかにしよう。

文字化けの原因と対策:PHPサーバ環境に依存するもの

まず、関数  phpinfo  を実行し、自サーバの環境を見てほしい。その中に、mbstring directive の一覧表があるはずだ。
ここで注目してほしいのは、
  • mbstring.http_input
  • mbstring.internal_encoding
の2つである。(⇒「HTTP入出力」)

POSTで渡されたデータは、まず mbstring.http_input に従ってデコードされる。
もし、mbstring.http_input の値が auto なら、自動でデコードされる。ただし、auto は万能ではないので、エンコード・タイプを間違えて文字化けを起こす場合がある。

mbstring.http_input の値が pass なら、デコードは行われない。この場合、POSTで渡されたエンコード・タイプと mbstring.internal_encoding で示されるエンコード・タイプが異なると、文字化けの原因となる。
mbstring.http_input の値が UTF-8, EUC-JP, SJIS などの具体的なエンコード・タイプであれば、明示的にこれらのタイプにデコードされる。しかし、mbstring.internal_encoding と一致していなければ、文字化けの原因となる。

こうしたことを考えると、mbstring.http_input の値は pass にしておき、スクリプト側でコード変換( mb_convert_encoding )を行うのが無難な設定だろう。この場合、送られてくる POST変数のエンコード・タイプが明らかでなければならない。関数  mb_convert_encoding  で変換元に auto を指定することは、mbstring.http_input に auto を設定することと同義であり、前述のように auto は万能ではないからだ。

先のサンプル・プログラムでは、じつは、HTMLフォームとPHPスクリプトのエンコード・タイプは一致させてある。だから、関数  mb_convert_encoding  で明示的にコード変換することは容易である。
しかし、POST変数に不正なデータを入れて攻撃を仕掛けてくるケースもあり、文字化けの原因になったり、セキュリティ・ホールになる可能性がある。そのため、冒頭で、このサンプル・プログラムを「ローカルな環境で実行すること」と注意したのである。

すなわち、PHPサーバ環境に依存する文字化け対策は、以下の通りとなる。
  1. mbstring.http_input を pass にする
  2. mbstring.internal_encoding を明示する
  3. POST渡しする側のHTMLフォームをmbstring.internal_encodingに合わせる
  4. 受け取ったPHPスクリプト側では  mb_convert_encoding  でコード変換するとともに、不正なデータでないかどうか検証する。

文字化けの原因と対策:ブラウザ環境に依存するもの

HTML4では、formタグに属性として accept-charset を指定することで、サーバ側のエンコード・タイプに合わせてデータを送ってくれるという便利な機能がある。この機能を使えば、何らかの事情でHTMLフォームとPHPスクリプトのエンコード・タイプが異なっていたとしても、うまく文字化けしないでデータを渡すことができるはずである。
ところが、この仕様に対応しているブラウザとそうでないものがある。手元にある Windows用InternetExplorer 6/7 は対応していなかった。他の Windows/MacOS/Linux 用の FireFox, Safari, Opera は対応している。
したがって、HTMLフォームにaccept-charset属性を用いた場合、ブラウザによって文字化けが発生する可能性がある。
上記のプログラムを実行すると、「Select InternalEncode」と「Select InputEncode」の2つのエンコード・タイプを選ぶよう指示される。前者は、最初のサンプル・プログラム(httpparam1.php)と同様、HTMLフォーム及びPHPの内部処理エンコード・タイプを指定するものである。後者は、accept-charset属性を指定するものである。
「Input Text」に何か日本語文字を入力し、[send] ボタンを押下すると、「live POST parameter」「POST is passed」「Auto Converted」の3種類が表示される。「live POST parameter」は、POSTに渡されたデータをそのまま表示している。後2者は、最初のサンプル・プログラム(httpparam1.php)と同じ内容である。

「Select InternalEncode」と「Select InputEncode」が同じである場合は、どのブラウザでも「live POST parameter」の結果は同じになる。
ところが、たとえばSelect InternalEncodeにUTF-8を、Select InputEncodeにShift-JISを指定した場合は、InternetExplorerだけ結果が異なる。
結果的にAuto Convertedは文字化けしないで済んでいると思うが、PHPに渡されているデータが異なるというのは、あまりよろしくない状況である。

結局、HTMLフォームではaccept-charset属性は指定しない方がいいだろう。

サンプル・プログラムの解説

php://input からPOSTの生データを読み込むことができる。

そこで、変数 $_POST を利用せず、自力でPOST変数を解析して配列として返すようにしたのがユーザー関数 parsePOST である。

0041: /**
0042:  * original parsing POST parameter
0043:  * @return array : parameter
0044: */
0045: function parsePOST() {
0046:     $param = file_get_contents("php://input");       //get POST parameter
0047:     $arr1 = preg_split('/\&/i', $param);
0048:     foreach ($arr1 as $val) {
0049:         $arr2 = preg_split('/\=/i', $val);
0050:         $arr[$arr2[0]] = $arr2[1];
0051:     }
0052: 
0053:     return $arr;
0054: }

enctypeを使った解決策

HTML4では、formタグに属性として enctype を指定することで、データを添付ファイルとして送信することができる。ここで enctype="multipart/form-data" を指定すると、エンコード・セットを含めてデータを送信することができるので、PHPスクリプト側で文字化けが発生することはない。

先のサンプル・プログラム(httpparam2.php)で用いたすべてのブラウザで enctype は有効に動作していることが確認できたので、この対策は効果的だといえる。

GET変数の場合

ここでは GET 変数による文字化け問題について触れてこなかったが、POST変数と同様、文字化けする場合がある。そこで、enctype="multipart/form-data" 指定で解決できるかというと、これが微妙なのである。
HTML4の仕様では enctype="multipart/form-data" が有効なのは method=POST の場合のみとしているが、ブラウザによって実装されていたりする。
したがって、GET変数まで含めて文字化け対策を考えようとすると、enctype="multipart/form-data" は上策とは言えない。

より効果的な対策法

原始的ではあるが、POST/GET で渡す変数にエンコード・タイプも持たせてやるというのが一番の解決策といえる。
実際、Yahoo!JAPAN の検索では、"ei=UTF-8" のようにしてエンコード・タイプを渡している。

ここまでの対策法を整理すると、下記のようになる。

【HTMLフォーム】
  1. accept-charset属性を指定しない。
  2. enctype="multipart/form-data"を指定しない。
  3. エンコード・タイプを変数として渡す。(例:ei=UTF-8)
【サーバサイド(PHP)】
  1. mbstring.http_input を pass にする
  2. mbstring.internal_encoding を明示する
  3. POST渡しする側のHTMLフォームをmbstring.internal_encodingに合わせる
  4. 受け取ったPHPスクリプト側では、エンコード・タイプを元に、 mb_convert_encoding  でコード変換するとともに、不正なデータでないかどうか検証する。

参考サイト

(この項おわり)
header