文字コードとUnicode

目次

概要

文字コードは、「文字」をコンピュータで保存・通信するための約束です。文字列の見た目、バイト列、比較、検索、正規化、ファイル名、HTTP headerなど、多くの場所に関わります。

要点

文字はそのまま保存されるのではなく、code pointencodingを通じてバイト列になります。UTF-8Unicode正規化、改行コードを理解すると、文字化けや比較の失敗を調べやすくなります。

文字とバイトは違う

同じ文字列でも、encodingによってバイト列は変わります。

文字列 -> code point -> encoding -> bytes
flowchart LR C["文字"] --> CP["code point"] CP --> E["encoding"] E --> B["bytes"]

「文字数」と「バイト数」は一致しません。

text = "日本語"
print(len(text))
print(len(text.encode("utf-8")))

この例では、Pythonの len(text)code point数に近い値を返します。一方、len(text.encode("utf-8"))UTF-8にしたときのbyte数です。見た目の文字数、code point数、byte数は別物です。

flowchart TD A["見た目の文字数"] --> D["ユーザーが数える単位"] B["code point数"] --> E["Unicode上の番号の数"] C["byte数"] --> F["保存・通信のサイズ"]

実務では、次のように目的で単位を変えます。

目的 見る単位
DBの容量制限 byte数
UIの文字数制限 grapheme cluster
protocolの長さ制限 byte数
parser内部 code point / code unit
表示幅 East Asian Widthやフォント依存

Unicodeとcode point

Unicodeは、世界中の文字に番号を割り当てる標準です。この番号をcode pointと呼びます。

A    U+0041
あU+3042
😀   U+1F600

code pointは文字の番号であり、実際にファイルへ保存するバイト列ではありません。保存や通信にはencodingが必要です。

Unicodeを理解するときは、次の3つを分けます。

用語 意味
character 抽象的な文字 A, , 😀
code point Unicode上の番号 U+0041, U+3042
encoding code pointをbyte列にする方式 UTF-8, UTF-16

「文字コード」という言葉は曖昧に使われがちです。会話では、Unicodeの文字集合を指しているのか、UTF-8のようなencodingを指しているのかを確認すると混乱が減ります。

code unitとgrapheme cluster

文字列APIでは、code pointではなくcode unitを数える言語があります。JavaScriptの文字列はUTF-16 code unitの列として扱われるため、絵文字のような文字で直感とずれることがあります。

"A".length      // 1
"あ".length     // 1
"😀".length     // 2

😀 はcode pointとしてはU+1F600ですが、UTF-16ではsurrogate pairになり、code unitが2つになります。

さらに、ユーザーが「1文字」と感じる単位はgrapheme clusterです。

文字列 見た目 内部
é 1文字 U+00E9の場合がある
1文字 e + combining acute accentの場合がある
👨‍👩‍👧‍👦 1絵文字 複数emoji + ZWJ

文字列を途中で切るときは、code unitやbyteの途中で切らないだけでは足りません。grapheme clusterを壊すと、濁点だけ残る、絵文字が分解される、表示が崩れる、といった問題が起きます。

flowchart LR A["bytes"] --> B["code units"] B --> C["code points"] C --> D["grapheme clusters"] D --> E["ユーザーが見る文字"]

JavaScriptでは Intl.Segmenter を使うと、grapheme cluster単位に近い分割ができます。

const segmenter = new Intl.Segmenter("ja", { granularity: "grapheme" });
const chars = [...segmenter.segment("👨‍👩‍👧‍👦abc")].map(s => s.segment);

UTF-8

UTF-8は、Unicodeのcode pointをバイト列へ変換するencodingです。ASCII文字は1 byte、日本語や絵文字は複数byteになります。

文字 code point UTF-8 byte数
A U+0041 1
U+3042 3
😀 U+1F600 4

UTF-8はWebや現代のソフトウェアで広く使われます。迷ったらUTF-8を基本にします。

UTF-8の重要な性質です。

性質 意味
ASCII互換 U+0000からU+007FはASCIIと同じ1byte
可変長 code pointにより1から4byte
byte列として保存 file、network、DBではbyteになる
現代Webの基本 HTML、JSON、APIで広く使われる

UTF-8のbyte数はだいたい次のように見ます。

範囲 byte数
U+0000…U+007F 1
U+0080…U+07FF 2
U+0800…U+FFFF 3
U+10000…U+10FFFF 4

