※こちらのページは技術評論社刊「WEB+DB PRESS Vol.37」に掲載された内容になります。
連載最後の開拓は、日本語処理を取り上げたいと思います。中でも「文字化け」という地雷原を突き進むので、みなさん気を引き締めてついてきてください!
なお、今回使用するMySQLのバージョンは、バイナリ配布されているCommunity Editionの最新版である5.0.27を使用します。
MySQLがサポートしている文字コード(注1)は、バージョン4.0、4.1、5.0で大きく異なります。サポートしている文字コードのうち、日本語に関連するものを表1にまとめます。
| 表2:サポートしている文字コード | ||||||
| バージョン | ujis | eucjpms | sjis | cp932 | utf8 | ucs2 |
| 4.0 | ○ | × | ○ | × | × | × |
| 4.1 | ○ | × | ○ | ○ | ○ | ○ |
| 5.0 | ○ | ○ | ○ | ○ | ○ | ○ |
表1にucs2がありますが、ucs2はMySQLが内部的に使う文字コードです。MySQL 4.1以降は、クライアント/サーバ間で文字コードの変換を行うようになったのですが、この変換はucs2を介して行われます。例えば、ujisをsjisに変換する場合は、ujis→sjisと変換されるのではなく、ujis→ucs2→sjisと変換されます。
一見、何の問題もなさそうに思えますが、「ucs2を介する」という点は非常に重要ですので覚えておいてください。
文字コードの範囲外のデータをINSERTしたときの動作が変わりました。
4.0まではどんなバイト列でもINSERTできたのですが、4.1以降では文字コードの範囲外以降のデータは切り捨てられてしまいます。
例えば、「あい?えお」(「?」はsjisの範囲外のバイト列とします)をINSERTすると、格納されるのは「あい?えお」でも「あいえお」でもなく「あい」だけになってしまいます。このような切り捨てが起こった場合にはwarningが出るので、SHOW WARNINGSで確認することができます。(注2)
設定によってはこんな文字化けが発生します。
こんな文字化けにあわないための方法を今回はじっくり解説していきます。
次節ではいきなりですが文字化けしないためのガイドラインをまとめます。これを守れば文字化けとはオサラバできるはずです。
そしてその後で、MySQLの世界でなぜこんな文字化けが発生するのかをみっちり開拓したいと思います。
文字化けは文字コードを変換する過程で起こります。よって、いちばんシンプルな文字化け回避方法は文字コードを変換しないようにすればいい、ということになります。
MySQLではクライアント側とサーバ側の2か所で文字コードの指定ができます。この両者で同じ文字コードを指定すれば、文字コード変換は行われないのでMySQLの世界で(文字コード変換が原因の)文字化けは起こらなくなります。
文字コードの統一ができず、どうしてもMySQLの世界で文字コードの変換をしなければならない場合は、次にあげる文字コードの間で変換するようにするのが鉄則です。
注意しなければならないのは、このリストにsjisとujisが入っていないという点です。
cp932からutf8に変換するのは問題ありませんが、sjisからutf8に変換すると化ける文字が存在するのです。それどころか、なんと同じシフトJIS系のsjisとcp932との間の変換でも化ける文字があります。
実例は後ほどお見せしますが、MySQLの世界で文字コードの変換をする場合は、この3つ以外は使ってはいけません。
文字コードがcp932(もしくはsjis)の場合に、文字化けというかバイナリデータが壊れるケースがあります。
例えば、0x9500(注3)というバイナリデータをINSERTすると、0x955C30になってしまいます。
これまた後ほど原因は解明しますが、データ破壊を避けるためには、
ようにするのが鉄則です。
文字化けの実例をお見せする前に、MySQLの文字コード変換のメカニズムを説明します。
MySQLで文字コードの指定ができるところは大きく分けて2つあります。
まずはこの2つの指定方法やその影響範囲をみていきましょう。
サーバ側として指定する文字コードは、ストレージに保存されるデータの文字コードに影響します。
MySQLでは次のような単位で、サーバ側の文字コードを指定できます。単位が小さい順に列挙します。
それぞれの指定方法を表2に、確認方法を表3にまとめます。
文字コード指定は単位が小さいものが優先され、指定されていなければより大きい単位の指定が採用されます。
具体的にいうと、リスト1のCREATE TABLEの場合、col1の文字コードはutf8で、col2はcp932になります。
また、CREATE TABLEで文字コード指定を省略した場合はデータベースの文字コードが指定されたことになり、CREATE DATABASEで文字コード指定を省略した場合はサーバ全体のdefault-character-setの文字コードが指定されたことになります。
つまり、サーバ側の文字コードを統一する場合は、default-character-setだけを指定して、CREATE TABLE文やCREATE DATABASE文では文字コードの指定をしなければいいわけです。これで、カラム、テーブル、データベースの文字コードが自然に統一されるようになります。
もし、サーバ側で文字コードを混在させる場合はよく考えた方がいいと思います。
は、場合によっては許容範囲かなと思いますが、リスト1のように、
は、あとで混乱するのは必至なので、のっぴきならない理由がない限りは避けるべきだと思います。
| 表2:文字コードの指定方法(サーバ側) | |
| 指定単位 | 指定方法 |
| カラム | CREATE, ALTER TABLE文 |
| テーブル | |
| データベース | CREATE, ALTER DATABASE文 |
| サーバ全体 | 起動オプションのdefault-character-set |
| 表3:文字コードの確認方法(サーバ側) | |
| 指定単位 | 確認方法 |
| カラム | SHOW CREATE TABLE文 |
| テーブル | |
| データベース | SHOW CREATE DATABASE文 |
| サーバ全体 | SHOW VARIABLES LIKE 'character\_set\_server' 文 |
<リスト1:テーブルとカラムの文字コード指定の例>
CREATE TABLE charset_test (
col1 VARCHAR(32)
,col2 VARCHAR(32) CHARACTER SET cp932
) ENGINE=InnoDB CHARACTER SET utf8;
クライアントは、サーバとデータのやり取りをする前に、「この文字コードでデータを送ったり受け取ったりしたい」という情報をサーバに知らせます。これが、クライアント側として指定する文字コードです。
サーバ(mysqld)は、データを格納したりクライアントにデータを返したりする前に、クライアント文字コードとテーブルやカラムの文字コードとの間で文字コードの変換を行います。文字コード変換の処理は、クライアント側ではなくサーバ側で行われている点に注意してください。
さて、このクライアント側の文字コードを指定するには次の方法があります。
実はdefault-character-setにはもう1つの作用があります。それは、クライアント自身が行う文字列処理です。
例えば、mysqlコマンドを対話的に使っているときのクォート(「'」や「"」)の処理や、libmysqlclientのmysql_real_escape_string(プリペアドステートメントのプレースホルダなどで使われます)がこれにあたります。
このおかげで、エスケープを指示する「\」(0x5C)と同じコードを含むシフトJISの「表」(0x955C)などが問題なく処理できるわけです。
ここで注意してほしいことがあります。
SET NAMES文では文字列処理用の文字コードを変更できないのです。
試しに、default-character-set=binaryで接続した後、mysqlコマンドでSELECT '表'すると図1のように正しく処理できません。また、PerlのDBD::mysql(バージョン4.001)でprepareとbind_paramで「表」をINSERTしようとすると、リスト2のように文法エラーになってしまいました。
では、default-character-set以外に文字列処理用の文字コードを指定するにはどうすればよいのでしょうか。その方法はいくつかあります。
バージョン5.0.25以上の場合(注4)、mysqlコマンドはcharset命令でSET NAMES相当のクライアント側文字コードと文字列処理用の文字コードの両方を変更することができます(図2)。従って、mysqlコマンドの場合はSET NAMES文を使わずcharset命令を使うようにすればよいでしょう。
PerlやPHPからMySQLに接続している場合は、その言語のドライバが上記のC言語APIに対応していればよいのですが、今のところ対応状況はよくないようです。特にmysql_set_character_set()は比較的最近、追加されたAPIなので、今後の対応を期待したいところです。
話が込み入ってきたので、最後にクライアント側文字コードの指定方法をチャートでまとめます(図3)。
<図1:SET NAMESが効かない例(mysqlコマンド)>
$ mysql --default-character-set=binary
wd@my50-1[(none)]> SET NAMES cp932;
Query OK, 0 rows affected (0.00 sec)
wd@my50-1[(none)]> SELECT '表';
'> ← ちゃんとクォートしているのに、クォートが閉じられていないとみなされている。
<リスト2:SET NAMESが効かない例(DBD::mysql)>
DBD::mysql::st execute failed: You have an error in your SQL syntax;
check the manual that corresponds to your MySQL server version for
the right syntax to use near '表\','表\')'
<図2:charsetの使用例(mysqlコマンド)>
$ mysql --default-character-set=binary
wd@my50-1[(none)]> charset cp932;
Charset changed
wd@my50-1[(none)]> SELECT '表';
+----+
| 表 | ← ちゃんと結果が返ってくる。
+----+
| 表 |
+----+
1 row in set (0.00 sec)
<図3:クライアント側文字コードの指定チャート> ■初期値の設定 │ ├mysqlコマンドの場合 │└【my.cnfの[mysql]にdefault-character-setで指定する】 │ └my.cnfを読めてdefault-character-setを解釈することができるクライアントか? ├(yes)→【my.cnfの[client]にdefault-character-setで指定する】 └(no )→「SET NAMESコース」へ ■途中で変更したい │ ├【途中で変更しなければならないような構成はやめて、初期値だけに頼るようにする】 │ ├mysqlコマンドの場合 │└5.0.25以上か? │ ├(yes)→【charset命令で指定する】 │ └(no) →「SET NAMESコース」へ │ └C言語APIのmysql_set_character_set()かmysql_options()が使えるクライアントか? ├(yes)→【mysql_set_character_set()かmysql_options()で指定する】 └(no) →「SET NAMESコース」へ ■SET NAMESコース │ └指定したいのはシフトJIS(cp932かsjis)か? │ ├(no )→【SET NAMES文で指定する】 └(yes)→【MyNAパッチ(注5)を当てた上で、SET NAMES文で指定する】
MySQLの世界で文字コードの変換が発生するのは、先ほど紹介したサーバ側文字コードとクライアント側文字コードが異なる場合です。
逆にいうと、一致していれば文字コード変換は起こりません。よって、鉄則(1)のためには、クライアント側とサーバ側の文字コードを一致させるのがいちばんシンプルかつ、わかりやすい方法といえるでしょう。
さて、文字コード変換を抑制するにはまだほかの方法もあります。
1つめは、
です。ただし、シフトJISの場合は意図しないエスケープ文字化けが発生するので、おすすめしません。
2つめは、
です。このオプションを設定すると、クライアントが申請したクライアント側文字コードは無視され、強制的にサーバ側文字コードにあわされます。その結果、クライアント側、サーバ側の文字コードが一致するので、文字コード変換は行われなくなります。
ただし注意点があります。skip-character-set-client-handshakeを指定すると、クライアントがdefault-character-setを指定したとしても無視されてしまいます。
例えば、クライアント側、サーバ側共にcp932で統一して運用している状況で、どうしても必要でmysql --default-character-set=eucjpmsで対話的なmysqlコマンドを実行し、eucjpmsの文字コードのデータをINSERTしたとします。通常ならば、サーバ側でeucjpms→cp932の文字コード変換がなされるので問題はないのですが、skip-character-set-client-handshakeが指定されていると文字コード変換が行われず、EUC-JPのバイト列のままcp932を期待するカラムにデータが格納されてしまいます。「明示的にdefault-character-setを指定しているのだから、これが優先されて文字コード変換されるだろう」と思い込んでいると、文字化けが発生してしまうので気をつけてください。
最後に「こんなに怖い文字化けの世界」を紹介します。鉄則を守らないのと、あなたもうっかり足を踏み入れてしまうことになるかもしれません…
鉄則(2)を守らずに、cp932、eucjpms、utf8以外の文字コードを使って文字コード変換をすると発生する文字化けを3つ紹介します。
少なくともMySQLの世界では、cp932、eucjpms、utf8と内部的に使っているucs2は文字化けせずに相互変換できます。
しかし、これらの文字コードと、sjisやujisとの間では相互変換できない文字があります。具体例を見てみましょう。
図4のように、クライアント側文字コードをsjisにして、eucjpmsのテーブル(カラム)に「〜」という文字をINSERTしてみます。
SELECTとしてみると、なんと「〜」ではなく「?」(0x3F)が返ってきました。
文字化けの過程を追ってみましょう。
まず、入力データとなるシフトJISの「〜」(0x8160 WAVE DASH)は、サーバ側でいったん、内部コードであるucs2(U+301C WAVE DASH)に変換されます。この次が問題で、ucs2の「〜」をeucjpmsに変換すると「?」(0x3F)になってしまうのです。これはMySQLの実装がおかしいのではなく、eucJP-msという文字コードの定義上、U+301Cとのマッピングが定義されていないためです。
このケースの文字化けで特徴的なのは、warningが出る点です。図4のように、INSERTすると「1 warning」と表示され、その内容はSHOW WARNINGS文で見ることができます。
ちなみに、eucjpmsの「〜」(0xA1C1)に変換できるucs2のコードポイントはU+FF5E(FULLWIDTH TILDE)です(図5)。今回のケースでクライアント側をsjisではなくcp932にした場合は文字化けしません。なぜなら、「〜」をcp932からucs2に変換した場合はU+FF5Eになるからです。
さて、ある文字コードから別の文字コードに変換して、さらにそれを元の文字コードに戻すことをラウンドトリップ変換というのですが、「〜」のようにラウンドトリップできない文字がほかにもいくつか存在するので表4(注6)にまとめます。
<図4:sjis→eucjpmsの文字化け>
$ mysql -uwd --default-character-set sjis wd
wd@my50-1[wd]> INSERT INTO mojibake (c_eucjpms) VALUES ('〜');
Query OK, 1 row affected, 1 warning (0.02 sec) ← warningが出ている
wd@my50-1[wd]> SHOW WARNINGS;
+---------+------+------------------------------------------------+
| Level | Code | Message |
+---------+------+------------------------------------------------+
| Warning | 1265 | Data truncated for column 'c_eucjpms' at row 1 |
+---------+------+------------------------------------------------+
wd@my50-1[wd]> SELECT c_eucjpms FROM mojibake;
+-----------+
| c_eucjpms |
+-----------+
| ? |
+-----------+
<図5:U+301CとU+FF5Eをeucjpmsに変換>
wd@my50-1[wd]> SELECT HEX( CONVERT(_ucs2 0x301C USING eucjpms) ) AS 301C
-> ,HEX( CONVERT(_ucs2 0xFF5E USING eucjpms) ) AS FF5E;
+------+------+
| 301C | FF5E |
+------+------+
| 3F | A1C1 |
+------+------+
| 表4:ラウンドトリップ変換が保証されない文字 | ||||||
| 字形 | ucs2 | Unicode Name | sjis | cp932 | ujis | eucjpms |
| 〜 | U+301C | WAVE DASH | 8160 | 3F | A1C1 | 3F |
| U+FF5E | FULLWIDTH TILDE | 3F | 8160 | 3F | A1C1 | |
| ‖ | U+2016 | DOUBLE VERTICAL LINE | 8161 | 3F | A1C2 | 3F |
| U+2225 | PARALLEL TO | 3F | 8161 | 3F | A1C2 | |
| − | U+2212 | MINUS SIGN | 817C | 3F | A1DD | 3F |
| U+FF0D | FULLWIDTH HYPHEN-MINUS | 3F | 817C | 3F | A1DD | |
| ¢ | U+00A2 | CENT SIGN | 8191 | 3F | A1F1 | 3F |
| U+FFE0 | FULLWIDTH CENT SIGN | 3F | 8191 | 3F | A1F1 | |
| £ | U+00A3 | POUND SIGN | 8192 | 3F | A1F2 | 3F |
| U+FFE1 | FULLWIDTH POUND SIGN | 3F | 8192 | 3F | A1F2 | |
| ¬ | U+00AC | NOT SIGN | 81CA | 3F | A2CC | 3F |
| U+FFE2 | FULLWIDTH NOT SIGN | 3F | 81CA | 3F | A2CC | |
字形(グリフ)は参考程度に考えてください。本来は、Unicodeのコードポイント別に異なった字形が割り当てられています。
SQLで文字コードを変換するにはCONVERT()関数を使います。
CONVERT(expr USING transcoding_name)
で、exprをtranscoding_nameで指定した文字コードに変換できます。
また、introducerという仕組みがあり、「_charset_name」を前置して、リテラルの文字コードを明示的に指定することができます。
例えば、EUC-JPの「凝」と、シフトJISの半角カタカナの「カナ」は、共に同じバイト列の0xB6C5なので、変換元の文字コードを指定しないと意図しない変換結果になってしまいます。このようなときに、introducerを使います。
SELECT HEX( CONVERT(_eucjpms 0xB6C5 USING ucs2) ); → 51DD SELECT HEX( CONVERT(_cp932 0xB6C5 USING ucs2) ); → FF76FF85
また、HEX()関数は文字列を与えるとそれを16進数表記にして返す関数です。この逆の変換をするUNHEX()という関数もあります。
これらのものは、文字化けの原因を探るときなどに非常に重宝するので、覚えておいて損はないでしょう。
先の例ではeucjpmsだったテーブル(カラム)の文字コードを、クライアントと同じシフトJIS系のcp932に変えた場合ではどうなるでしょうか。
実はこれも表4からわかるように、まったく同じ理由で化けて「?」(0x3F)が格納されてしまいます。
同じシフトJIS系のsjisとcp932、もしくは、同じEUC-JP系のujisとeucjpmsはラウンドトリップ変換が保証されているように思い込みがちですが、そうではないということを覚えておいてください。
クライアント側をsjis以外、サーバ側をsjisにした場合、1バイトのバックスラッシュ(0x5C)をINSERTすると、2バイトのバックスラッシュが格納されてしまいます。クライアント側をcp932にした場合の例を図6に示します。
このメカニズムはこうです。MySQL ABのドキュメント『The cp932 Character Set』(注7)を見ると、cp932の「\」(0x5C)をucs2に変換するとU+005Cになります。ここからが問題で、ucs2のU+005Cをsjisに変換すると0x5Cではなく、なんと「\」(0x815F)になってしまうのです。これは図6のSELECT HEX()の結果とも一致します。
このバックスラッシュ文字化けの最悪な点は、先のラウンドトリップ変換問題と違いwarningが出ないので気づきづらいのと、サーバ側にsjisで0x815Fが格納されている場合、クライアント側文字コードをcp932にしてSELECTすると「\」(0x5C)が返ってくることです(図6の後半)。したがって、クライアント側をcp932、サーバ側をsjisにしている場合、格納されているデータは化けている(0x5Cではなくて0x815Fが格納されている)にも関わらず、まったく気が付かない可能性があります。
<図6:バックスラッシュの文字化け>
$ mysql -uwd --default-character-set cp932 wd
wd@my50-1[wd]> INSERT INTO mojibake (c_sjis) VALUES ('\\');
Query OK, 1 row affected (0.02 sec) ← warningが出ていない
wd@my50-1[wd]> SELECT HEX(c_sjis) FROM mojibake;
+-------------+
| HEX(c_sjis) |
+-------------+
| 815F | ←「\」が「\」(0x815F)に化けた!
+-------------+
wd@my50-1[wd]> charset sjis; ←クライアント側をsjisに変更
Charset changed
wd@my50-1[wd]> SELECT c_sjis FROM mojibake;
+--------+
| c_sjis |
+--------+
| \ | ←格納されているままの「\」が返ってくる。
+--------+
wd@my50-1[wd]> charset cp932; ←クライアント側をcp932に変更
Charset changed
wd@my50-1[wd]> SELECT c_sjis FROM mojibake;
+--------+
| c_sjis |
+--------+
| \ | ←sjisのときと違い「\」が返ってくる!
+--------+
次は鉄則(1)を守って文字コード変換をしていないのに、文字化けしてしまうケースを紹介します。
「cp932とバイナリデータの注意点」の節で書きましたが、クライアント側文字コードをシフトJIS系(cp932、sjis)にして、C、PHP、Perlなどから、BLOBなどバイナリ型のカラムに0x9500をINSERTすると、なぜか0x955C30が格納されてしまいます。
この挙動はバージョンによって異なりややこしいので、0x9500をINSERTした場合に格納されるデータを表5にまとめます。
この表を見ると、「skipなし」で「cs指定なし」の場合はどちらのバージョンも正しく格納されているので、これが最善の回避策のように見えますが、この設定はしてはいけません。クライアント側のdefault-character-setを指定しなかった場合、デフォルトのlatin1になるのですが、latin1でバイナリデータが壊れる現象を回避できても、クライアント側文字コードを正しく設定しないと、これまでみてきたほかにもその弊害はいろいろとあるからです。
したがって、5.0.21またはそれ以下のバージョンの場合は、16進数表記へ変換するとかBASE64変換するなどして、バイナリをASCIIに変換してINSERTするのがよいのではないかと思います。
一方、5.0.22またはそれ以上(注8)の場合は、「cs指定あり」ならばskip-character-set-client-handshakeに関わらず正しいデータが格納されます。先にのべたように、skip-character-set-client-handshakeにはdefault-character-setを無視するという副作用があるので、「skipなし」で「cs指定あり」と設定することをお勧めします。
| 表5:バイナリデータが壊れるケース | ||||||
| 5.0.21 | cs指定あり | cs指定なし | ||||
| skipなし | 955C30 | 9500 | ||||
| skipあり | 955C30 | 955C30 | ||||
| 5.0.22 | cs指定あり | cs指定なし | ||||
| skipなし | 9500 | 9500 | ||||
| skipあり | 9500 | 955C30 | ||||
今回はMySQL 4.1以降の鬼門である日本語処理について、文字化けにしぼって開拓しました。
MySQL 5.0では、cp932、eucjpms、utf8に対応するなど、ほかのRDBMSと比較して日本語対応はすぐれていると思うのですが、4.1以降の混乱から出た悪評がまだ残っている感があります。
私も少し前までは5.0系の日本語処理には問題が多すぎで実用は難しいという判断だったのですが、5.0.25以降ならば本格的に使っても問題ないと最近では思っています。
日本語問題のために4.0を使い続けている人も、次の機会には5.0を評価してみてはいかがでしょうか?