Unicodeについて

Maxyは、Unicode対応のテキストエディタです。 ところで、そもそもUnicodeって何なのでしょうか。 UnicodeUTF-8とかUTF-16との関係は? そのあたりのことを簡単に説明していきます。

文字コードとは

まずは基本中の基本だけど、(現在のところ)コンピュータが扱えるのは数値のみという大前提がある。 何をするにも数値をいじるしかない。 きれいなCGも、優雅な音楽も、つまるところ数値なわけだ。 「ここに100を書き込んで、ここに150を書き込んで…」と、コンピュータが必死になっていろんなところに数値を書き込んだ結果としてCGが表示されたり音楽が流れたりするわけ。

そんなわけだから、当然コンピュータ内で文字を表現するにも数値が必要で、あらかじめ「この文字はこの数値で表す」という取り決めをする必要があるんだ。 その取り決め(文字⇔数値の対応)のことを文字コード、1つ1つの文字に割り当てられた数値のことをコードポイントと呼んでいる。 英数字とか記号とかの基本的な文字を集めた文字コードがASCIIと呼ばれるもので、現在使われてる文字コードのほとんどはこのASCIIを基に拡張されている。

ASCIIは、7bit(27=128通り)で文字を表現している(昔は8ビット目、つまり最上位ビットは誤り検出用のパリティビットだったらしい。今は0だけどね)。 例えば、文字"A"のコードポイントは41H、といった具合。 アルファベットは大文字・小文字がそれぞれ26文字ずつ。 さらに数字が0~9までの10通り。 それにいろいろな記号を加えても128通りあれば十分にお釣りがくるってわけ。 コンピュータはアメリカを中心に発達してきたから、当面はそれだけで十分だった。 英数字しか使わないアメリカ人にとっては「文字コード=ASCII」という認識で何も困らない。

日本語を扱うには?

月日は流れ、日本でもコンピュータが発達してきた。 ただし、日本語はASCIIのように都合よくいかない。 ひらがなだけでも50文字。 カタカナを含めるとその倍の100文字。 これにさっきの英数字を加えるだけで、7bitなんて軽くオーバーしてしまう。 さらに、日本語には「漢字」なんていうものまであるから、どう逆立ちしても従来のASCIIではまかない切れない。 そこで、「英数字は1byteで、日本語は2byteで表そう!」ということを誰かが考えた。 つまり、こういうこと。

  1. ASCII範囲内の文字(00H7FH)は、そのままASCII文字として扱う
  2. ASCII範囲外の文字(80HFFH)が現れたら、次の1byteと組み合わせて2byteの数値を作り、その数値に文字を割り当てる

これはなかなかうまい考え。 何がうまいかって、この方法だと従来のASCIIで書かれた文書も読めるし、日本語と英数字が混じった文書も矛盾なく読めるから。

文字化け

ただし、ここで問題が発生。 コンピュータはパソコンやワークステーションなど、いろいろな方面から発達してきたから、日本語の文字コードもパソコンとかワークステーションで独自に発展してきちゃったわけ。 例えば、パソコンで一般に使われる日本語の文字コードはShift-JIS、ワークステーションはEUC-JPといった具合。 別々に発展してきたから、同じ文字でも「あ」のコードポイントはShift-JISでは82A0Hだけど、EUC-JPではA4A2Hといった具合に別々のコードポイントが割り当てられちゃったのだ。

ということは、逆に考えれば2つの文字コードの全く別の文字のコードポイントがたまたま同じ値を指し示すこともあるってこと。 例えば、EUC-JPで「表示」という文字は[C9 BD BC A8]というデータ列で表されるけど、これを間違ってShift-JISで読み直したら「ノスシィ」とかいうわけのわからない文字になってしまうわけ。 どこのSNSだよ。 それとも結婚情報誌か? っていうか、Maxyも似たような響きじゃねーか。 ちくしょう。

とにかく、これが文字化けと呼ばれる現象。 要するに、文字コードの判定ミスからおきる問題ってことです。 実際は他の要因で起こることもあるけど、ほとんどは判定ミスが原因。

余談だけど、今ではmojibakeは英単語として通用するらしい。 それもそのはず、ASCIIだけで十分なアメリカでは2種類以上の文字コードを使う必要はなく、当然文字化けなんて問題は起こらない。 そもそも文字化けなんていう概念すらなかった。 日本でコンピュータが発達してきて初めて直面した問題なわけだ。

多言語の混在

