世界を動かす技術を、日本語で。

UTF-8は素晴らしい設計です

概要

  • UTF-8 は、数百万文字をカバーしつつ ASCII との互換性を維持する設計
  • 1〜4バイトの可変長エンコーディングで Unicode 全体を表現可能
  • ASCIIファイルは常にUTF-8ファイルとして有効 であり、その逆も成り立つ場合がある
  • バイトの先頭ビットパターンで 文字の長さと種類 を判別
  • 実例や他エンコーディングとの比較で UTF-8設計の優秀さ を解説

UTF-8の設計が優れている理由

  • UTF-8 は、世界中の言語・文字体系をカバーする Unicode 文字集合を、1〜4バイトで表現する可変長エンコーディング
  • 最初の128文字(U+0000〜U+007F)は 1バイト で表現され、ASCIIと完全互換
  • ASCIIのみのファイルは、そのままUTF-8ファイルとして有効。逆もまた、ASCII文字しか含まないUTF-8ファイルはASCIIファイルとして扱える
  • 数百万文字への拡張性と、既存のASCII資産との共存を両立する 設計思想の妙
  • 既存システムとの互換性を保ちつつ、将来の多言語化にも対応

UTF-8の仕組み

  • バイトの先頭ビット で、その文字が何バイトで構成されるかを判断
    • 0xxxxxxx → 1バイト(ASCII)
    • 110xxxxx → 2バイト
    • 1110xxxx → 3バイト
    • 11110xxx → 4バイト
  • 2バイト以上のとき、2〜4バイト目の先頭は常に 10 で始まり、「継続バイト」であることを示す
  • 先頭バイトと継続バイトの残りビットを連結し、 Unicodeコードポイント を生成
  • コードポイントは通常 16進数 で表し、"U+"で始まる(例:U+0041は"A")

デコード手順

  • バイトを1つ読む
    • 0で始まる→ASCII文字、残り7ビットで表示
    • 110, 1110, 11110で始まる→2, 3, 4バイト文字なので、必要なだけ継続バイトを読む
  • 先頭ビット以外のビットを全て連結し、 バイナリ値=コードポイント を作成
  • Unicode表 から該当文字を特定・表示

実例:ヒンディー語の「अ」

  • "अ"(Devanagari Letter A)は UTF-8では3バイト (11100000 10100100 10000101)
  • 先頭ビットを除いたビットを連結→00001001 00000101(16進で0x0905)
  • U+0905 が「अ」を表すUnicodeコードポイント

実例:テキストファイルのバイト解析

1. 「Hey👋 Buddy」を含むファイル

  • 英字+絵文字(👋)を含む13バイト
  • 各バイトの先頭ビットを見て、1バイト文字(ASCII)と4バイト文字(絵文字)を判別
  • 👋は4バイト(11110000 10011111 10010001 10001011)で U+1F44B に対応

2. 「Hey Buddy」のみ(ASCIIのみ)のファイル

  • すべてのバイトが0で始まる→ 全て1バイトASCII文字
  • このファイルは UTF-8としてもASCIIとしても有効

他のエンコーディングとの比較

  • GB 18030 (中国標準)など、ASCII互換な他のエンコーディングも存在
  • ISO/IEC 8859 系は1バイト拡張だが、最大256文字まで
  • UTF-16/UTF-32 はASCII互換性なし
    • 例:"A"はUTF-16で00 41(2バイト)、UTF-32で00 00 00 41(4バイト)

UTF-8 Playgroundの紹介

  • UTF-8エンコーディングの仕組みを インタラクティブに可視化 するツール「UTF-8 Playground」を自作
  • 実際に文字やバイト列を試してUTF-8の動作を理解可能
  • 詳細はHacker News等の議論も参考

まとめ

  • UTF-8 は、既存資産との互換性と多言語対応を両立した 卓越した設計
  • バイトパターンで柔軟かつ効率的に文字を表現
  • ASCII互換 という特長が、普及と長期的な運用を支えている