文字化けの多くは、UTF-8のbyte列をShift_JISやWindows-1252として読んだり、その逆をしたりすることで起きます。保存時のencodingと読み込み時のencodingを一致させるのが基本です。

BOMとUTF-16

BOM(Byte Order Mark)は、テキスト先頭に置かれることがある印です。UTF-8では必須ではありませんが、Windows系ツールではUTF-8 BOM付きファイルが出ることがあります。

表現 先頭byte
UTF-8 BOM EF BB BF
UTF-16LE BOM FF FE
UTF-16BE BOM FE FF

BOMがあると、parserが先頭の不可視文字を含む文字列として扱ってしまうことがあります。

name,age
Alice,20

見た目では分かりませんが、最初の列名が name ではなく \ufeffname になることがあります。

調べる例です。

xxd sample.csv | head
python - <<'PY'
from pathlib import Path
print(Path("sample.csv").read_bytes()[:4])
PY

UTF-16は、JavaScriptやWindows APIなどで内部表現として関わることがあります。ファイル交換やWeb APIでは、特別な理由がなければUTF-8に寄せる方が扱いやすいです。

正規化

Unicodeでは、見た目が同じでも内部表現が違う文字列があります。

é
e + combining acute accent

比較や検索では、正規化が必要になることがあります。

形式 意味
NFC 合成済みに寄せる
NFD 分解済みに寄せる
NFKC 互換文字も含めて寄せる
NFKD 互換分解

Pythonでは unicodedata.normalize を使います。

import unicodedata

text = unicodedata.normalize("NFC", text)

JavaScriptでは String.prototype.normalize() を使います。

const a = "\u00e9";
const b = "e\u0301";

console.log(a === b); // false
console.log(a.normalize("NFC") === b.normalize("NFC")); // true

NFCとNFKCの違いは重要です。NFCはcanonical equivalenceをそろえる用途に向きます。NFKCは互換文字も寄せるため、見た目や意味が近い文字をより強くまとめます。

目的 候補
保存前に表現揺れを減らす NFC
検索用の別カラムを作る NFKC + case folding
password 安易に正規化しない。仕様を明確にする
識別子 UAX #31/UTS #39の考え方を参考に制限する
表示 元の入力を保持することもある

NFKCは便利ですが、元の文字の区別を失うことがあります。ユーザー表示用の原文と、検索・照合用の正規化済み文字列を分けて保存する設計が安全なことがあります。

flowchart LR A["user input"] --> B["raw value<br>表示・監査用"] A --> C["normalized value<br>検索・重複判定用"]

改行コード

改行にも種類があります。

表記 主な環境
LF \n Unix, Linux, macOS
CRLF \r\n Windows
CR \r 古いMac

Gitでは改行コードの差分が大量に出ることがあります。.gitattributes で方針を決めると安定します。

* text=auto
*.sh text eol=lf
*.bat text eol=crlf

改行コードは、文字コードとは別の問題ですが、テキスト処理では一緒にハマりがちです。

症状 原因
shell scriptが動かない CRLFの \r が混ざっている
diffが大量に出る LF/CRLFの変換
CSVの行数が合わない 改行を含むquoted field
Windowsで表示が崩れる LFだけを想定しない古いtool

確認例です。

file sample.sh
python - <<'PY'
from pathlib import Path
print(Path("sample.sh").read_bytes()[:80])
PY

絵文字とZWJ

絵文字は、1つの見た目が複数code pointからできていることがあります。ZWJ(Zero Width Joiner)は、複数の絵文字を結合して1つの見た目にするために使われます。

👨‍👩‍👧‍👦

このような文字列では、見た目の文字数、code point数、byte数が一致しません。カーソル移動、文字数制限、切り詰め処理では注意が必要です。

絵文字には、skin tone、variation selector、regional indicator、tag sequenceなども関わります。

仕組み 意味
ZWJ family emoji 複数絵文字を結合
variation selector text/emoji presentation 表示スタイル指定
skin tone modifier 👍🏽 肌色修飾
regional indicators 🇯🇵 国旗

「最大10文字」のようなUI制限で、絵文字を途中で切ると壊れます。SNS、チャット、プロフィール名、通知タイトルではgrapheme cluster単位で切る必要があります。

const text = "👍🏽👍🏽👍🏽";
console.log(text.length); // 見た目の数とは一致しない

文字化けの調べ方

文字化けは、encodingの解釈がずれたときに起きます。

