PHP の文字化けについて考える

SPECIAL


PHP での文字化けが発生

 CPI レンタルサーバー Shared Plan Business X20 SSL 上へ PHP スクリプトをアップロードしてみたところ、文字化けが発生してしまいました。

Web ページは UTF-8 で表示できるように設定し、PHP スクリプトや JavaScript 等も UTF-8 でエンコードして保存していたのですけど、PHP から出力される文字列部分が EUC-JP となってしまっていたため、UTF-8 と EUC-JP とが混在し、文字化けに至ってしまった様子です。

データベースでは UTF-8 にて保存するように CREATE TABLE 時に文字コードを指定してあったのですけど、そこへ記録される文字コードも EUC-JP になってしまっている感じです。

 

EUC-JP で Web ページもデータベースもそろえてしまえば、とりあえず問題が起こらないようになってはいるようです。ただ、それではあまりすっきりしないので、すべてを UTF-8 に統一するか、または最終的に HTML を正しい UTF-8 で出力できるように、調整を図ってみました。

 

PHP の内部で扱う文字コードを変換する

まず、PHP で文字コードを変換するための関数について調べてみたところ、主に次のような関数が用意されているようでした。

$result = mb_convert_encoding($value, $destinationCode, $sourceCode);
$result 文字コードが変換された文字列です。
$value 文字コードを変換したい文字列です。
$destinationCode 変換する文字コードです。"UTF-8" や "EUC-JP", "SJIS" といった形で指定します。
$sourceCode 変換元の文字列の文字コードです。省略した場合は自動的に文字コードが検出されます。また、"auto" を指定することで "ASCII,JIS,UTF-8,EUC-JP,SJIS" と指定されたのと同じに扱われるとのことでした。

 

$result = mb_detect_encoding($value);
$result 検出された文字コードです。
$value 文字コードを検出したい文字列です。

 

ただ、これらを利用して文字コードの変換を行おうとしても、なんだか上手く行きませんでした。

今思えば、文字コードの変換についてよくしらなかっただけかもしれないですけど、もしかするとサーバー側で mbstring が http_input や http_output などのどこかで自動的に文字エンコードを施すように設定されていて、変換してもまた元に戻されてしまっていたという可能性もあるかもしれないです。

 

PHP の内部で扱う文字コードを変換する

PHP は標準で、内部的には EUC-JP で文字列を扱うように構成されていると聞いたことがあります。

そのため、Web ページが UTF-8 の場合、PHP から出力された EUC-JP の文字列が HTML 内に記述された UTF-8 文字列と混在してしまうため、結果として文字化けが発生してしまうのではないかと考えられます。

PHP の内部文字コードを変更する 1 つの方法として、php.info の mbstring.internal_encoding や mbstring.http_output に UTF-8 を指定することで、内部処理コードが UTF-8 となるという情報も見受けられましたけど、php.ini を直接変更できない場合もありますし、自分の場合、Windows Server 2008 R2 上の PHP でこれを試してみたのですが、設定が効果を為しているようには感じられませんでした。

 

そこで、次のような宣言を実装し、その enableCharacterEncoding 関数を PHP スクリプトの冒頭で実行することにより、内部的に文字コードが UTF-8 に変換されるようにしてみました。

 

PHP

define('CHARACTERSET_LANGUAGE', 'uni');

define('CHARACTERSET_INTERNAL', 'UTF8');

define('CHARACTERSET_INPUT', 'auto');

define('CHARACTERSET_OUTPUT', 'UTF8');

 

function enableCharacterEncoding()

{

mb_language(CHARACTERSET_LANGUAGE);

mb_internal_encoding(CHARACTERSET_INTERNAL);

mb_http_input(CHARACTERSET_INPUT);

mb_http_output(CHARACTERSET_OUTPUT);

 

ob_start('mb_output_handler');

}

ここで、ob_start('mb_output_hander') という行を忘れないようにする必要があります。どうやらこの命令により、PHP から出力される文字コードを mb_http_output で指定したものに変換する仕組みが機能するようになるらしく、これがないと変換されないままの文字列が出力されてしまう感じです。

また、mb_language で指定した言語情報は、mb_send_mail というメール送信関数を利用する場合にのみ、影響するもののようです。この関数を使用しないならばわざわざ指定する必要もないですが、もし使うときになってまた文字化けで悩む可能性を少なくするためにも、併せて指定してしまっておいた方が、幾分ばかり安心かもしれません。

ともあれこのようにすることで、HTML の文字列と PHP からの出力が UTF-8 に統一され、ブラウザで文字化けを起こすようなことはなくなりました。

 

MySQL で扱う文字コードを変換する

MySQL のテーブルへデータを記録したり、読みだすスクリプトを作成してみたところ、MySQL から取得した文字列が化けて表示されてしまう問題が発生しました。

テーブルの文字列フィールドは UTF-8 で扱われるように設定してあったのですけど、そこへ PHP から書き込んだデータが文字化けを起こしてしまい、その化けた文字を読み込んで出力するために、当然ながら文字化けを起こしてしまった感じです。

これを回避するために、PHP 5.2.3 以上では MySQL とコネクションを張る際に、併せて mysql_set_charset() 関数を用いて、クライアント側の文字コードの設定を行うことができるようです。

 

PHP

define('DB_CHARACTERSET', 'UTF8');

 

 

$dbId = mysql_connect($server, $account, $password);

 

mysql_set_charset(DB_CHARACTERSET, $dbId);