Hackerたちの意見

バックワードコンパチビリティには愛憎入り混じった感情があるな。ごちゃごちゃしたのは嫌いだけど、進化のために物事を壊す覚悟がある権力者には惚れる。でも、巧妙さも好きなんだよね。UTF-8やUTF-16、EANとかさ。まあ、UTF-8はバックワードコンパチビリティを保つためにほとんど犠牲にしてないけどね。

うーん、正直何を変えたらいいか分からないな。完全に無茶してUnicodeのバックワードコンパチビリティも壊すなら、制御文字をもうちょっと一般的な文字に置き換えて、ほんの少しだけスペースを節約するってのもアリかも。でも、一般的なマルチバイト文字エンコーディングフォーマットとしては、孤立してても完全に最適に見えるよ。

まあ、UTF-8はバックワードコンパチビリティを保つためにほとんど犠牲にしてないけどね。21ビット以上をエンコードする能力を犠牲にしてるんだけど、これはUTF-16との互換性のためだと思う。UTF-16のひどい「サロゲート」メカニズムは、2^21-1までのコードユニットしか表現できないからね。この制限をいつか後悔しないといいけど。他に大きなUTF-8コードユニットを禁止する理由は知らないな。

権力のある存在が進歩の名のもとに物事を壊すのが大好き。誰かがパラメータの名前を変えたせいで、動き続ける必要があるものが壊れるのはあまり楽しくない。

続きのバイトが常に 10 で始まるおかげで、ランダムなバイトにアクセスしても、文字の始まりか続きのバイトかが簡単に分かるんだよね。だから次の文字や前の文字の始まりをすぐに見つけられる。もし文字がEBMLの可変サイズ整数みたいにエンコードされてたら(1バイトのケースでASCII互換を保つために1と0を反転させるとして)、ランダムにシークしたときに、文字の始まりか xxxx xxxx のバイトのどれかに着地したのかを知るのは難しいかも。

結局、高コストのスイープを高コストのスイープに置き換えるだけだよね。それがnバイトジャンプに対して何の利点になるのか、全く理解できない。君が言ってるのは、スキャンするたびに何を探してるかを知るための最低限のことだよ。

可変長エンコーディングを使うとき、使った拡張バイトの数をユナリーエンコーディングで書くのは珍しくないよね。https://en.wikipedia.org/wiki/Unary_numeral_system それに、残ったビットを使って長さをエンコードする(例えば8ビットブロックで1111/1111 10xx/xxxxって書いて8バイトの拡張をエンコードする)っていう方法もある。このことはこのCSの古典書籍に載ってるよ。https://archive.org/details/managinggigabyte0000witt テキストとそのインデックスを圧縮して、ストップワードリストを使わずに済む方法も紹介されてる。君が言うように、UTF-8も似たようなことをするけど、ASCII互換で、データが壊れたり切り詰められたりしたときに速く同期できるんだ。

そうだね。これがUTF-8の素晴らしい特徴の一つだよ。UTF-8の文字列を前後に移動できるから、最初からやり直す必要がないんだ。Pythonはこの点で問題があったんだよね。Pythonの文字列は文字ごとにインデックスできるから、CPythonはワイドキャラクターを使ってた。CPythonを構築する時に2バイトか4バイトのキャラクターを選べた時期もあったけど、その後は実行時に自動的に切り替わるようになった。でも、結局ワイドキャラクターであって、UTF-8じゃないんだ。一つの絵文字で文字列のサイズが4倍になることもあるし、内部でUTF-8を使いたくなる気持ちもわかる。文字列へのインデックスは、整数のように小さな整数を足したり引いたりできる不透明なインデックスタイプになるんだ。それで文字列を移動できる。もしその不透明なタイプを実際の整数に変換したり、文字列を直接サブスクリプトしようとしたら、文字列へのインデックスが生成される。これは珍しいケースだね。正規表現を含むすべての標準操作は、不透明なインデックスオブジェクトを使ってUTF-8表現で動作できるよ。