さらに、コンピュータは日本だけじゃなくて韓国とか中国とかでも使われるわけで、当然それぞれの国に独自の文字コードがある。 だけど、自分の国の文字(と英数字)以外のことは何も考えてないもんだから、

「ありがとう」を中国では「谢谢」、韓国では「고마워요」と言います。

みたいな、いろんな国の文字が混ざった文書は書けなかったりする。 これじゃ国際化も何もあったもんじゃない。

Unicodeの誕生

こんな感じでいい具合にカオスになってきた文字コード。 そんな時、ゼロックスが「言語ごとに文字コードがあるのはややこしい。世界中の文字をすべて含んだ新しい文字コードを作ろう!これからはみんなでその文字コードを使えば1つの文書内で多言語を混在できるし文字化けの心配もしなくて済むしみんな幸せだ!」と言い始めた。 これがUnicode単一の文字コードという意味)の始まり。 そして、ユニコード・コンソーシアムとかいう組織が作られた。 ユニコード・コンソーシアムには、マイクロソフトアップルIBMサン・マイクロシステムズヒューレット・パッカードといった錚々たる大企業が参加してるそうな。 日本からはジャストシステムも参加してるらしい。

最初は「これからは、全ての文字を16bit(216=65536通り)で表そう!それなら世界中の文字を含めることができる!」と張り切ってた。 そして「Unicodeを使う時は、コードポイントはU+12ABのように16進数の頭に"U+"をつけて表そう!」という決まりを作った。 そんなこんなで1991年に当面必要な文字だけを収録した16ビットの文字コードとしてUnicode 1.0が公表されて、Unicodeは順風満帆かと思われた。

でも、残念ながら世の中そんなに甘くない。 日本人ならその理由はなんとなくわかると思う。 日本で普通に使われる常用漢字だけで2000字近くあるし、漢字検定1級で出題される可能性がある漢字は6000文字らしい。 ちなみに、漢字の本場・中国の漢字字典「康熙字典」では50000字近い漢字が収録されている。 一生かかっても憶えられる自信なんてありません。 漢字だけでこんな状態だから、タイとかベトナムとかシンガポールとか古代文字(何しろ「世界中の全ての文字」を表そうとしてるんだから…)なんかを入れると当然16ビットなんかじゃ全然足りないわけで。 Unicode 1.0の公表後に、いろんな国から「この文字も追加しろ!」と言われちゃって、「世界中の文字を16bitで表す」という野望は露と消えてしまいましたとさ。 諸外国(特にアジア)がこんなにたくさんの文字を使っているとは、欧米人は夢にも思わなかったみたい。

「16bitで足りないなら17bitとか18bit使えばいいやん!」と言いたいところだけど、残念ながら17bitとか18bitとかはコンピュータで扱うのが面倒だったりする。 もともとUnicode(というか文字コード自体)はコンピュータで文字を表現するためのものだから、コンピュータで扱いにくい文字コードにしたらそれこそ本末転倒。

コンピュータで扱いやすいサイズといえば、やっぱり8bit単位。 もっと細かく言うと「8x2nbit単位」が扱いやすい。 だから8bit(8x20)とか16bit(8x21)とかは無問題なわけ。 で、16bitが使えないとなると、次は32bit(8x22)になる。 これだと40億通り以上(232=4294967296)の文字を表せるから十分世界中の文字を収録できる…んだけど、何しろ無駄が多すぎる。 当面必要な文字は16bit内に収まっていて、これ以外の文字が必要になることはほとんどないのに常に1文字につき32bit使っていたら無駄無駄無駄ァァ!! 16bitでは足りなくて32bitでは多すぎる。 絵に描いたような「帯に短し襷に長し」状態。

サロゲートペア

実は、Unicode 1.0でも65536通りの全てに文字を割り当ててたわけじゃなくて、ところどころ空きがあったりする。 その中に、D800HDFFFHという2048文字分の空き領域がある。 そして「この範囲の文字が出てきたらちょっと特殊なことをしよう」と考えた。 どう特殊かというと、「前半1024文字(D800HDBFFH)のどれかが出てきたら、その次には必ず後半1024文字(DC00HDFFFH)のどれかを出すようにしよう!これで表せる文字が100万文字(1024x1024=1048576)くらい増えた!」というわけ。 で、この「2つで1文字を表す」ペアのことをサロゲートペアと呼ぶことにしましたとさ。

