前回、JavaでASCIIの制御文字の取り扱いについて記事にしました。
そして今回は、前回の対策を応用して、もっと厄介なサロゲートペア文字をJavaでゴニョゴニョする方法を2つ程ご紹介したいと思います。
サロゲートペア文字とは?
簡単に説明すると、全世界で文字コードを統一するため、Unicodeという文字コードが一般的に主流になっています。
通常1文字を2バイトで表現するのですが、2バイト(65536文字)では文字が足りなくなってしまったため、1文字を4バイトで表現する方法として「サロゲートペア」が誕生しました。
サロゲートペアは何が問題なの?
今ではそんなに意識することはなくなりましたが、古いシステムや古いデータを取り扱う際に問題が発生してしまいます。
と言うのも、昔はUnicode以外の文字コードも多く使われていました。
それ故にバグ、いわゆる文字化けが発生してしまうのです。
これによって、データの不整合が起きたり、プログラムが想定したとおりに動かなかったりします。
サロゲートペア文字一覧
実際、サロゲートペア文字にはどのような文字があるのか、Qiitaの記事をご覧ください。
サロゲートペア文字を置換・削除する方法
前回のASCII制御文字の方法を応用して、2つの方法でサロゲートペア文字を変換していきます。
以下の2つの例では、「𩸽(ほっけ)」サロゲートペア文字を「口(くち)」という漢字に置換します。
1. 正規表現を利用する
前回ご紹介したJavaの正規表現一覧が掲載されている海外のサイトを見ると「Unicode Categories」にサロゲートペア文字を判定する正規表現がありました。
https://regular-expressions.mobi/unicode.html?wlr=1
1 2 3 |
・・・ p{Cs} or p{Surrogate}: one half of a surrogate pair in UTF-16 encoding. ・・・ |
p{Cs}
かp{Surrogate}
どちらでも同じ動きをするようですね。
これをreplaceAll()
で使うとこんな感じです。
1 2 |
String str = "𩸽"; str.replaceAll("\p{Cs}", "口"); |
そして、変換してみた結果がこれ。
1 |
口口 |
2文字になった!?
なるほど…。
前述の通り、通常1文字を2バイトで表現しているので、1文字4バイトであるサロゲートペア文字は2文字扱いになってしまうようです。
そこで、上位サロゲートと下位サロゲートに分けて変換することにします。同じく上記のサイトを見ると「Unicode Blocks」の92.と94.に上位サロゲート(InHigh_Surrogates
)と下位サロゲート(InLow_Surrogates
)を判定する正規表現がありました。
この辺りです。
1 2 3 4 5 |
・・・ 92. p{InHigh_Surrogates}: U+D800–U+DB7F 93. ・・・ 94. p{InLow_Surrogates}: U+DC00–U+DFFF ・・・ |
上位サロゲートだけ、または下位サロゲートだけ変換してしまうと、どちらかの2バイト分が残ってしまうので、片方は削除します。
1 2 3 |
String str = "𩸽"; str.replaceAll("\p{InHigh_Surrogates}", "口"); str.replaceAll("\p{InLow_Surrogates}", ""); |
上記の例では、上位サロゲートを置換し、下位サロゲートは空文字にしました。
当然、結果は1文字で思った通りになりました。
1 |
口 |
ただ削除するだけの場合は、前者の方法で1発で便利ですね。
置換する場合は後者の方が応用が利くかもしれません。
2. 1文字ずつ判定する
続いて、こちらも前回の応用で、1文字ずつループしてサロゲートペア文字を判定します。
判定方法はJavaのCharacterクラスに用意されている以下の関数を使います。
- isSurrogate()
- isHighSurrogate()
- isLowSurrogate()
詳しくはこの辺りを参考にしてね!
前回同様、関数を作ってみました!
1 2 3 4 5 6 7 8 9 10 11 12 |
public static String replaceSurrogate(String str) { if (str == null) { return str; } StringBuffer sb = new StringBuffer(); for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if (Character.isSurrogate(str.charAt(i))) { sb.append("口"); } String result = sb.toString(); return result; } |
もちろん、これだけだと正規表現と同じように「口口」2文字になってしまいますね。
ですので、こちらも同じように上位サロゲートのみ置換、下位サロゲートは空文字にしてしまいましょう。
以下、抜粋。
1 2 3 4 5 6 7 8 |
・・・ if (Character.isHighSurrogate(str.charAt(i))) { sb.append("口"); } if (Character.isLowSurrogate(str.charAt(i))) { sb.append(""); } ・・・ |
または、上位サロゲートを置換した後、ループを進めて下位サロゲートを無視する方法もありますね!
1 2 3 4 5 6 |
・・・ if (Character.isHighSurrogate(str.charAt(i))) { sb.append("口"); i++; } ・・・ |
以上、こんな感じでいかがでしたでしょうか。
方法は1つじゃないですね。
前回も書きましたが、正規表現は遅いらしいですが、コンマ何秒の世界なので自分は好きな方で良いと思います。
コメント