それは、テキストが壊れていないか、悪意を持って改ざんされていないという前提だけどね。無効なUTF-8シーケンスのパースやエスケープによる脆弱性がたくさんあった(今もある)。ちょっとググってみると(全部がトピックに関係してるわけじゃないけど):https://www.rapid7.com/blog/post/2025/02/13/cve-2025-1094-po... https://www.cve.org/CVERecord/SearchResults?query=utf-8

次の文字や前の文字の始まりを簡単に見つけられる。これは本当じゃない [1]。これはUTF-8の問題というわけではなく、UTF-8の使い方の問題なんだ。 [1] https://paulbutler.org/2025/smuggling-arbitrary-data-through...

それに、冗長性があるから「これはUTF-8か?」っていう良いヒューリスティックが得られるんだ。ランダムデータや他のエンコーディングは、少なくとも小さくない文字列に関しては、正当なUTF-8である可能性はかなり低いよ。

現在のバイトが続きのバイトかどうかを確認するために、最大で3バイトだけ逆に読む必要があるだけじゃない?最大マルチバイトサイズが4バイトなら、その時点でマルチバイトの開始文字が見えなければ、単一バイトの文字だってわかるよね。理由は似てるのかな?UTF-8に対応していないライブラリで作業する際のエラー回復かもしれない。もしUTF-8バイトの配列を素朴にスライスすると、UTF-8に対応したライブラリは不正な先頭や末尾のバイトを無視して、そこから合理的な文字列を取り出すことができるんだ。

続きのバイトが常に 10 で始まるおかげで、ランダムなバイトにシークするのが簡単になるし、あなたが言ったように、文字の始まりか続きのバイトかを簡単に知ることができるから、次の文字や前の文字の始まりをすぐに見つけられるよね。最大4バイトだから、あなたが言った他のケースでも同じように簡単なアルゴリズムだと思う。私が見る主な違いは、UTF-8がストリーム内のエラーをキャッチしてフラグを立てる可能性を高めることだね。例えば、ストリームから欠けている非ASCIIバイトは、無効なシーケンスを引き起こす可能性が高い。一方、あなたが言った他のケースでは、続きのバイトがサイレントエラーを引き起こすことになる(ASCII文字が続きのバイトと区別できなくなるから)。エンコーディングの専門家、合ってる?

いつも疑問に思うことがあるんだけど、Unicodeのコードポイントをバイト数が多すぎる形でエンコードすることは可能なんだよね。UTF-8ではそれを禁止していて、最短のものだけが有効。例えば、00000001は11000000 10000001と同じ。じゃあ、最後の有効な選択肢の始まりを追加して、代替案を不可能にすればいいんじゃない?つまり、11000000 10000001は128+1のコードポイントを与えることになる。値0から127は1バイトのシーケンスでカバーされてるから。メリットは明らかで、違法なコードがなくなって、エッジケースのために少し短い文字列になる。デザイナーたちはこれを考えたと思うけど、何がデメリットだったんだろう?当時のハードウェアコストが許容できなかったとか?追記:最後のビットシーケンスはもちろん10000001で、00000001じゃない。ごめん、修正した。

quectophotonのコメントを見てみて—続きのバイトが常に先頭に10を付ける必要があるのは、パーサーがランダムなオフセットでジャンプする時に便利だよね。もっと一般的には、テキストストリームが断片化する場合にも。これが実際にUTF-8が90年代初頭に考案されたときの大きな懸念だったんだ。あの頃は通信が今ほど信頼性がなかったから。

そうすると、バイトを見ただけでそれが文字の始まりかどうかを判断するのが不可能になっちゃうから、それがUTF-8の便利な特性なんだよね。

それってランダムアクセスを混乱させるんじゃない?