つまり、文字データの中にD800HとかD801Hとかが出てきたら、その次は「DC00HDFFFHのどれか」が必ず出てくる、というルールを決めておく。 そうすれば、「D800Hに対して1024通り(DC00HDFFFHのどれか)」、「D801Hに対して1024通り」…「DBFFHに対して1024通り」の表しかたがあるので、結局1024x1024=1048576通り増えるということ。 何のことはない、Shift-JISとかの日本語文字コードで普通にやってたのと似たような方法じゃん。

これでUnicodeで表せる文字範囲はU+0000U+10FFFFまで広がった。 16ビットで表せる範囲内の文字(U+0000U+FFFF)は「基本多言語面(BMP)」、残りの100万文字余り(U+010000U+10FFFF)は「追加多言語面(SMP)」として区別することにしたそうな。

エンコード方式

当初の構想では16bitで全ての文字を表そうとしていたUnicodeだけど、仕様がここまで複雑になってしまったので効率的なエンコード、つまり8bitのデータ列で表す方法を考えなきゃいけなくなった。 つまり、16bitだけで全てが表せるなら何も考えずに全ての文字を16bitで表せばよくて、あとはエンディアンの問題だけを考えればよかったんだけど、サロゲートペアなんでものが出てきて中途半端に32bitも使うことになってしまったわけだから、なんかうまい方法を考えなきゃ、というわけ。

そこで出てきたのがUTFというエンコード方式。 ただ、このUTFもいくつか種類があって、それがまた混乱に拍車をかけてたりする。

UTF-16
上で書いたことを素直に表現したもの。 つまり、「BMP内の文字は16bit、それ以外(SMP)の文字はサロゲートペアを使った32bitで表す」というルール。 最近のWindows®では文字の内部表現にUTF-16を使ってる。 ちなみにWindowsで「ワイド文字」といえばUTF-16のこと。
UTF-32
サロゲートペアなんて面倒だ!1文字32bitで固定したほうが容量は食うけどシンプルでわかりやすい!」ということで、1文字を常に32bitで表すことにしたもの。 でも、無駄すぎるので実際はあんまり使われてない。
UTF-8
UTF-16はややこしい。UTF-32は無駄すぎ。もうちょっと頭を使おうよ」ということで、1文字を8, 16, 24, 32bitのどれか(文字によって異なる。理論上48bitまで拡張可能だけど、Unicodeの定義上32bitまで)で表現したもの。 変換方法はちょっと面倒だから省略。 文字によって1文字のサイズが変わるのがなんか気持ち悪いけど、UTF-16だってどうせサロゲートペアがあるんだからお互い様。
このUTF-8はなかなかよく考えられたエンコード方式で、英数字は今までどおりの8bitで表せるもんだから、英数字がほとんどの文書(英語の手紙とかプログラムとか)ではUTF-16よりもずっと容量が小さくてすむ。 それに、英数字だけの文書は、実は今まで使ってたASCIIとまったく同じ表現ですむから、今まで大量に生産されてきた英語のドキュメントとかプログラムのソースコードをそのまま使えるのでとってもありがたい。
他にも「入出力が8bit単位だからエンディアンを気にしなくていい」とか「文字の境界が明確だから、文字検索時に境界とは別の場所にヒットすることがない」とかいろいろ優れたところがあるわけ。 そんなわけでちこちでバカウケして、いろんなところで使われてるらしい。 ちなみにこのドキュメントもUTF-8でエンコードしてたりする。
ただし、この方法で日本語を表すと24bitになることが多いから、典型的な日本語の文書だとShift-JISとかUTF-16より1.5倍容量を食っちゃうことになる。 それに、24bitっていうのはコンピュータでは微妙に扱いにくい。 ていうか24bitのデータっていうものがアリなら、全データを24bit固定にしたUTF-24とかいう方式でも作ればええやん…と思うんだけど、それは素人の浅知恵なんだろうか(もしかしてすでにあったりして。詳細を知ってる人がいたら教えてください)。

実際には他にもUTF-7とかUTF-EBCDICとか、さらにはUTF以外のエンコード方式もあるけど、あんまり気にしなくていい。 ていうかよくわかんない。

余談だけど、「BMP内の文字だけを16bit固定で表した文字コード」をUCS-2と呼ぶ。 つまり「UTF-16のサロゲートペアがないやつ」。 BMP外の文字はよほどのこと(研究目的とか)でなければまず使われることはないので、普通に使う分にはこれで十分なのだ。 ちなみにMaxyも、内部コードは事実上UCS-2だったりする。 APIの仕様上、BMP外の文字の表示・編集もできるけど、カレットが変なところに来ることがあるよ。

