文字コード嫌いだ


WEBシステムの構築をしていて、文字コード関連でつまずくことがよくある。
そのたびに、何やったけ?って悩むことが嫌なのでまとめていきます。文字コード関連は、ほんとにめんどくさくていやだ。クライアントも、サーバもDBも全部UTF-8にしてやれば良いのに。。。

全角チルダ問題

簡単にいうと、SJISとUTF-8で文字コードもマッピングがずれている問題。
なんでこんなことになっているのか。。。

どのような事象か

見た目がほぼ同じな「〜」(波ダッシュ)、「~」(全角チルダ)という二つの文字がそれぞれUTF-8で「0x301C」「0xFF5E」の文字コードが割り当てられています。しかし、Sjisでは、「〜」(波ダッシュ)が「0x8160」、「~」(全角チルダ)は対応なしとなっています。そのため、SJISに変換するときに対応する文字がないということがあり得てしまいます。

■SJISとUnicodeの変換対応

SJIS Unicode
波ダッシュ(0x8160) 波ダッシュ(0x301C)

■MS932(Windowsの仕様)とUnicodeの変換対応

SJIS Unicode
波ダッシュ(0x8160) 全角チルダ(0xFF5E)

このように、Windowsに特殊事情があり波ダッシュと全角チルダをマッピングさせており、Windowsで波ダッシュをUTF-8に変換した場合と他のOSで波ダッシュをUTF-8に変換した場合とで結果が異なってしまいます。
そのため、UTF-8からSJISに変換するときに、対応するマッピングがなくて文字化けが発生してしまいます。
例)クライアント(OS:Windows、UTF-8)⇒サーバ(OS:Linux、UTF-8)⇒DB(SJIS)

対象

このように、Windowsと他のOSでマッピングが異なる文字がいくつか存在します。

文字 SJIS Unicode(Unix) Unicode(Windows)
~(ウェーブダッシュ) 8160 U+301C U+FF5E
-(全角マイナス) 817C U+2212 U+FF0D
¢(セント) 8191 U+00A2 U+FFE0
£(ポンド) 8192 U+00A3 U+FFE1
¬(ノット) 81CA U+00AC U+FFE2
―(全角マイナスより少し幅のある文字) 815D U+2014 U+2015
∥(半角パイプが2つ並んだような文字) 816B U+2016 U+2225

対策

この事象の対策方法としては、3つあります。

対策方法1

単純にALL「UTF-8」でWEBシステムを作成する。
当然SJISに変換する必要がないなら、この事象は発生しません。

対策方法2

Windowsサーバを使用する。
クライアントもWindows、サーバもWindowsなら当然マッピングの整合性が取れるので問題が発生しません。

対策方法3

マッピングがずれている文字を正しい文字に置換する。
例えば、以下のような関数を作成して使用することによって文字化けを防ぐことができます。

private String replaceUnmappingChar(final String target) {
    return target.chars().mapToObj(i -> {
        char changed = (char)i;
        switch(changed) {
            // 全角チルダ
            case 0x301C:
                changed = 0xFF5E;
                break;
            // 全角マイナス
            case 0x2212:
                changed = 0xFF0D;
                break;
            // セント
            case 0x00A2:
                changed = 0xFFE0;
                break;
            // ポンド
            case 0x00A3:
                changed = 0xFFE1;
                break;
            // ノット
            case 0x00AC:
                changed = 0xFFE2;
                break;
            // 全角マイナスより少し幅のある文字
            case 0x2014:
                changed = 0x2015;
                break;
            // 半角パイプが2つ並んだような文字
            case 0x2016:
                changed = 0x2225;
                break;
        }

        return String.valueOf((char) changed);
    }).collect(Collectors.joining());
}

サロゲートペア

unicodeでは、通常1文字を0x0071のように2バイトも文字で表します。しかし、1文字を4バイトで表現する文字も存在します。それがサロゲートペアです。

問題

サロゲートペアが導入されることによってどのような問題が発生するでしょうか?文字の表現幅が広がって便利になります。しかし、通常1文字2バイトです。それが、4バイトの可能性もあるということは、DBのサイズも考慮しないといけません。
また、文字列の長さが想定した長さになりません。

// なんと読むかわからない漢字
System.out.println("𤠫".length());
実行結果
2

このように明らかに1文字なのに2文字と返却されてしまいます。なぜでしょうか。「𤠫」という文字はUnicodeで「U+2482B」、UTF-16で「0xD852 0xDC2B」と2文字分使用しているからです。
なんの対策も実施しないと、入力チェックなどの処理で実際はエラーとしてはいけないのに、エラーと判断してしまうこともあり得ます。

対策

どうすれば良いのか。対策方法としては、2つあります。

対策1

サロゲートペアを禁止します。この方法が一番一般的ではないでしょうか?
JAVAでは、以下のような方法でサロゲートペアかどうか判定ができます。

private static boolean containSurrogate(final String checkTarget) {
    return checkTarget.chars().anyMatch(i -> {
        return Character.isSurrogate((char) i);
    });
}

対策2

とはいってもサロゲートペアを許容しないといけないこともあると思います。そういった時は、サロゲートペアを含むと入力チェックとかが失敗する可能性があります。
JAVAには、String.codePointCountという関数が準備されています。この関数は、文字列長を取得する関数ですが、サロゲートペアの場合も考慮してくれます。

System.out.println("𤠫".codePointCount(0, "𤠫".length()));
実行結果
1

今回は以上です。
また、文字コード関係で悩むようなことがあれば追加していきたいと思います。