背景
ユーザーにパスワードを入力してもらう時にクライアント側で絵文字の入力を禁止したかったので、雑に絵文字入力を判定するにはどうすればいいか考えました。
無理やり絵文字の入力を許すような仕組みにすることもできたかもしれませんが、絵文字は同じデータ値であっても環境によって表示される画像が違っていたり、サポートの有無もあるはずなのでパスワードとして使うのは適切でないでしょう。
事前知識
そもそも文字コードに関しての知識もあやふやだったので、それついて理解することから始めました。ここで簡単にまとめてみたいと思います。
文字コード
文字コード(もじコード)はコンピュータ上で文字(キャラクタ (コンピュータ))を利用するために各文字に割り当てられるバイト表現。もしくは、バイト表現と文字の対応関係(文字コード体系)のことを指して「文字コード」と呼ぶことも多い。
引用:Wikipedia
つまり、文字データを画像や図形として扱うのではなく、各文字に符号割り当てることによってテキストデータを扱う表現のこと。あるいは各文字と符号との対応関係のことを「文字コード」と呼んでいるようです。
UnicodeとUTF8
これらがそれぞれ何を指しているものなのかちゃんと理解していないことが問題でした。
文字コードは文字集合(文字セット)と文字符号化方式の2つの概念から成り立っています。これらの概念は以下のように説明することができると思います。
- 文字集合
- 「全てのアルファベット」(a, b, c, ..., z, A, B, C, ..., Z)や「全てのひらがな」(あ, い, う, ..., ん)などのような、文字コードで表現したい文字の範囲
- 文字符号化方式
- 文字集合中の各文字をバイト列に変換するための符号化方式。各文字とビット値との対応付け
普段私達が日常的に使うのは日本語文字やアルファベットが中心ですが、コンピュータは世界中で動いているので中国語や韓国語、アラビア語も表現できなくてはなりません。
こういった表現したい文字範囲を文字集合によって定め、それぞれの文字に対してコンピュータ上でどのような数値を割り当てるかを定めたのが符号化方式です。
つまり、Unicodeは文字集合であり、UTF-8は符号化方式ということです。
Unicode
UnicodeではU+
の後に16進数値を続けることで、文字集合中における各文字の位置(符号位置)を表現します。例えば、アルファベットの「a」はU+0061といった具合に”位置”が定義されています。
日本語のひらがなのためにはU+3040~U+309F の範囲が用意されており、「あ」の符号位置はU+3042です。
文字 | 符号位置 |
---|---|
a | U+0061 |
あ | U+3042 |
字 | U+5B57 |
基本的な文字は U+0000 ~ U+FFFF の範囲の$16^4$(=65536)種の中に収まるということで、基本的には16bitで1文字を表現することができます。
UTF-8
UTF-8は文字集合Unicodeに使える符号化方式です。
文字コードには、文字集合は等しいが符号化方式だけが異なる文字コードと、そもそも対象としている文字集合そのものが異なる文字コードがある。たとえば、日本語には JIS X 0208 というひとつの文字集合に対して ISO-2022-JP (JIS コード等と呼ばれることが多い)、EUC-JP、Shift_JIS など複数の符号化方式が存在する。Unicode にも単一の文字集合に対して UTF-8、UTF-16、Punycode などの異なる符号化方式が存在する。
引用:Wikipedia
文字集合Unicodeに対して複数の符号化方式が存在していますが、UTF-8はその一つです。
符号位置であるU+3042などは文字集合中の位置を表しているものなので、実際にデータとして文字を表現するには各符号化方式によってエンコードしたバイト列を使う必要があります。
UTF-8やUTF-16は異なる符号化方式ですが、同じUnicode文字集合の符号化方式なので、各文字を表現する値は異なりますが、表現できる文字は同じということになります。
文字 | UTF-8 | UTF-16 |
---|---|---|
a | a | 61 |
あ | E38182 | 3042 |
字 | E5AD97 | 5B57 |
UTF-16はUnicodeと同じ16bitの表現なので符号位置を表す値と同じような値になってますね。
サロゲートペア
単純なUnicodeの16bit表現だと65,536通りの文字しか表現できません。英数字だけであれば7bitあれば十分まかなえる程度の数しかありませんが、世界各国の文字を表現することを目標としてUnicodeからするとこれでは全然足りんということになったようです。
中国語の漢字辞典にはなんと10万種を超える漢字が収録されているものがあるようなので、他の言語のことも考えるとその足りなさ加減がわかるかと思います。
そこで導入されたのがサロゲートペアという概念です。
これは2つのUnicode符号位置を連結させることで1つの文字を表現しようというもので、その拡張のために用意された領域が U+D800 ~ U+DFFF(前半 U+D800 〜 U+DBFF、後半 U+DC00 〜 U+DFFF)です。前半1024、後半1024個分領域がこれに当てられています。
前半 U+D800 〜 U+DBFF のどれかが出てきたら、後半 U+DC00 〜 U+DFFF のどれかが次に続くというルールにしておき、2つで1文字を表現するということに成功しました。
組み合わせの数を考えると、$1024 * 1024 = 1048576$ の数だけ表現できる文字種が増えたことになります。
追加された100万余りの領域として、U+010000 ~ U+10FFFF が新たに追加面として定義されました。
実装
さて、では実際にどうやって絵文字を判定するかということで、絵文字がUnicode上のある特定の領域にまとまっていればまとまっていればその範囲の文字が含まれているかどうかを判定すればよいなと考えました。
そこで追加面の領域に注目してみると、ここに収録されている文字は古代文字や絵文字、基本多言語面に入りきらなかった漢字などということになっています。
ので、日常的にはあまり使われることのない追加面の文字、つまりサロゲートペアで表現される文字を全て弾いてしまおうということにしました(雑…
基本多言語面にも記号などは含まれているのですが、まあそれはいいとしてサロゲートペアを弾こうと…
以下がそれを判定するために用意したメソッドです。
public static boolean isSurrogate(String text) {
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (Character.isHighSurrogate(c) || Character.isLowSurrogate(c)) {
return true;
}
}
return false;
}
与えられたStringを前から1文字ずつ確認しています。
便利なことに、Charcterクラスに判定のためのメソッドが用意されていました。
public static final char MIN_HIGH_SURROGATE = '\uD800';
public static final char MAX_HIGH_SURROGATE = '\uDBFF';
public static final char MIN_LOW_SURROGATE = '\uDC00';
public static final char MAX_LOW_SURROGATE = '\uDFFF';
public static boolean isHighSurrogate(char ch) {
return (MIN_HIGH_SURROGATE <= ch && MAX_HIGH_SURROGATE >= ch);
}
public static boolean isLowSurrogate(char ch) {
return (MIN_LOW_SURROGATE <= ch && MAX_LOW_SURROGATE >= ch);
}
char配列、StringクラスおよびStringBufferクラスではUTF-16表現が使われいるので、このような実装になっていることが理解できるかと思います。
以上、門外漢なりに文字コードについて勉強したことをまとめてみましたが、ホントはもっと考慮しないといけない点があったかもしれません。その点に関しては必要性が判明した時にまたやってみようかと。