ここまでくると、「UnicodeUTF-16は同じなんだよね?UTF-8とかUTF-32Unicodeとは違うの??」と混乱する人がいるかもしれない。 そこで、もう一度おさらい。

Unicodeとは、ASCIIShift-JISのように、世界中のあらゆる文字を単一の文字コードで表そうとして策定された文字コード自体を指す。

で、UnicodeはU+0000~U+10FFFFの21bitで表されるけど、コンピュータでデータを扱うときは8bit単位が一番扱いやすい。 コードポイントを8bitのデータ列にエンコードするための方式がUTFで、具体的にはUTF-8UTF-16UTF-32の3つがあるということです。

要するに、Unicodeというのは世界中の文字を数値に置き換えた際の数値を指し、UTFというのはそれを8bitのデータ列にエンコードする方式のことを指していると考えればいい。

当然同じ文字でもエンコード方式が違えば異なるデータ列になる。 例えば、「𠀋(「大丈夫」の「丈」とは違うよ)」という文字を考えてみよう。 これはBMP外の文字で、コードポイントはU+2000B。 そしてこれをUTF-32のデータ列で表すと[0002000B]UTF-16では[D840 DC0B]UTF-8では[F0 A0 80 8B]となる。 表現方法は違うけど、どれも同じU+2000Bを指してるってこと。

BOM

結局、1つのコードポイントをデータ列で表現するのにUTF-8とかUTF-16とかUTF-32とかいろいろな方法ができて、ややこしくなっちゃった。 だって、コードポイントが共通でも、バイト列が何種類もあったらやっぱり誤認識・文字化けの問題は出てくるからね。

そこで、「どの表し方を使っているのかという情報」をデータの最初に置くことを誰かが考えた。 つまり、「これから始まるデータ列はUTF-8だ!」といった情報をデータの最初につけておけば、正しく読み取れる。 だから文字化け問題も解消する!というわけ。 そのデータがBOM。 その実態は、コードポイントU+FEFFをそれぞれの方式でエンコードしたデータ列。 だから、文書の先頭にこのデータ列が出てきたら「これはBOMなんだな」と判断できるわけ。

本来は「バイトオーダーマーク」という名の通り、データのバイトオーダー、つまりビッグエンディアンリトルエンディアンかを識別するためのマークだったんだけど、結果的にこのBOMによってエンコード方式までわかるようになったから、エンコード方式の判別にも使われてるとゆーこと。

UTF-16なら[FE FF]UTF-32なら[00 00 FE FF]UTF-8なら[EF BB BF]というデータ列になる。 正確に言うなら、ビッグエンディアンのUTF-16[FE FF]、リトルエンディアンのUTF-16[FF FE]、ビッグエンディアンのUTF-32[00 00 FE FF]、リトルエンディアンのUTF-32[FF FE 00 00]って感じ。 UTF-8はそもそもバイトオーダーに依存しない仕様だから本来の意味(バイトオーダーを示す)でのBOMじゃなく、エンコード方式を表明するためのBOMなんだけどね。

あー、ややこしくなってきた。 まとめておこう。

エンコード方式とBOM
エンコード方式 BOM
UTF-8 EF BB BF
UTF-16 ビッグエンディアン FE FF
リトルエンディアン FF FE
UTF-32 ビッグエンディアン 00 00 FE FF
リトルエンディアン FF FE 00 00

うん、表にするとスッキリ。 要するに、先頭の4byteを読み込んだ時点で文字コードとバイトオーダーがわかるというスグレモノなわけだ。 U+0000は普通の文書には現れないコードポイントだから、リトルエンディアンのUTF-16UTF-32を間違える心配はない(リトルエンディアンのUTF-16[FF FE 00 00]とゆーデータ列がくることはない)。 逆に、先頭2byteだけだと両者を区別できないから、きちんと4byte読んで判断してね、ということ。

BOMは必ずしもつける必要はないんだけど、その場合はビッグエンディアンにするように決められてる。 ただ、BOMをつけないと認識ミス(≒文字化け)が起こる可能性があるからつけたほうが無難かな。

そして今…

そんなこんなで、Unicodeのバージョンは今や5.1.0(2008年6月現在)にまで上がっている。 一時はハングル文字のコードがごそっと別のところに割り当てられたり(ハングルの大移動)、それなりの混乱もあったみたい。 仕様もどんどん複雑化していて、合成文字とか双方向文字とか、もうお腹いっぱい。

がんばれ、Unicode。 君はこれからどこへ行く?

資料