調べる順序です。

  1. 元のencodingは何か
  2. 読み込み時のencodingは何か
  3. 保存時に変換していないか
  4. HTTP headerやHTML metaは正しいか
  5. terminalのlocaleは何か
file -I sample.txt
locale

Webでは、Content-Type にcharsetを含めることがあります。

Content-Type: text/html; charset=utf-8

もう少し実践的には、byte列を見ます。

xxd sample.txt | head
iconv -f shift_jis -t utf-8 sample.txt > sample.utf8.txt

調査表です。

症状 可能性
が出る 不正byte列をreplacement characterで置換
あ のように見える UTF-8をLatin-1/Windows-1252として読んだ可能性
縺ゅ のように見える UTF-8とShift_JIS/CP932の取り違え
CSVの先頭列名だけ変 BOMが列名に入った可能性
terminalだけ崩れる locale、font、terminal設定

文字化け調査では、変換を繰り返す前に元ファイルを保存します。誤った変換を上書きすると、元のbyte列が失われて復元が難しくなります。

flowchart TD A["元ファイルをコピー"] --> B["byte列を見る"] B --> C["encoding候補を立てる"] C --> D["iconv等で変換を試す"] D --> E["読めるか確認"] E --> F["変換手順を記録"]

検索・比較・ソート

文字列処理では、見た目が同じか、バイト列が同じか、意味として同じかを分けます。

flowchart TD A["文字列比較"] --> B{"目的は何か"} B -->|完全一致| C["bytes / code pointを比較"] B -->|ユーザー入力検索| D["正規化 + case folding"] B -->|表示順| E["locale aware collation"]

たとえば、検索では次のような前処理が必要になることがあります。

  • Unicode正規化
  • 大文字小文字の統一
  • 全角半角の扱い
  • 濁点・結合文字の扱い
  • localeに応じたソート

Pythonでは単純な比較は簡単ですが、言語ごとの自然な並び順には追加の処理が必要です。

「一致」と「並び順」は別です。

目的 必要な処理
byte完全一致 cache key encoding後のbytes比較
正規化後一致 重複ユーザー名 normalization
大文字小文字無視 email local policyなど case folding
検索 表記揺れを吸収 normalization + tokenizer
ソート 辞書順、言語別順 collation

Unicode Collation Algorithmは、文字列を言語や用途に応じて比較・整列するための考え方を提供します。ただし、実際の並び順はlocaleによって変わります。

const words = ["ä", "z", "a"];
console.log(words.sort((a, b) => a.localeCompare(b, "de")));
console.log(words.sort((a, b) => a.localeCompare(b, "sv")));

ドイツ語とスウェーデン語では、ä の扱いが違います。ユーザーに見せるソートでは、単純なUnicode code point順ではなくlocale awareな比較を使うのが自然です。

識別子とセキュリティ

Unicodeには見た目が似ている文字があります。これは国際化には必要ですが、識別子やURLではspoofingの入口になります。

paypal.com
раураl.com

後者は一部にキリル文字が混ざっていても、見た目では気づきにくい場合があります。

セキュリティ上の観点です。

問題 対策
confusable Latin a とCyrillic а mixed script detection
mixed script 複数scriptの混在 許可scriptを制限
invisible文字 zero width系 識別子では制限
正規化 同じ見た目で別表現 保存前にnormalize
全角半角 A 検索用にはNFKC

ユーザー名、organization名、domain風の識別子、API key名、repository名では、自由なUnicodeを許すか、制限するかを仕様として決めます。一般文章の入力欄と、識別子の入力欄は同じ扱いにしない方が安全です。

Web・URL・HTTPでの文字

Webでは、HTML、URL、HTTP header、JSON bodyで文字の扱いが変わります。

場所 観点
HTML <meta charset="utf-8">HTTP header
JSON UTF-8が基本。escape表記もあり得る
URL path/query percent-encoding
domain IDNA / punycode
header headerごとに仕様が違う
form browserのencodingとserverのdecode

URLでは、見た目の文字列がそのままbyte列として流れるわけではありません。

検索?q=日本語
%E6%A4%9C%E7%B4%A2?q=%E6%97%A5%E6%9C%AC%E8%AA%9E

domain名に非ASCII文字を含める場合は、IDNA/punycodeが関わります。

例え.テスト
xn--r8jz45g.xn--zckzah