https://en.m.wikipedia.org/wiki/Self-synchronizing_code

君が言ってるのは「11000000 10000001」で、すべての継続バイトが「10」で始まる特性を保つってことだよね?[編集: それを追記したみたいだね] その特性がないと、UTF-8は自己同期性を失う。つまり、切り詰められたUTF-8ストリームがあっても、コードポイントの境界を常に見つけられるし、全体が混乱することなく、せいぜいコードポイント分だけ失うことになる。理論的にはその方法も可能だけど、デコーダのパフォーマンスに影響が出るよ。UTF-8では、ストリームからコードポイントを再構成するのに、速いビット演算(&, |, <<)だけで済むんだ。もし短いシーケンスで表現された合法的なコードポイントを引き算しなきゃいけないって宣言したら、エンコーディングとデコーディングで追加の算術演算を導入しなきゃいけなくなるよ。

兄弟たちは今までインジケーターの同期的な性質について話してるけど、それは君の質問には関係ないよ。君の質問は「U+0080がc2 80としてエンコードされるのはなぜか、c0 80ではなく、7fの次に来る最小シーケンスなのに?」ってことだと思う。答えはa) オーバーロングエンコーディングのセキュリティ影響が考慮されていなかったからだと思う。オーバーロングエンコーディングを受け入れるけど、最短エンコーディングだけをスキャンしているものがあれば、面白いことになるよね。b) 標準化されたUTF-8はビットマスクとビットシフトだけでエンコードとデコードができるから。君が提案したエンコーディングは、加算と減算に加えてビットマスクとビットシフトが必要なんだ。1992年のメールの議論がここにあるよ [1] ... 一番下にはUTF-8になった経緯についてのメモがある:> 1. 2バイトシーケンスには2^11のコードがあるけど、実際には2^11-2^7しか許可されていない。0-7fの範囲のコードは不正なんだ。これは、実際の利益がないのに魔法の加算定数の山よりも好ましいと思う。長いシーケンスにも同じことが言える。メモの前に含まれているFSS-UTFには加算定数が含まれてる。 [1] https://www.cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt

リンクされてるUTF-8プレイグラウンド、めっちゃいいね!: https://utf8-playground.netlify.app/ コードポイントを直接入力できたら最高なんだけど、URL経由(例:/F8FF)ではできるけど、UIではできないんだよね。(編集:未来は今だ。https://github.com/vishnuharidas/utf8-playground/pull/6)

ありがとう、これで統合して公開したよ。

UTF-8のデザインについてもっと知りたいなら、ラッス・コックスのワンページを見てみて: https://research.swtch.com/utf8 それと、ロブ・パイクの設計の歴史についての説明も: https://www.cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt

UTF-8は本当に天才的なデザインだよね。でも、もちろんASCIIが7ビットだけを使うって決めたことに大きく依存してる。1963年でもそれはちょっと変な選択だったよね。これはただの歴史的な運?ASCIIのデザイナーたちがもう1ビットのコードスペースを確保して、いろいろな便利なものを考えていた世界もあったのかな?それとも最初からコードページや他の拡張性を考えていたのかな?ここにいる誰かが知ってるかもね。

アイデアとしては、余ったビットを再利用するつもりだったんだろうね。多分、パリティ用に。

これが理由かどうかは分からないし、因果関係が逆かもしれないけど、8ビットの汎用ビットが常にあったわけじゃないってことは覚えておく価値があるよ。7ビット + 1パリティビットやフラグビット、あるいは他の何かがすごく一般的だった(今でもメールは7ビットバイトをエンコードするためにquoted-printable [1]を使ってるくらい)。通信チャネルがバイト内の全8ビットをそのまま送信できることを「8ビットクリーン [2]」って呼ぶけど、これは常に保証されてたわけじゃない。ある意味、UTF-8はASCIIバイトの余った8ビットを使った良い例の一つなんだよね... [1] https://en.wikipedia.org/wiki/Quoted-printable [2] https://en.wikipedia.org/wiki/8-bit_clean

