谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

いいから聞け! 俺が文字コードについて教えてやるよ Advent Calendar 特別編

長らく更新の止まっている「いいから俺文字コード」シリーズですが、
このたび、Java Advent Calendarの一環として復活させました!

Java Advent Calendarって?

本エントリーはJava Advent Calendarの5日目です。
Java Advent Calendarについては、以下のサイトをご覧ください。
http://atnd.org/events/22434


前の4日目は @akirakoyasu さんの「SDKで身近になるAmazon Web Service」
http://www.akirakoyasu.net/2011/12/04/easily-use-aws-through-sdk/
S3、SimpleDB、SESの使い方をサンプルコードつきで紹介しています。


次の6日目は @shuji_w6e さんの「JUnit のセカイ」
http://d.hatena.ne.jp/shuji_w6e/20111205/1323098690
同じ事前条件(前準備)のテストケースを内部クラスとしてまとめる方法が目ウロコでした。


では、文字コードの話に入りましょう。
今回は特別編として、Javaの開発案件で特に頻発する問題や
陥りやすい問題についてフォーカスして、お送りします。

問題1:開発中は問題なかったのに、本番環境では急に文字化けが起きるようになりました!

新人くん「開発中は問題なかったのに、本番環境で急にファイルが文字化けしたと連絡があったんです!」
先輩社員「結合試験の環境では、どうだったの?」
新人くん「えっと、ファイル出力は試してません・・・」
先輩社員「なんで?」
新人くん(だって先輩、やれって言わなかったじゃないですか・・・)
先輩社員「え、何か言った?」

いきなり気まずい雰囲気から始まった「いいから俺文字コード」ですが(しつこい)
この問題が起きる原因は、十中八九、デフォルトエンコーディングの違いによるものでしょう。


開発環境はWindowsを利用し、結合試験や本番環境ではLinuxを利用するというのは
よくある組み合わせだと思います(最近は開発環境にMacが増え始めましたが)


ファイルや文字列のデフォルトエンコーディングとして、
Windows環境では「Windows-31j」が利用され、
Linux環境では「UTF-8」や「euc-jp」が利用されるため、
結果的に、開発環境と結合試験環境で生成されるファイルのエンコード
変わってしまうという現象が発生しうるのです。


そのため、特にサーバサイドJavaの開発においては、
「デフォルトエンコーディングを使わない」ことをルールづけたほうが安全です。


デフォルトエンコーディングを利用するクラスやメソッドとしては
以下のものが、よく使われています。

  • new String()
  • String#getBytes()
  • FileReader
  • FileWriter
  • new InputStreamReader()
  • new OutputStreamWriter()


このうち、
FileReaderとFileWriterは使わずに、FileInputStreamとInputStreamReaderで代用する、
それ以外のメソッドやコンストラクタでは、必ずエンコーディングやcharsetを指定するようにすれば、
開発環境と結合試験や本番環境で、結果が異なるということがなくなるでしょう。


ちなみに私は、Checkstyle正規表現チェック機能を使って
エンコーディングの指定漏れを検出しています。

問題2:なぜか「〜」ニョロだけ文字化けします!

新人くん「なんか、特定の文字だけが化けてしまって、解決しないんですが・・・」
先輩社員「特定の文字って?」
新人くん「ニョロです」
先輩社員「あぁ、全角チルダね」
新人くん「え、何かご存知なんですか?」
先輩社員「あぁ、よく化けるよ」

キング・オブ・文字化けと言えば、この、チルダ(ニョロ)でしょう。

「〜」と「〜」
\uFF5Eと、\u301C


Windowsでは「綺麗なニョロ」と「汚いニョロ」と呼べば何となく伝わりますが、
Macだと同じ形に見えるので、より発見が困難ですね。


さて、
Javaでは(厳密に言えば、Java1.4.1以降では)
Windows-31j」と「Shift_JIS」は少しだけ異なった範囲の文字を扱っており、
特に記号の扱いが異なっています。


問題のニョロについて言うと、
Windowsで普通に入力できる「〜」(\uFF5E)は「Windows-31j」の文字であり、
Macで普通に入力できる「〜」(\u301C)は「Shift_JIS」の範囲にある文字なのです。


「問題1」の最後に示したクラスやメソッドの引数に「Shift_JIS」を指定している
Windowsでよく入力する方の「〜」(\uFF5E)を扱うことができず、
ただの「?」、半角クエスチョンに文字化けしてしまうのです。


対策としては、
システム全体として、エンコーディングUTF-8に統一するのがオススメです。