Webアプリでは、入力、保存、表示、URL生成、ログ出力のどこでencode/decodeするかを明確にします。二重decodeや二重escapeは、文字化けだけでなくsecurity bugにもつながります。

DBとファイル名

DBでは、encodingとcollationの両方が重要です。

項目 意味
encoding / character set 保存できる文字とbyte表現
collation 比較・ソート・大文字小文字などの規則
index length byte長制限に影響
normalization DBが自動で正規化するとは限らない

同じ文字列でも、DBのcollationにより一致判定や並び順が変わります。ユーザー名の一意制約、検索、sort orderでは、アプリ側の正規化方針とDBのcollationを合わせます。

ファイル名も注意が必要です。OSやファイルシステムにより、Unicode正規化や大文字小文字の扱いが異なる場合があります。macOS、Linux、Windowsをまたぐプロジェクトでは、見た目が同じファイル名が別物として扱われたり、逆に衝突したりすることがあります。

python - <<'PY'
import unicodedata
names = ["é.txt", "e\u0301.txt"]
for name in names:
    print(name, [hex(ord(c)) for c in name], unicodedata.normalize("NFC", name))
PY

repositoryでは、ファイル名をASCII寄りにする、正規化方針を決める、caseだけ違うファイル名を避ける、といった運用が安全です。

言語別の注意点

言語やruntimeごとに文字列の内部表現とAPIの癖があります。

言語 注意点
JavaScript length はUTF-16 code unit数。Intl.Segmenternormalize を使う
Python str はUnicode文字列。file I/Oでは encoding を明示する
Go string はbyte列。range はrune(code point)単位
Rust StringUTF-8。byte indexで文字境界を壊さない
Java String はUTF-16。code point APIが必要な場面がある
SQL collationとcharsetを明示的に見る

Goの例です。

s := "日本語"
fmt.Println(len(s)) // bytes
for _, r := range s {
    fmt.Printf("%U\n", r)
}

Rustの例です。

let s = "日本語";
println!("{}", s.len());        // bytes
println!("{}", s.chars().count()); // scalar values

どの言語でも「標準の length が何を数えるのか」を確認するのが第一歩です。

実務チェックリスト

文字列まわりで確認することです。

場面 確認
ファイル読み書き encodingを明示しているか
HTTP charset=utf-8 が必要か
DB保存 collationとencodingは何か
検索 正規化して比較するか
入力制限 文字数かbyte数か
切り詰め grapheme clusterを壊さないか
Git 改行コード方針はあるか
URL percent-encodingとdecode回数は適切か
DB制約 collationと一意制約は仕様通りか
ファイル名 OS差分、正規化差、case差を考慮しているか
セキュリティ confusable、mixed script、不可視文字を制限するか

特に「最大20文字」のような仕様では、何を1文字と数えるのかを決める必要があります。絵文字や結合文字を含むと、見た目の文字数とcode point数がずれるためです。

設計時に決めることです。

決めること
保存encoding UTF-8
入力正規化 保存前NFC、検索用NFKC
表示 原文を保持するか
文字数制限 grapheme cluster単位か、byte単位か
URL生成 encode/decodeの責務
識別子 許可script、長さ、正規化
ログ 不可視文字や制御文字の扱い

