モーグルとカバとパウダーの日記

モーグルやカバ(EXカービング)山スキー(BC)などがメインの日記でした。今は仕事のコンピュータ系のネタが主になっています。以前はスパム対策関連が多かったのですが最近はディープラーニング関連が多めです。

JavaでWebフォームに入力された内容を文字化けせずにメールで送る

Webでフォームに入力された内容をメールでどこかに送る、というのはすごくありふれた内容だと思います。
が、Javaで普通に書くと、いろんな理由で文字化けが発生してしまいます。
一番よくあるのがUTF-8→JIS(ISO-2022-JP)に変換するときに、変換できなくて「?」になってしまうというやつです。


そこで、文字化けが起きないように事前にいくつかフィルタを掛けてやる必要があるのですが、あまりまとまった情報がなくて困ったのでまとめてみました。


大きく下記3種のフィルタを掛けることで対応しました。


波ダッシュ問題はとても有名ですが、その上UTF-8には色んな波ダッシュやハイフンがあり、一つに対応してあっても他ので化けたりします。
どうしたもんかと思っていたら、下記ページに波ダッシュやハイフンの各文字コードでの対応状況がまとめられており、大変に参考になりました。
また、変換コードもありましたので、そこに追加する形で他の化ける文字の対応をおこないました。

Unicodeの似た文字を整理してみた - y-kawazの日記


他にもIBM-UnicodeとMS-Unicodeの違いにより化けてしまうという問題があります。
これで化けるものは、波ダッシュの他に「¢」や「£」などがあります。下記のページを参考にしました。

UTF-8での文字化け - mokkouyou2001の日記
perlでメール送信時の いわゆる波ダッシュ「〜」問題 | clicktx::Tech::Memo


また、接続環境なのかクライアントによってなのか、フォームに入力された記号のいくつかが、例えば「-」が「−」のように、HTMLエンティティに変換されて送られてくる場合があるようです。
これはどういった場合に、どの記号が変換されるのかということが調べきれなかったため、全てのHTMLエンティティを変換することにしました。
これには StringEscapeUtils クラスの unescapeHtml4 を利用しました。


これらをまとめたものが以下のコードです。

/**
 * HTMLエスケープ文字、UTF8→JISへの変換で化ける文字、を変換する
 * @param str
 * @return 変換された文字列を全て戻した文字列
 */
public String unescapeForMail(String str) {
  String result = str;
  result = StringEscapeUtils.unescapeHtml4(result);
  result = normalizeSimilarCharacter(result, "iso-2022-jp");
  return result;
}

/**
 * 文字化けの原因になる文字を、文字化けない文字に置換します。
 * http://d.hatena.ne.jp/y-kawaz/20101112/1289554290
 * http://perl.no-tubo.net/2011/01/12/perl%E3%81%A7%E3%83%A1%E3%83%BC%E3%83%AB%E9%80%81%E4%BF%A1%E6%99%82%E3%81%AE-%E3%81%84%E3%82%8F%E3%82%86%E3%82%8B%E6%B3%A2%E3%83%80%E3%83%83%E3%82%B7%E3%83%A5%E3%80%8C%E3%80%9C%E3%80%8D%E5%95%8F/
 * @param str
 * @param encoding 外部出力予定の文字コード(この値により置換テーブルが代わります)
 * @return
 */
public static String normalizeSimilarCharacter(String str, String encoding) {
    if(str == null || encoding == null) {
        return str;
    }
    encoding = encoding.toLowerCase();
    if("windows-31j".equals(encoding)) {
        return StringUtils.replaceEach(str, SIMILAR_CHARS_W31J_FROM, SIMILAR_CHARS_W31J_TO);
    } else if("shift_jis".equals(encoding)) {
        return StringUtils.replaceEach(str, SIMILAR_CHARS_SJIS_FROM, SIMILAR_CHARS_SJIS_TO);
    } else if("euc-jp".equals(encoding)) {
        return StringUtils.replaceEach(str, SIMILAR_CHARS_EUCJP_FROM, SIMILAR_CHARS_EUCJP_TO);
    } else if("iso-2022-jp".equals(encoding)) {
        return StringUtils.replaceEach(str, SIMILAR_CHARS_ISO2022JP_FROM, SIMILAR_CHARS_ISO2022JP_TO);
    }
    return str;
}

