まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:utf8

Java で(JDBCで)MySQL データベースに日本語文字を書き込んだ時に文字化けする、という問題に遭遇した時の対処方法を紹介します。

まず、MySQL データベースを自分で用意する場合にはあまり起こらないことだと思っています。起こらないというか、起こっても自分で MySQL サーバーやクライアントの文字コード設定を変えれば対処できることだと思っています。

ところが最近は PaaS が流行っていることもあり、自分でデータベースサーバーを構築するのではなく、「用意されたデータベースのインスタンスを作って使う」という使い方が可能になり、用意された(自分で設定したわけではない)データベースを使うだけ、という手法も多く使われはじめています(IBM Bluemix とか、AWS の RDS とか、・・)。これはこれで簡単で便利なのですが、いざ問題が起こった場合にサーバーの管理者ではないため、設定を変更できる部分とできない部分があったり、そもそもどういう設定で動いているのかを確認できたりできなかったりします。そしてその結果として、今回のような文字化けの問題が発生したりします(デフォルトのデータベース文字コードが分からない、当然のように UTF-8 だと思って UTF-8 で書き込んだら文字化けした、というパターン)。

これを回避するにはデータベースの設定がどのようになっていたとしても UTF-8 前提で接続して、UTF-8 を明示して書き込む、という実装が必要になるわけです。


これが Java(JDBC) と MySQL で問題になったりします(同じ MySQL でも PHP では特に意識せずに文字化けも回避して使えていたが、同じ処理を Java で行うと文字化けする、といったことが起こります)。コードのテキストそのものが UTF-8 で記述されていても起こります。要は上記で書いたような部分が自動判別では実現できておらず、コードとして実装する必要があるからです。

Java(JDBC) で MySQL に対して UTF-8 を明示して接続するには、このように記述する必要があります:
    :
    :
  Connection conn = null;

  String dburi = "mysql://mysql.test.com/mydb?useUnicode=true&characterEncoding=utf8";

  try{
    Class.forName( "com.mysql.jdbc.Driver" );
    conn = DriverManager.getConnection( "jdbc:" + dburi, mysql_username, mysql_password );
  }catch( Exception e ){
    e.printStackTrace();
  }

    :
    :

上記青字部分が指定している箇所です。useUnicode でユニコード指定を名言し、characterEncoding でそのエンコードを utf8 に指定しています。

この方法でコネクションを取得すると、読み書き時の文字化けを回避できました。まあ全ての文字化けのケースにこの方法が有効とはいえませんが、原因不明の文字化けが発生した際の回避方法の1つとして。


2日ほど泥沼にハマって抜け出せなくなっていた問題が解決できたので、その謎を共有します。

元々はこんなシステムを PHP で作ろうとしていました:
 - 画像をアップロードできる
 - アップロード画像は ID を付与した上で MySQL の BLOB (バイナリ・ラージ・オブジェクト)として格納する
 - ID を指定して、アップロード済みの画像を表示させることができる

自分自身でも Java では何度も作ったことのある仕組みで、その PHP 版を作ろうとしただけでした。

いざ作って動かしてみると、アップロードは成功します。でも表示時に画像が壊れてしまい、うまく表示できないのです。この原因究明と解決に時間を使ってしまいました。


もう少し詳しく状況を説明します。調べていくうちに同じ現象が MySQL を使わなくても再現できることがわかったので、話をシンプルにするために MySQL なしで説明します(実際にはこの切り分けも簡単ではなかった)。

ソースコードはこんな感じ。同一ディレクトリにある bmxug148.png というファイルのバイナリコンテンツ(元々のコードでは MySQL の BLOB に格納されていたもの)を取り出して、PNG 画像用の HTTP ヘッダを指定して echo でバイナリをそのまま出力しています:
<?php
$img = file_get_contents("./bmxug148.png");
header('Content-Type: image/png');
echo $img;
?>

この PHP ファイルをブラウザから呼び出すと、画像が表示・・・されることを期待していたのに、画像が壊れてしまっている旨のメッセージが表示されてしまいます(FireFox で確認した場合):
2015100202


こういう状況だと、表示側の PHP スクリプトに問題があるのか、それともアップロードしたファイルバイナリデータの格納時に既に壊れてしまっているのかすらわかりません。まあ、上記のシンプルなスクリプトでも再現するということは前者ということになるのですが、それが分かったのは相当後になってのことです。


で、とりあえずこの「壊れた画像バイナリ」を無理やり取り出してファイルとして保存し、バイナリエディタで開いてみました。そこで分かったのは画像の最初3バイトに PNG 画像としてふさわしくない情報が追加されていた、ということです:
2015100203


0xEF, 0xBB, 0xBF という3バイトが余分です(4バイト目から本来の PNG 画像のヘッダが始まっています)。4バイト目から表示されていれば正しく表示できそうなのですが、ではこの最初の3バイトはどこで付与されてしまったのでしょう? DB への格納時の処理を疑うこともできたのですが、この EF BB BF という3バイトの組みあわせ、どこかで見たことありました。。

ちょっと調べると分かるのですが、これは BOM(Byte Order Mark) と呼ばれる符号で、テキストが Unicode で記述されていることと、その符号化の種類を示すものです。で、UTF-8 の場合の BOM がまさにこの 0xEF 0xBB 0xBF の3バイトなのです。


つまり、実はもとの PHP ファイルそのものが UTF-8 で記述されていて、しかも BOM 付きでファイルが保存されていたことが直接の原因だったのでした。このため、ブラウザでこのファイル実行した場合も、最初に 0xEF 0xBB 0xBF の3バイトは(PHP ファイルの一部として)返ってきて、次に PHP で処理した結果の画像バイナリが続いていた、ということになります:
2015100201


画像の情報は正しく返っていたのに、その前に画像とは関係のない3バイトが送られてきていたため、「画像としては壊れている」と判断された、ということです:
2015100202


したがって、解決策は PHP ファイルを BOM なしで保存することです:
2015100204


この PHP をブラウザから実行すれば、本来の正しい画像が表示されるようになります:
2015100205


あー、よかった。
 

アマゾン(http://www.amazon.co.jp/) のトップページの文字コードは Shift_JIS でした。
ama1
 
ここで検索ボックスにキーワードを入れて検索してみると、その結果ページの文字コードは UTF-8 ・・・
ama2

更に検索結果から1つ選んで詳細ページに移ると・・・ 今度はまた Shift_JIS でした。
ama3

いっそ最後は EUC-JP にでもすればいいのに・・・
 

このページのトップヘ