zation Forms](https://unicode.org/reports/tr15/)

Unicode の字形変化と表示の問題

同じ文字でも、環境やフォントによって異なる見た目を持つことがあります。

字形の変化(Variants)

同じ文字コード、異なる表現
- 数字の "0"(ゼロ)vs "O"(大文字オー)
- ラテン小文字 "l" vs 数字 "1"
- CJK の字形バリエーション(中日韓の異なる字形)

例: U+2F00 - CJK Compatibility Ideograph(互換用文字)

RFC 3454 (STRINGPREP) では、ある文字をその正規形に変換してから処理することで、視覚的な混同を防ぐ方法を推奨しています。

組み合わせ文字による視覚的攻撃

# 例: ドメイン名の偽装
"раypal.com"  # Cyrillic の "а" を Latin の "a" の代わりに使用
# 表示上は同じ見える可能性がある

import unicodedata

def detect_mixed_scripts(text):
    scripts = set()
    for char in text:
        name = unicodedata.name(char, 'UNKNOWN')
        # Script を抽出("LATIN SMALL LETTER A" から "LATIN")
        script = name.split()[0]
        scripts.add(script)
    return scripts

# 複数のスクリプトが混在していないか検査
if len(detect_mixed_scripts(domain)) > 1:
    raise ValueError("Mixed scripts detected")

W3C と WHATWG の仕様でも、ドメイン名の正規化と検査が定められています。

Unicode バージョンの差異

Unicode は定期的に新しいバージョンがリリースされ、新しい文字が追加されます。

バージョン履歴

バージョン リリース 新文字数 主な追加
1.0 1991 7,129 基本的な Latin, Greek, Cyrillic, CJK
3.0 2000 +10,176 Arabic, Thai, Lao, Khmer
6.0 2010 +2,088 絵文字の初期化
8.0 2015 +7,716 絵文字大幅拡充
14.0 2021 +838 モダン言語サポート
15.0 2022 +4,489 絵文字など

アプリケーションが対応する Unicode バージョンに不一致があると、文字が表示されないか、異なる見た目で表示される問題が発生します。

import sys
print(f"Python supports Unicode {sys.version}")  # 使用可能な範囲

# ファイルの文字を確認
with open('file.txt', 'r', encoding='utf-8') as f:
    text = f.read()
    for char in text:
        code_point = ord(char)
        print(f"{char} -> U+{code_point:04X}")

正規化形式の詳細選択基準

4つの正規化形式を使い分ける基準です。

どの正規化形式を選ぶか

NFC(合成形): 多くのコンテキストで推奨
  - ファイルシステム(HFS+ は強制)
  - Web(HTML5 は推奨)
  - JSONペイロード

NFD(分解形): ほとんど避けるべき
  - ただし、文字ごとの分析が必要な場合のみ

NFKC(互換合成形): テキスト検索・マッチング
  - ユーザー入力の正規化
  - セキュリティチェック

NFKD(互換分解形): 分析・レポート用
  - テキスト統計
  - 文字分析

Python での実装例

import unicodedata

text = "Café"  # U+00E9(Cyrillic)

# NFC: 現代的、Web推奨
nfc = unicodedata.normalize('NFC', text)

# NFD: 分解形
nfd = unicodedata.normalize('NFD', text)

# NFKC: 互換性統一
nfkc = unicodedata.normalize('NFKC', text)

# バリデーション: 入力を NFKC 正規化してから検査
def validate_email(email):
    normalized = unicodedata.normalize('NFKC', email)
    # パターンマッチ
    return re.match(r'^[a-z0-9+\-_.]+@[a-z0-9+\-_.]+{{CONTENT}}#x27;, normalized, re.I)

開発言語の Unicode サポート状況

各言語の Unicode 対応度は異なります。

言語別対応状況

言語 バージョン Unicode 正規化 注記
Python 3.x 完全(UCS4) unicodedata.normalize 文字列は Unicode
JavaScript ES6+ 制限(UTF-16) Intl.Collator サロゲートペア対応
Java 8+ 制限(UTF-16) java.text.Normalizer String.length は不正確
C# / .NET 4.x+ 限定(UTF-16) StringInfo 結合文字対応
Rust 1.0+ 完全 unicode-normalization crate 最適化されている
Go 1.x 完全(UTF-8) unicode/norm UTF-8 native

文字列長の計算例

# Python: 正しく計算
text = "Café"
len(text)  # 4

# JavaScript: 注意が必要(絵文字)
const text = "👨‍👩‍👧‍👦";  // Family emoji(複数の基本文字 + Zero-Width Joiner)
text.length  # 25(サロゲートペアと結合順序)
[...text].length  # 4(スプレッド演算子で正確に)

Git での文字コード処理

Git は UTF-8 を標準としていますが、古い設定では問題が起きます。

Git 設定チェックリスト

# グローバル設定
git config --global core.safecrlf warn
git config --global core.precomposedUnicode true  # macOS 向け

# リポジトリ設定
git config core.quotePath false  # 日本語ファイル名をそのまま表示

.gitattributes での統一

# すべてのテキストファイル
* text eol=lf

# ファイル形式の自動検出を無効化
*.json text
*.csv text
*.txt text

# バイナリ
*.jpg binary
*.png binary

文字エンコード の自動検出と問題

ファイルの文字エンコードを自動検出するのは困難です。

自動検出ツール

import chardet

with open('file.txt', 'rb') as f:
    raw_data = f.read()
    result = chardet.detect(raw_data)
    print(result)  # {'encoding': 'utf-8', 'confidence': 0.99}

chardet の精度は、サンプルサイズに依存し、短いテキストでは失敗することもあります。

ベストプラクティス

  1. 明示的な指定を常とする

    open('file.txt', encoding='utf-8')  # デフォルトを上書き
    
  2. BOM をチェック

    if raw_bytes.startswith(b''):
        encoding = 'utf-8-sig'
    
  3. フォールバック

    for encoding in ['utf-8', 'utf-8-sig', 'cp1252', 'iso-8859-1']:
        try:
            return text.decode(encoding)
        except UnicodeDecodeError:
            continue
    

HTTP での文字エンコード指定

HTTP Content-Type ヘッダで指定されるエンコードです。

Content-Type の例

Content-Type: text/html; charset=UTF-8
Content-Type: application/json; charset=UTF-8
Content-Type: text/plain; charset=ISO-8859-1

HTML5 では、<meta charset="UTF-8"> で明示します。

Python での送受信

import requests

# リクエスト送信時
response = requests.get('http://example.com')
response.encoding  # Content-Type から自動検出
response.text      # 正しくデコード

# レスポンス返却時(Flask)
from flask import Response
return Response("テキスト", mimetype='text/html; charset=utf-8')

Unicode と正規表現の相互作用

正規表現エンジンが Unicode を理解するかどうかで、パターンの動作が変わります。

パターン例

# ASCII-only
[a-z]+

# Unicode 対応(Python の \w)
\w+

# 特定スクリプト
\p{Latin}+    # PCRE
[\u0041-\u005A]+  # 手動での Latin 大文字指定

Python での Unicode フラグ

import re

text = "Café"

# ASCII のみ(デフォルト)
re.findall(r'\w+', text)  # ['Caf']

# Unicode 対応
re.findall(r'\w+', text, re.UNICODE)  # ['Café']

RFC 仕様(WHATWG Encoding Standard など)では、エンコード識別の規範的プロセスが定義されています。

WHATWG Encoding Standardの詳細仕様

WHATWG Encoding Standardは、Webブラウザとサーバーの文字エンコード処理を統一する仕様です。以下は実装レベルの詳細です。

エンコーダ・デコーダの構造

エンコーディング処理は、I/O queueという抽象化された入出力バッファを用いて定義されます。

  • 即座モード(Immediate mode): メモリ内にすべてのデータを保持。end-of-queue が最後のアイテム
  • ストリーミングモード(Streaming mode): ネットワークから順次データが流入。event loop内では使用不可
# I/O queue の概念実装イメージ
class IOQueue:
    def __init__(self):
        self.items = []
        self.end_of_queue = False
    
    def read_item(self):
        if not self.items:
            # ストリーミング: ここでブロック
            return None
        return self.items.pop(0)
    
    def read_number(self, n):
        # n個のアイテムを読む
        result = []
        for _ in range(n):
            item = self.read_item()
            if item is not None:
                result.append(item)
        return result

セキュリティ課題と対策

WHATWG仕様では複数バイト文字の脆弱性に対応しています。

例: Shift_JISを使った2011年の攻撃

攻撃者は0x82(先行バイト) + 0x22(末尾バイト)の非正規組み合わせを利用して、JSON処理を操作しました。

  • 生産側(サーバー): 0x22(ダブルクォート)として解釈
  • 消費側(ブラウザ): U+FFFD(置換文字)として解釈
  • 結果: JSON文法が破壊され、セキュリティが侵害される可能性

WHATWG対策: 複数バイトエンコーディングでは、非正規なバイト組み合わせ時に以下を適用

  1. U+0000〜U+007F の ASCII文字は「マスク」されない
  2. 非正規な組み合わせは複数の置換文字に分離出力(例: U+FFFD U+0022)
# エンコーディング検証の疑似コード
def validate_sjis_sequence(byte1, byte2):
    if is_invalid_combination(byte1, byte2):
        # 非正規組み合わせ -> 分離出力
        return [U_REPLACEMENT, byte_to_codepoint(byte2)]
    # 正規な組み合わせの処理...
    return [decode_properly(byte1, byte2)]

ASCII互換性の要件

ASCII互換エンコーディング: バイト0x00-0x7F が U+0000-U+007F にマップされる

ASCII非互換エンコーディング: この対応が成立しない(例: UTF-16BE/LE, ISO-2022-JP)

  • WHATWG仕様では、後方互換性が必要な場合以外、新たなASCII非互換エンコーディングの追加は認めない
  • 対応ラベルを置換エンコーディングにマップするか、unknown encodingとして扱う

エンコーディング喪失と情報保護

例: windows-1252でのフォーム送信

ユーザー入力:      💩 (U+1F4A9)
エンコード後:      エラーまたは代替文字
サーバー解釈:      "💩" と代替文字の区別ができない

HTMLフォームのエンコーディングは、特別な場合を除きUTF-8を使用すべきです。

サポート対象エンコーディング

WHATWG仕様でサポートされるエンコーディング(主要なもの):

エンコーディング ラベル例 マルチバイト 用途
UTF-8 utf-8 標準・推奨
UTF-16BE utf-16be ASCII非互換
UTF-16LE utf-16le ASCII非互換
ISO-2022-JP iso-2022-jp レガシー日本語
Shift_JIS shift_jis, sjis レガシー日本語
GBK gbk レガシー中国語
GB18030 gb18030 中国語標準
windows-1252 windows-1252 Webレガシー
ISO-8859-1 iso-8859-1 Webレガシー

TextDecoder/TextEncoderの実装

JavaScript環境でのエンコーディング操作:

// TextDecoder: バイト列 -> 文字列
const decoder = new TextDecoder('utf-8', { fatal: false });
const bytes = new Uint8Array([0xE3, 0x81, 0x82]); // 'あ' in UTF-8
const text = decoder.decode(bytes);  // 'あ'

// fatal: true なら不正なバイト列で例外発生
try {
    const strictDecoder = new TextDecoder('utf-8', { fatal: true });
    strictDecoder.decode(new Uint8Array([0xFF, 0xFF]));
} catch (e) {
    console.log('Invalid UTF-8 sequence');
}

// TextEncoder: 文字列 -> UTF-8バイト列(常にUTF-8)
const encoder = new TextEncoder();
const utf8Bytes = encoder.encode('こんにちは');
console.log(utf8Bytes.length);  // 15 bytes

エンコーディング自動検出の優先順位

ブラウザはHTTPレスポンスやHTMLメタデータからエンコーディングを決定します(WHATWG仕様順序):

  1. HTTP Content-Type ヘッダ - 最優先
  2. HTML <meta charset> - 同期的スキャン可能範囲内
  3. BOM(Byte Order Mark) - UTF-8/UTF-16のみ
  4. レガシー <meta http-equiv> - 後方互換性
  5. 言語モデル - 統計的推定(フォールバック)
<!-- 推奨: Content-Type ヘッダ -->
<!-- Server: Content-Type: text/html; charset=utf-8 -->

<!-- HTML内での明示 -->
<head>
  <meta charset="utf-8">
</head>

<!-- BOM(自動検出) -->
<!-- UTF-8 BOM: EF BB BF -->
<!-- UTF-16BE BOM: FE FF -->
<!-- UTF-16LE BOM: FF FE -->

Python実装例:

# 標準ライブラリ chardet での自動検出
from chardet.universaldetector import UniversalDetector

detector = UniversalDetector()
with open('file.txt', 'rb') as f:
    for line in f:
        detector.feed(line)
        if detector.done:
            break

detected = detector.result
# {'encoding': 'UTF-8', 'confidence': 0.99, ...}

# aiohttp での自動検出
import aiohttp
async with aiohttp.ClientSession() as session:
    async with session.get(url) as resp:
        # resp.encoding が自動検出された値
        text = await resp.text()  # 自動エンコーディング適用

ストリーミング型デコーディング

テキストのストリーミング受信を想定した処理:

# Python 3.9+ の TextDecoder(概念的な実装)
import codecs

decoder = codecs.getincrementaldecoder('utf-8')()

# チャンク単位でデータを受信して処理
chunks = [
    b'あ',  # 'あ' 
    b'い',  # 'い'
]

for chunk in chunks:
    text = decoder.decode(chunk, False)  # False: final=False(続きあり)
    print(f"Decoded: {text}")

# 最後のチャンク
final_text = decoder.decode(b'', True)  # True: final=True(終了)

まとめ

文字列は、見た目、grapheme cluster、code point、code unit、encoding、bytesの層に分けて考えると理解しやすくなります。UTF-8正規化改行コード、絵文字、照合、URL、DB、ファイル名の扱いを押さえると、文字化けや比較の失敗を落ち着いて調べられます。特に入力制限や識別子では、ユーザーが見る文字とシステムが数える単位を混同しないことが重要です。

参考文献

公式・標準