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

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

2025年9月13日原文(iamvishnu.com)

概要

  • 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に対応したライブラリは不正な先頭や末尾のバイトを無視して、そこから合理的な文字列を取り出すことができるんだ。

Hacker Newsで議論の続きを見る