ただ、どうしても案件都合でShift_JISを選ばなければいけない場合は、
基本的には「Windowsを優遇」しましょう。


JavaソースコードJSPファイルなどを検索して
「Shift-JIS」となっている箇所を「Windows-31j」に置換していけば、
きっとそれで問題は解決するでしょう。


、、、ただ、IEWindows-31jという文字列を認識できない、という問題があるため
HTMLのMETAタグに指定するcharsetだけは、「Shift_JIS」のままにしておいてください。


この件については「IE Windows-31j」で検索してくださいね。

問題3:Windows-31jの文字だけ許容したいから、getBytes("Windows-31j")でチェックしますた!

新人くん「Windows-31jの文字だけ許容したいんですが、どうすればいいですか?」
先輩社員「getBytesして、バイト値がWindows-31jの範囲に一致するかどうか判定すれば良いんじゃない?」
新人くん「やってみます」
・・・
新人くん「なんか期待通りに動かないんですが・・・」

私自身、いくつかの案件で
Windows-31jで扱える範囲内の文字列だけを許容したい」という
仕様を要望されたことがあります。


この要件は「JIS第四水準の漢字」や「サロゲートペア」などの
機種によっては対応していない文字の入力を防ぐことが目的なのです。


# 一方で ① や ㍼ のようなWindows固有文字については、
# イマドキのMacやケータイでも見えるから問題ない、という判断です。


さて、この要件自体の是非はさておき、
このチェック処理のために以下のようなコードが使われることがあります。

private boolean isWindows31j(String str) {
	byte[] bytes = str.getBytes();
	for (byte b : bytes) {
		if (isWindows31byte(b) == false) { // Windows31jの文字コード範囲かのチェック
			return false;
		}
	}

	return true;
}
// 実際は上位バイトと下位バイトのチェックをするので、もう少し違うコードになる。

残念ながら、この文字チェック処理では、
決して除外文字を検出することができず、常にtrueを返し続けます。


なぜか?
Windows-31jで扱えない文字がstrに入っていた場合、
getBytesした時点で、その文字は "?" を示す 0x3F に変換されてしまうのです。


たとえば「問題2」で話題にした、Mac用の「〜」(\u301C)も、
getBytesした時点で "?" というただの半角クエスチョンマークになり、
isWindows31byteメソッドの中でWindows-31jの文字と識別されてしまい、
「これはWindows-31jの文字だ」と誤判定されてしまうのです。


では、どうチェックするのが良いのか?
私は以下の処理でチェックすることをオススメしています。

byte[] bytes = str.getBytes(encoding);
return str.equals(new String(bytes, encoding));

詳細については、過去のエントリを参照してください。
http://d.hatena.ne.jp/cero-t/20100204/1265302329


フォーマットをバリデーションする際には、
「変換して、さらに逆変換した文字列が、元の文字列と一致するかどうか」
というチェックするのが王道だと、私は考えています。

問題4:せろさんの言う通りチェックしたのに、JIS第四水準の文字が通っちゃったんですけど!

新人くん「なんか、せろ部屋とかいうblogを見て文字チェックのコードを書いたんですが」
先輩社員「聞いたことないな」
新人くん「それを実際のWebアプリに組み込んだんですが、やっぱり第四水準の文字が通っちゃうんですよ」
先輩社員「そのサイトの説明が間違ってるんじゃないの?」

失敬な!


さておき、Webアプリ開発においては、
もう一つ追加で気をつけなければいけない事があります。
それが、文字の「数値文字参照」です。


たとえばWebブラウザで、charsetが「euc-jp」のHTMLページを開き、
入力ボックスに「euc-jp」の範囲外の文字を入力してsubmitした場合、
Webブラウザは、その文字を「数値文字参照」という形式に変換することがあります。
たとえば「❶」を「❶」という文字列変換して送信してくるのです。


「❶」という文字列の、文字を一つひとつ見れば、
もちろんWindows-31jの範囲内ですから、
「問題3」に書いたチェック処理ではtrueを返してしまい、通ってしまうわけですね。


この問題への対策としては、数値文字参照を含む(可能性のある)文字列について
一度、数値文字参照から文字への変換を行なう必要があります。


サンプルコードは長くなるので割愛します。
Googleで「数値文字参照 java」で検索してください。

問題5:サロゲートペア対策が必要だと、上の方から言われました。

WindowsXPをご利用の方で、この「𠀋」という文字列が見えない場合、
 以降の章を楽しむために、このパッチを導入することをオススメします。