https://www.sensitiveresearch.com/Archive/CharCodeHist/X3.4-... これは偶然の産物に見えるね。彼らは8ビットは無駄だと思ってたし、そんなに多くの文字が必要だとは思ってなかったんだ。

歴史的な「運」って感じかな。ただ「運」って言うのはちょっと言い過ぎかも。数学の証明が過去の作業に基づいて「運が良い」って言われるのと似てる。実際にはほとんど自然な結果なんだよね。ASCIIの前にはBCDICがあって、これは6ビットで標準化されてなかった(バリエーションがあったし、技術的にはASCIIにもいくつかのバリエーションがあるけど、今は一般的にASCIIって呼ばれてる)。BCDICは大文字の英字と一般的な句読点、数字を含んでた。2^6は64で、大文字と数字を合わせると36、そこにいくつかの一般的な句読点を加えると50くらいになる。確かIBMの元々のは45くらいだったと思う。スラッシュ、ピリオド、カンマとかね。だから小文字をサポートする決定がされたとき、必要な分だけビットを追加したんだ。印刷機も128文字以上は印刷できなかったし、óやöみたいな文字も印刷できなかったから、サポートする理由もなかったんだよね。でも最終的には8ビットのエンコーディング(ラテン1拡張とか、ñみたいな文字を含む)に移行した。重要なのは、UTF-8は7ビットのASCIIとしか互換性がないってこと。8ビットのASCIIは、8ビット目を使ってるからUTF-8とは互換性がないんだよ。

よくわからないけど、これは歴史的な先見の明があるように思える。何かを標準化する人にとっての教訓だね。「32ビットの整数を使いたい?それなら31ビットにしとけ」みたいな。もちろん、これはいつも当てはまるわけじゃないけど(サイズとかね)、将来の拡張性のために少しでもスペースを残しておくってのは重要だよ。

専門家じゃないけど、ちょっと前にこの歴史について読んだことがある。ASCIIはテレタイプコードにルーツがあって、これはモールス信号のような電信コードから発展したものなんだ。モールス信号は可変長だから、自動電信機やテレタイプを実装するのが面倒だったんだよね。解決策は5ビットのボードコードだった。固定長のコードを使うことで、デバイスが簡単になったんだ。オペレーターは片手で5キーのキーボードを使ってボードコードを入力できたし、コードの設計の一部はオペレーターの疲労を最小限に抑えることだった。ボードコードがあるから、モデムやその類のシンボルレートをボードで表現するんだよ。で、次の変化は、電信機が直接ワイヤで信号を送るのではなく、タイプライターを使ってコードポイントのパンチテープを作成し、それを電信機に読み込ませて送信するようになった。キーボードがワイヤコードから切り離されたことで、追加のコードポイントを追加する柔軟性が生まれた。これが「キャリッジリターン」や「ラインフィード」の起源なんだ。これがウェスタンユニオンによって標準化され、国際的にも広まった。ASCIIにたどり着く頃には、テレプリンターが一般的になっていて、初期のコンピュータ業界はパンチカードを広く入力フォーマットとして採用してた。最初は電信コードをそのまま使うというシンプルな方法を取ったんだけど、IBMの誰かがパンチカードを使ったソートマシンでより速くなる新しいスキームを考えついたんだ。それが最終的にASCIIになった。要するに、バイナリコードから始まり、技術が進化するにつれて新しいスキームを採用していったってこと。デジタルコンピューティングの世界が8ビットバイトを標準として定着するずっと前の話だよ。ASCIIはバイトとしては、古いテレタイプコードと新しい規則の間の実用的な妥協なんだ。

