エンジニア的なネタを毎週書くブログ

東京でWebサービスの開発をしています 【英語版やってみました】http://taichiw-e.hatenablog.com/

U+FFFF以上の文字ってなんや → サロゲートペアってなんや → Spring Web Services が言うことを聞きません! とかで一日潰れた話

表題のような感じなのですが、これまで理解が曖昧だったUnicodeとか何とかが今までよりわかったのでメモ。

尚、こちらのサイトを非常に参考にさせていただきました。

Unicodeについて

コードポイントとは 文字コードとは

今日覚えた単語その一。Unicodeに限らず、文字をコンピュータ上で表現する際、1つの文字に1つの数値を対応させるわけですが、この文字に対応する数値をコードポイントというそう。
いままでASCIIコードとか呼んでました。

そして、文字と数値の割り当てのルールのことを「文字コード」と言うんだそうです。

Unicodeとは から UTF-XXは何が違うんじゃ という話へ

Unicode誕生

文字コードが乱立したため、あるコードポイントで表現される文字が、文字コードによって、てんでばらばらという状況に。

ややこしいから、ひとつの統一した文字コードをつくろう! ということで「Unicode」が作られることになりました。

Unicodeを使うときは、U+12ABのように16進数の先頭にU+をつけて表すことにして、
U+0000~U+FFFFの216パターンで世の中の文字を表そう!ってことでUnicode1.0が生まれたそうです。

サロゲートペアの誕生

ところが、特にアジア圏の文字を表そうとしたらこれでは全然足りなくて、U+10FFFFまでUnicodeを拡張することになりました。

しかしこれだと1文字表現するのに21bit必要で、2nじゃないのでとっても切りが悪いという事態に。

そこで、もともとUnicode1.0で使われていた16bitでなんとか表現するために、

たまたま未使用だったD800H~DFFFHを2つ組み合わせ、32bitを使ってU+10000以上を表現するという、むりくりな方法が発明されたそうです。これが「サロゲートペア」。

で、U+0000~U+FFFFの文字をBMP(Basic Multilingual Plane, 基本多言語面)、U+010000~U+10FFFFまでの文字をSMP(Supplementary Multilingual Plane, 追加多言語面)と呼ぶことにしたそうです。

UTF-XX

上記のような経緯を踏まえ、実際にデータ上でどう表すかという、エンコード方式がいくつか考えられたそうです。

UTF-16 → 上の通りのサロゲートペアをそのまま実装した、一文字に16bit使うエンコード方式。一番正直。

UTF-32 → サロゲートペアはややこしいので、すべての文字に対して32bitを割り当てた方式。一番わかり易いけど、一番無駄が多い。

UTF-8 → 文字に合わせて、8bit, 16bit, 24bit, 32bitのどれかを使うようにしている。Unicodeの中では無駄が一番少なく、最も普及している。

JavaでのSMP

少し話変わってJavaのお話。

突然ですが、Javaのプリミティブ変数のchar型は、上限が (char) 0xFFFF なので、なんとSMPに割り当てられた文字が表現できないそうです!

そこで、Java5.0以降、上記のサロゲートペアの考え方を流用して、SMPは、char2つで1文字を表すようにしています。

Javaでのサロゲートペアの扱いについてはこの記事がわかりやすかったです。

Java とサロゲートペアについて - にょきにょきブログ

Spring Web Serviceが言うことを聞きません!

…と、ここまで説明してようやく私の身に起こったことを。

いろいろありまして、XML上にU+1F6ADのようなSMPを出力する必要がありました。

XMLの仕様的には単純で、数値文字参照を使って 🚭 とか 🚭 と表現できればOKです。

一方、私達のサービスではSpring Web ServiceというSOAPアプリのためのフレームワークを使っています。

こいつ、普段はなかなか便利な子でして、EndPointのレスポンスとしてBeanをReturnすると、そのBeanの階層構造をそのまま反映したXMLを作ってくれるのです。
また、XML上必要なエスケープも自動で行ってくれて、たとえば「&」が文字列の値として含まれていたら、XMLを出力する際にはきちんと「&」と出力してくれる、という具合です。

では、このSpring Web Serviceを使った時に、
レスポンスのBeanに含まれる hoge フィールドに、🚭(U+1F6AD)が入っていたらどうなるか!?

<hoge>&#55357;&#57005;</hoge>

なんじゃこりゃぁ!?

55357 = D83DH, 57005 = DEADHで、サロゲートペアに分解された上で実体参照として表現されています。

おそらく、String hoge 内のcharの配列を頭から順に1文字ずつ処理してるんだろうなぁ… と推測されます。

更に良くないことに、D800H~DFFFHはUnicodeでは単独で存在できない文字なので、上記&#55357;や&#57005;はXML上使えない文字で、これらが含まれたXMLは正しくパースされません。

 

結局現状で、XMLを破壊しないためには、Character#isSupplementaryCodePoint methodを使って、SMPを除外する以外方法がなさそう。

うーむ…。

おまけ Oracle上では

じゃあこの禁煙マークがOracle(UTF-8エンコードで)に入ってた場合どうなるか

select ascii(hoge) from hoge_table;

-> 4036991661

おうふ。

これは、4036991661 = F09F9AADH でして、SMPをUTF-8で表現する場合、やたらややこしいルールで、やたらでかい数値に変換されるため…みたいです。