mysql_select_db($database, $dbId);

ここで、mysql_set_charset で UTF-8 を指定する場合、'UTF8' とする必要があります。ここを 'UTF-8' としてしまうと、不明な文字コードが指定されたとして警告となってしまうようでした。

このようにすることで、PHP に書き込む際に正しく文字コードの変換がおこなわれるのか、テーブル内の UTF-8 フィールドへ文字化けせずに書き込むことができるようになりました。読み込みについても同様に、テーブルから読み込まれた文字列が、HTML 上で正しく表現されるようになりました。

 

もうひとつ、同様の対応として MySQL へ接続後に "SELECT NAMES 'UTF8'" という SQL 文を実行すると上手く行くという情報もあったのですけど、これを試してみても上手く行かず、相変わらず文字化けを起こしたままでした。

その理由は、クライアント側の文字コードを設定する処理がサーバー側でしか行われないため、相変わらずクライアント側とのギャップが残ってしまう感じになるかららしいです。これを回避するために、従来は MySQL 自体のディフォルト文字コードを変更して運用する必要があったとのことです。

現在は、mysqli にも mysqli_set_charset 関数が存在するらしく、PHP 5.2.3 以前でもこちらを利用することができれば、同様の設定を行うことが可能なようです。

 

 

フォームから入力された文字列の文字コードを変換する

フォームから入力した文字列を MySQL へ書き込むスクリプトを作成してみたところ、日本語で入力したフィールドが記録されていない問題が発生しました。

原因を確認してみると、フォームから送られてきた文字コードが PHP にとって予期しない文字コードであったらしく、それを変数に代入する際に、それらが無視されて処理されてしまったためでした。

これを回避するためには、送られてきた文字コードを検出し、内部処理用の文字コードに変換する必要があるようです。

 

PHP

define('CHARACTERSET_INTERNAL', 'UTF-8');

 

$code = mb_detect_encoding($value);

$value = mb_convert_encoding($_POST[$name], CHARACTERSET_INTERNAL, $code);

 

ただ、これだと文字列によっては文字コードをご検出してしまう場合があるそうです。

それを極力避けるために、あらかじめフォームの中に文字コード検出用の文字列をしのばせておくという方法があるようでした。

まず、HTML のフォーム内で、文字コード検出用の文字列を保持した隠しフィールドを用意します。そして、PHP 側でその文字列から文字コードを検出し、その文字コードから内部処理用の文字コードに変換するという方法をとることで、文字化けを回避することができるとのことです。

 

HTML

<form id="inputForm" method="post" action="">

 

<input type="hidden" name="_encode_hint" value="あ">

 

</form>

PHP

define('CHARACTERSET_INTERNAL', 'UTF-8');

 

$code = mb_detect_encoding($_POST['_encode_hint']);

$value = mb_convert_encoding($_POST[$name], CHARACTERSET_INTERNAL, $code);

これにより、正しく判定される文字列を設定すれば、フォームから送信されてきた文字列の安定した文字コード変換を行うことができます。

 

なお、HTML フォーム内の隠しフィールドを JavaScript を用いて追加することも可能です。

たとえば対象フォームに id 属性を設定して、その値を $id に渡し、そのフォームに文字コードヒントを設定する関数は、次のような感じになります。

 

JavaScript

function insertEncodeHint($formId)

{

var targetForm;

var node;

 

targetForm = document.getElementById($formId);

 

node = document.createElement('input');

 

node.setAttribute('type', 'hidden');

node.setAttribute('name', '_encode_hint');

node.setAttribute('value', 'あ');

 

targetForm.appendChild(node);

}

この関数を呼び出すことで、HTML コードに直接明示しなくても、後からこっそりとエンコードヒントをしのばせることもできるようでした。

 

フォームタグでエンコードを指定する方法

今回調べて行く中で、フォームタグの属性としてエンコード方法を指定することができることが分かりました。

 

HTML

<form id="inputForm" method="post" action="" accept-charset="UTF-8 US-ASCII">

 

</form>

このように "accept-charset" 属性として "UTF-8 US-ASCII" を指定することで、ブラウザが情報を送信する際に、入力データを UTF-8 または ASCII コードにて送るようにブラウザに指示できるとのことでした。この値が指定されていない場合は "UNKNOWN" が設定されているものとされ、"Web ページを構成している文字コードで情報が送られることが望ましいが、送信データの文字コードの選択はブラウザに委ねる" という意味合いになっているそうです。

ただ、この "accept-charset" を指定しても、ブラウザが対応していなければ意味がなく、確認はしていませんが対応していないブラウザも多いらしいです。ただ、IE 5.0 以降や Firefox, Opera, Netscape 4.78 以降では対応しているとの情報も見受けられますので、今時のブラウザなら信用しても良いのか悪いのか、そんな感じのように思います。

ともあれ、サーバー設定やスクリプトでの文字コード対応と併せてこの属性を指定しておくことで、より安定性を高めるといった感じの利用が良いのではないかと思います。もし、サーバー設定やスクリプトに手を加えることができない場合でも、この属性を設定しておくことによって、少しは文字化けの危険性を減らすことが期待できるかもしれないです。

 

ちなみに JavaScript から "accept-charset" 属性を設定する場合には次のようにします。これもまた、効果が期待できるかはわからないですが。

 

JavaScript

targetForm = document.getElementById($formId);

 

targetForm.setAttribute('accept-charset', 'UTF-8 US-ASCII');