//共通置換テーブル
private static final String[] SIMILAR_CHARS_COMMON_FROM = new String[]{
    "\u00AD", "\u2011", "\u2012", "\u2013", "\u2043", "\uFE63", //半角ハイフン
    "\u223C", "\u223E", //半角波線→半角チルダ
    "\u22EF", //3点
    "\u00B7", "\u2022", "\u2219", "\u22C5", //半角中点
    "\u2225", "\uffe0", "\uffe1", "\uffe2"  // ‖ ¢ £ ¬
};
private static final String[] SIMILAR_CHARS_COMMON_TO = new String[]{
    "\u002D", "\u002D", "\u002D", "\u002D", "\u002D", "\u002D", //半角ハイフン
    "\u007E", "\u007E", //半角波線→半角チルダ
    "\u2026", //3点
    "\uFF65", "\uFF65", "\uFF65", "\uFF65", //半角中点
    "\u2016", "\u00a2", "\u00a3", "\u00ac"  // ‖ ¢ £ ¬
};
//エンコーディング別置換テーブル
private static final String[] SIMILAR_CHARS_SJIS_FROM;
private static final String[] SIMILAR_CHARS_SJIS_TO;
private static final String[] SIMILAR_CHARS_W31J_FROM;
private static final String[] SIMILAR_CHARS_W31J_TO;
private static final String[] SIMILAR_CHARS_EUCJP_FROM;
private static final String[] SIMILAR_CHARS_EUCJP_TO;
private static final String[] SIMILAR_CHARS_ISO2022JP_FROM;
private static final String[] SIMILAR_CHARS_ISO2022JP_TO;
static {
    SIMILAR_CHARS_SJIS_FROM = (String[]) ArrayUtils.addAll(SIMILAR_CHARS_COMMON_FROM, new String[]{
        "\uFF0D"/*全角マイナス*/, "\u00AF"/*長音符号*/, "\u2015"/*強調引用*/, "\u3030", "\uFF5E"/*波線*/
    });
    SIMILAR_CHARS_SJIS_TO = (String[]) ArrayUtils.addAll(SIMILAR_CHARS_COMMON_TO, new String[]{
        "\u2212"/*全角マイナス*/, "\uFFE3"/*長音符号*/, "\u2014"/*強調引用*/, "\u301C", "\u301C"/*波線*/
    });
    SIMILAR_CHARS_W31J_FROM = (String[]) ArrayUtils.addAll(SIMILAR_CHARS_COMMON_FROM, new String[]{
        "\u2212"/*全角マイナス*/, "\u2014"/*強調引用*/, "\u3030", "\u301C"/*波線*/
    });
    SIMILAR_CHARS_W31J_TO = (String[]) ArrayUtils.addAll(SIMILAR_CHARS_COMMON_TO, new String[]{
        "\uFF0D"/*全角マイナス*/, "\u2015"/*強調引用*/, "\uFF5E", "\uFF5E"/*波線*/
    });
    SIMILAR_CHARS_EUCJP_FROM = (String[]) ArrayUtils.addAll(SIMILAR_CHARS_COMMON_FROM, new String[]{
        "\uFF0D"/*全角マイナス*/, "\u2015"/*強調引用*/, "\u3030"/*波線*/
    });
    SIMILAR_CHARS_EUCJP_TO = (String[]) ArrayUtils.addAll(SIMILAR_CHARS_COMMON_TO, new String[]{
        "\u2212"/*全角マイナス*/, "\u2014"/*強調引用*/, "\uFF5E"/*波線*/
    });
    SIMILAR_CHARS_ISO2022JP_FROM = (String[]) ArrayUtils.addAll(SIMILAR_CHARS_COMMON_FROM, new String[]{
        "\uFF0D"/*全角マイナス*/, "\u00AF"/*長音符号*/, "\u2015"/*強調引用*/, "\u3030", "\uFF5E"/*波線*/
    });
    SIMILAR_CHARS_ISO2022JP_TO = (String[]) ArrayUtils.addAll(SIMILAR_CHARS_COMMON_TO, new String[]{
        "\u2212"/*全角マイナス*/, "\uFFE3"/*長音符号*/, "\u2014"/*強調引用*/, "\u301C", "\u301C"/*波線*/
    });
}