日本語は1文字何バイト?

こんにちは、wakです。秋ですね。寒いですね。

さて、今日もどこかから「英語は1文字1バイト、日本語は2バイト」といった雑な話が耳に入ってきて、「UTF-8で日本語はだいたい1文字3バイト!」と抗議していたのですが、エンジニアとして「だいたい」という言葉を使うのもまた雑な話です。どんな例外があるのかをまとめておくことにしました。

f:id:nurenezumi:20171109173329j:plain

1匹あたり数兆個の細胞からなる猫

基礎知識

コードポイント

Unicodeでは世界中全ての文字に個別のコードを振っています(これをコードポイントと呼びます)。アルファベットでもひらがな・漢字でも、絵文字でもヒエログリフでも全部です。このコードポイントは通常16進数で表し、 U+FFFF の形式で書きます。たとえば「A」なら 0x41 なので U+0041*1、「あ」なら U+3042 です。JavaScriptでは "\u0041", "\u3042" などと書けば直接文字リテラルを書いたのと同じことになります。

let hoge = "\u3042"; // let hoge = "あ"; と書くのと同じ

UTF-8の文字エンコーディング

Unicode以前の文字コードSJISEUCなどでは文字コードと文字エンコーディング(=バイト列として表現する方法)とが同一でした。たとえばSJISで「ソ」の文字コード0x835C ですので、ファイルに 0x83 0x5C と書けばSJISで「ソ」と書いたことになります*2

UTF-8では、この文字エンコーディング方法にちょっと面倒な方法を採用しています。つまり、コードポイントの範囲によってバイト数が変わるのです。

  • 1バイト: U+0000U+007F (ASCII文字。例: 「A」)
  • 2バイト: U+0080U+07FF (主にギリシャ文字アラビア文字など。例: 「¶」「Ψ」)
  • 3バイト: U+0800U+FFFF (日常的に使うほとんどの文字はここ)
  • 4バイト: U+10000U+1FFFFF (その他)
  • 5バイト: U+200000U+3FFFFFF (未使用)
  • 6バイト: U+4000000U+7FFFFFFF (未使用)

U+0000-U+FFFF までの文字を基本多言語面(BMP)と呼びますが、このBMPに入っていない文字は全てUTF-8で4バイトになります*3。また、ちょっと盲点になることもあるのですが、上に示したようにギリシャ文字などは2バイトになります。

これを踏まえた上で、さらに2つのUnicodeの仕様を見ていきましょう。

異体字セレクタ

「ワタナベ」さんの「邊」(U+908A)には細かいバリエーションがあることはご存知でしょう。たとえば二点しんにょうが一点しんにょうになった「邊󠄄」(U+908A U+E0104)、右上の「自」が「白」になった「邊󠄆」(U+908A U+E0106)などです(環境によっては全く同じように表示されるため、手持ちのAndroidで表示したスクリーンショットを貼っておきます)。

f:id:nurenezumi:20171109163959p:plain

これら異体字の数は数十種類にもおよび、別々のコードポイントを振ることは現実的ではありません。そこで、まず基底文字(基本となる文字)「邊󠄆」(U+908A)を書き、続けて見えない制御文字をもう1文字書くと字体が変わるという仕組みが作られました。これが異体字セレクタです。

つまり、「邊󠄄」(U+908A U+E0104)は1文字に見えますが実際は2文字分で、しかも2文字目はBMPからはみ出していて4バイトになるので、計7バイトになります。原理的には1文字8バイトまで行きます。

結合文字

Unicodeには、単独では使われず、他の文字とくっつけるための結合文字があります。たとえばひらがなの「か」(U+304B)に結合文字の半濁点(U+309A)を繋げると「か゚」(U+304B U+309A)という見慣れない文字になります。これは1文字ですが、実体としては2文字なので、こちらもやはりUTF-8で6バイトになります。

しかも結合文字には「何文字まで」という限度がありません。「か゚」の後ろに結合文字の「○」(U+20DD)を続けて書くと、1文字9バイトのこんな文字になります(今度はWindowsのWordpadで表示した画面キャプチャを示します)。

f:id:nurenezumi:20171109164024p:plain

こうなってくると「1文字は何バイトか」という議論自体がナンセンスになってきますね。

絵文字

泥沼に踏み込むことになるのでここでは触れませんが、絵文字ではこの異体字セレクタと結合文字がふんだんに使われています。絵文字をパーツごとに組み立てていくような仕組みが採られているため、「1文字」で何バイトになるか見当も付きません。この記事が綺麗にまとまっていました。

qiita.com

結論

というわけで、UTF-8ではどのような文字が3バイト以外になるかをまとめます。

ASCII文字

いわゆる半角英数字と記号は1バイトです。これはいいでしょう。

ギリシャ文字アラビア文字など

Wikipedia一覧がありました。この U+0080U+07FF の間の文字は2バイトになります。

第3・第4水準漢字の一部

JIS X 0213に含まれる第3・第4水準漢字の大半はBMP外に収録されています。具体的には「𠀋」(U+2000B) はBMPに入らない文字の一つで、UTF-8では4バイトになります。

異体字

「邊󠄄」(U+908A U+E0104)、「邊󠄆」(U+908A U+E0106)などの異体字です。1文字に見えますが実体は2文字なので最大8バイトになります。

結合文字

いくらでも文字がくっつくため、1文字何バイトになるか分かりません。「1文字」扱いすべきかどうかは要件によります。

まとめ

これですっきりしました。「日本語のほとんどはUTF-8で3バイトになる。ただし第3・第4水準漢字の大半は4バイト。記号・結合文字は最低3バイト。あとギリシャ文字とかは2バイトだよ」と言えばいいのですね。分かっているつもりのことでもきちんと調べると気持ちが良いものです。それでは。

*1:ASCIIコードと同じ

*2:この2バイト目は偶然にもSJISの「\」(0x5C)と一致しているため、様々な問題を引き起こしていました。興味のある人は「ダメ文字」で検索しましょう

*3:5バイト・6バイトになる領域にはまだ文字が定義されていません