http://www.microsoft.com/ja-jp/windows/products/windowsvista/jp_font/jis04/default.aspx

先輩社員「お客に、サロゲートペアの対策をしろって言われたんだよねー」
新人くん「サロゲートペアって何ですか?」
先輩社員「1つの文字を2文字で表すヤツなんだけどね、Windows Vistaからサポートされて、Java5でも対応してるはずなんだけど」
新人くん「なんだか面倒臭さそうなことは、分かりました」

2006〜7年頃、つまり、Windows Vistaが出てくる頃に話題になり、
その後は誰も触れなくなってしまった用語、サロゲートペア。
サロゲートペアとは一体何だったのか」というスレとか、立ってそうですよね。


サロゲートペアの詳細な話は割愛しますが、
Javaにおいては「char2つで、1つの文字を表現する」文字だと考えれば良いでしょう。


サンプルコードで見てみましょう。

String str = "𠀋";
System.out.println(str.length());
System.out.println(str.toCharArray().length);

このコードを実行すると
2
2
と表示されるのが、サロゲートペアなのです。


開発案件で「サロゲートペアに対策してください」と言われた場合、
多くの場合が「禁止してください」という意味でした。


禁止するだけなら、Character#isXxxSurrogateメソッドを使うと簡単です。

char[] chars = str.toCharArray();
for (char c : chars) {
	if (Character.isLowSurrogate(c) || Character.isHighSurrogate(c)) {
		return true;
	}
}

return false;

このコードでtrueが返ればサロゲートペアを含んでいるためエラーとし、
falseが返れば問題なしと判定してやれば良いのです。

問題6:サロゲートペアを許容しろと、上の方から言われました。

新人くん「お客様から、やっぱりサロゲートペアを許容して欲しいと言われたんですけど」
先輩社員「えっ、前は禁止って言ってたのに?」
新人くん「そうなんですが、やはり使う人がいるだろうし、WindowsXPを使う人も減ってきたということで」
先輩社員「ちゃんと押し返して来いよなー」
新人くん(そんな事言うなら、打ち合わせにちゃんと参加してくださいよ)
先輩社員「え、何か言った?」

さて、最後の問題は、サロゲートペアを許容する場合の話です。
そもそも、サロゲートペアを許容するとは、どういう意味なのでしょうか?


「問題5」では「𠀋」という文字が「2文字」として扱われると説明しましたが、
人間の目から見れば、どう見ても「1文字」です。
サロゲートペアを許容するためには、この差異をなくす必要があります。


たとえば「最大100文字」まで許容する入力ボックスに、
サロゲートペアを51文字入力した場合、
Javaとしては「102文字」として扱ってしまい、文字数オーバーとなりますが
人間の目には「100文字に到達していないのにエラーになった」ように見えるのです。
# 実際には、こんなことをする人がいないため、サロゲートペアはあまり問題になっていないのでしょう。


つまり、サロゲートペアを許容するポイントは、
特に「バリデーション処理を適切にすること」にあると、私は考えています。


見た目の文字数を取得するためには、String#codePointCountメソッドを利用します。

String str = "あ𠀋𠀋𠀋あ";
System.out.println(str.length());
System.out.println(str.codePointCount(0, str.length()));

この処理を実行すると
8
5
と表示されます。この「5」が見た目と一致しますね。


これまで長さのバリデーションを行なう際には
String#lengthメソッドを利用してきたと思いますが、
サロゲートペアに対応するためには
String#codePointCountメソッドを利用して判定を行なう必要があるのです。


また、サロゲートペアをRDBMSなどに保存する場合には
RDBMS側が対応しているかどうか(Unicodeの4byte対応)など、
周辺のミドルウェアサロゲートペアに対応しているかどうかも
必ず確認すべきでしょうね。

まとめ

  • この連載は「いいから俺文字コード」シリーズって言うらしい。
  • 意図せず、デフォルトエンコーディングを使ってしまわないように気をつけろ!
  • Windows-31jShift_JISは、全く別ものだから! そもそも、UTF-8でいいならそっちで!
  • getBytesしてから文字範囲チェックするのって、間違ってるから!
  • 数値文字参照に気をつけろ! 冷や汗かいてるだろ、そこのお前。お前のことだよ!
  • サロゲートペアなんて危険なもの、禁止にしてしまえ!
  • サロゲートペアに対応するなら、バリデーションでcodePointCountを使うのが肝だ!
  • この連載は「いいから俺文字コード」シリーズって言うらしい。


大事なことは、2回言いました。