7ビットってそんなに変じゃないよ。ボードコードは5ビットで、不十分だとされて6ビットコードが開発されたけど、それも不十分だったから7ビットのASCIIが開発されたんだ。IBMはSystem/360で8ビットバイトを標準化して、8ビットのEBCDICエンコーディングを開発した。他のコンピュータベンダーはバイト長が一貫してなかったし… 7ビットは変だったけど、文字は必ずしもシステムワードにうまく収まるわけじゃなかったんだよね。

ASCIIの8ビット拡張(ISO 8859-xファミリーみたいな)は数十年にわたって広まってたし、今でもある程度はWindowsで使われてる(標準のWindowsコードページ)。もしASCIIが最初から8ビットだったとしても、最も一般的な文字が最初の128整数の中に収まっていたら、UTF-8はうまく機能してたと思う。歴史の偶然は、ASCIIが7ビットであることよりも、コンピュータ開発の関連する段階が主に英語圏で起こったこと、そして英語のテキストが7ビット単位でうまく表現できることなんだ。

UTF-8は期待できるデザインだけど、Unicodeにはスコープクリープの問題がある。Unicodeには何が含まれるべきなのか?素朴に考えると、人間がコミュニケーションのために使う、印刷可能な十分に広まった異なるグリフがすべて含まれるべきだと思うかもしれない。でもそれは違う。なぜなら、* それは離散的ではない。一部のコードポイントは他のコードポイントと組み合わせるためのものだから。 * それは明確ではない。一部のグリフは複数の方法で書かれることがあるし、ほとんど同じように表示されるグリフでも、異なるコードポイントと意味を持つことがあるから。 * すべてが印刷可能ではない。制御文字が含まれていて、ASCIIとの互換性のためにそれらはほぼ必須だったけど、自分たちのものもたくさん追加されてる。アニメーションされたUnicodeコードポイントは知らないけど、少なくとも印刷可能なものは紙に印刷できるもので、画面だけではなく、マルキーや点滅制御文字はない、神様に感謝だ。でも、その不変性もいつか崩れるかもしれないね。ちなみに、著者が言及しなかったutf-7というutfエンコーディングも知ってる。utf-8に似てるけど、最後のビットがネットワーク上で使うのが安全じゃないと仮定してる(80年代のネットワーク上での賢明な予防策らしい)。一度、上司がutf-7でエンコードされたメールを送ってきたことがあって、それで何かを知ったんだ。どうやって送ったのかはわからないけど。

ちょっと話が逸れるけど、UTF-8やそのASCII互換性についての議論が多い中で、私がASCIIに対して一つ不満があることを言いたい。誰も話してないし、私も今まで言ったことがないことなんだけど、クソったれな0x7fキャラクター。あらゆる意味で本当にイライラする異常だよ。もっと他のちゃんとした印刷可能な句読点や、それに近いキャラクターだったら良かったのに。著作権マークとか、円周率の記号とか、今のままのもの以外の何かが良かった。私はプログラミングやパケットダンプの勉強を長いことやってきたから、基本的に16進数をASCIIに、またその逆も頭の中で変換できるけど、この異常なキャラクター(DELETE?って呼ぶべきなのかな?)には毎回ビクッとする。

あらゆる面でずっと良くなったけど、一番大事なところだけはダメだった:紙テープのパンチミスをやり直さずに修正できること。あなたが、ネイティブにその機能がないタイプライターでタイピングミスを修正するためにホワイトアウトを使ったことがあるかどうかわからないけど、ホワイトアウトが出る前は、手紙を最初から再び打ち直すしか選択肢がなかった。0x7fは、パンチされた紙テープのためのホワイトアウトだったんだ。

そうだね、Unixの父であるケン・トンプソンが関わってるよ。

UTF-8が理解できるようになるまで時間がかかったね。世紀の変わり目の頃は、すべてが大きすぎて苦労した。本当に大変だったけど、今は容量や計算能力がずっと大きくなったから、今はもっと理解しやすい。でも当時は本当に面倒だった。