文字コード入門 - UTF-8, Shift_JIS等の仕組みと使い分け
文字コードは、コンピュータが文字を数値として扱うための規則。文字集合(どの文字を扱うか)と符号化方式(その文字をどのバイト列で表現するか)の2つの概念で構成される。
文字集合と符号化方式
文字コードを正しく理解するには、**文字集合(Character Set)と符号化方式(Character Encoding)**を区別する必要がある。
| 概念 | 説明 | 例 |
|---|---|---|
| 文字集合 | 扱う文字の集合と、各文字に割り当てたコードポイント | Unicode, JIS X 0208 |
| 符号化方式 | コードポイントをバイト列に変換する方法 | UTF-8, UTF-16, Shift_JIS |
同じ文字集合でも異なる符号化方式を持つことがある。例えばUnicodeという文字集合に対して、UTF-8・UTF-16・UTF-32という異なる符号化方式が存在する。
ASCII
**ASCII(American Standard Code for Information Interchange)**は、1963年に策定された7ビットの文字コード。128文字(0x00〜0x7F)を定義する。
| 範囲 | 内容 |
|---|---|
| 0x00〜0x1F | 制御文字(改行、タブ等) |
| 0x20 | スペース |
| 0x21〜0x2F | 記号(!, ", # 等) |
| 0x30〜0x39 | 数字(0〜9) |
| 0x41〜0x5A | 英大文字(A〜Z) |
| 0x61〜0x7A | 英小文字(a〜z) |
| 0x7F | DEL(削除) |
ASCIIは英語圏の文字しか含まないため、各国が8ビット目を利用して独自の拡張を行った(ISO 8859シリーズ等)。これが文字コードの乱立と文字化けの遠因となった。
Unicode
Unicodeは、世界中の文字を単一の文字集合で扱うことを目指した国際規格。各文字にコードポイント(U+ に続く16進数)を割り当てる。
A → U+0041
あ → U+3042
😀 → U+1F600
コードポイントの範囲
Unicodeのコードポイントは U+0000 〜 U+10FFFF の範囲で、**面(Plane)**に分類される。
| 面 | 範囲 | 名称 | 主な内容 |
|---|---|---|---|
| 第0面 | U+0000〜U+FFFF | BMP(Basic Multilingual Plane) | ラテン文字、CJK統合漢字、ひらがな等 |
| 第1面 | U+10000〜U+1FFFF | SMP(Supplementary Multilingual Plane) | 絵文字、古代文字、音楽記号 |
| 第2面 | U+20000〜U+2FFFF | SIP(Supplementary Ideographic Plane) | CJK統合漢字拡張B以降 |
| 第3〜13面 | U+30000〜U+DFFFF | 未割り当てまたは予約 | — |
| 第14面 | U+E0000〜U+EFFFF | SSP(Supplement Special-purpose Plane) | タグ文字等 |
| 第15〜16面 | U+F0000〜U+10FFFF | 私用面 | アプリケーション固有の用途 |
日常的に使用する文字の大半はBMP内に収まる。絵文字や一部の漢字はBMP外(第1面以降)に配置されている。
サロゲートペア
UTF-16でBMP外の文字(U+10000以降)を表現するための仕組み。2つの16ビット値(上位サロゲート U+D800〜U+DBFF、下位サロゲート U+DC00〜U+DFFF)の組み合わせで1文字を表す。
U+1F600(😀)のサロゲートペア計算:
1. コードポイントからU+10000を引く: 0x1F600 - 0x10000 = 0xF600
2. 上位10ビット: 0xF600 >> 10 = 0x003D → 上位サロゲート: 0xD83D (0xD800 + 0x003D)
3. 下位10ビット: 0xF600 & 0x3FF = 0x0200 → 下位サロゲート: 0xDE00 (0xDC00 + 0x0200)
結果: 0xD83D 0xDE00
サロゲートペアの範囲(U+D800〜U+DFFF)は文字のコードポイントとしては使用されない。
UTF-8
UTF-8は、Unicodeの符号化方式で最も広く使われている。可変長エンコーディングであり、1文字を1〜4バイトで表現する。
エンコーディング規則
| コードポイント範囲 | バイト数 | ビットパターン | 格納可能ビット数 |
|---|---|---|---|
| U+0000〜U+007F | 1バイト | 0xxxxxxx | 7ビット |
| U+0080〜U+07FF | 2バイト | 110xxxxx 10xxxxxx | 11ビット |
| U+0800〜U+FFFF | 3バイト | 1110xxxx 10xxxxxx 10xxxxxx | 16ビット |
| U+10000〜U+10FFFF | 4バイト | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 21ビット |
エンコード例
「A」U+0041 → 0x41
0041 = 0100 0001
→ 01000001
→ 0x41(1バイト)
「あ」U+3042 → 0xE3 0x81 0x82
3042 = 0011 0000 0100 0010
→ 1110_0011 10_000001 10_000010
→ 0xE3 0x81 0x82(3バイト)
「😀」U+1F600 → 0xF0 0x9F 0x98 0x80
1F600 = 0 0001 1111 0110 0000 0000
→ 11110_000 10_011111 10_011000 10_000000
→ 0xF0 0x9F 0x98 0x80(4バイト)
UTF-8の特徴
- ASCII互換: U+0000〜U+007Fは1バイトでASCIIと同一。既存のASCIIテキストはそのまま有効なUTF-8
- 自己同期性: バイト列の途中から読み始めても、先頭バイトと継続バイトの区別ができる(
0xxxxxxxor11xxxxxxが先頭、10xxxxxxが継続) - ソート順: バイト列の辞書順がコードポイントの昇順と一致する
- エンディアン非依存: バイト単位の処理のためBOMが不要
UTF-16
Unicodeの符号化方式の1つ。BMP内の文字は2バイト、BMP外の文字はサロゲートペアにより4バイトで表現する。
| コードポイント範囲 | バイト数 | 表現方法 |
|---|---|---|
| U+0000〜U+FFFF | 2バイト | コードポイントをそのまま16ビットで格納 |
| U+10000〜U+10FFFF | 4バイト | サロゲートペア(上位 + 下位) |
UTF-16にはバイトオーダーの問題があり、UTF-16BE(ビッグエンディアン)とUTF-16LE(リトルエンディアン)の2種類がある。Windows内部やJava、JavaScript(ES2015以前)の文字列内部表現として使われている。
UTF-32
全てのコードポイントを固定長の4バイト(32ビット)で表現する。ランダムアクセスが容易だが、メモリ効率が悪い。実用上はほぼ使われない。
| コードポイント範囲 | バイト数 | 表現方法 |
|---|---|---|
| U+0000〜U+10FFFF | 4バイト | コードポイントをそのまま32ビットで格納 |
UTF比較表
| 特性 | UTF-8 | UTF-16 | UTF-32 |
|---|---|---|---|
| 最小バイト数 | 1 | 2 | 4 |
| 最大バイト数 | 4 | 4 | 4 |
| ASCII互換 | あり | なし | なし |
| エンディアン問題 | なし | あり | あり |
| BOM | 不要(付与可能) | 推奨 | 推奨 |
| 主な用途 | ファイル保存、Web、通信 | Windows/Java内部 | 内部処理(稀) |
日本語の文字コード
Shift_JIS
MicrosoftとASCII社が共同開発した符号化方式。JIS X 0201(半角カナ含む)とJIS X 0208(漢字等)を1〜2バイトの可変長で符号化する。
| バイト数 | 範囲 | 内容 |
|---|---|---|
| 1バイト | 0x00〜0x7F | ASCII |
| 1バイト | 0xA1〜0xDF | 半角カタカナ |
| 2バイト | 第1バイト: 0x81〜0x9F, 0xE0〜0xEF | JIS X 0208の漢字・記号等 |
Windowsでは**CP932(Windows-31J)**と呼ばれる拡張版が使われ、NEC特殊文字やIBM拡張文字が追加されている。Shift_JISとCP932は厳密には異なるが、実務上は混同されることが多い。
注意点: \(0x5C)がバックスラッシュと円記号の両方に使われる問題がある。また、2バイト目に0x5Cを含む漢字(「表」「ソ」「能」等)がパス区切りやエスケープ文字と誤認される問題が有名。
EUC-JP
UNIX環境で広く使われた符号化方式。JIS X 0208をベースとする。
| バイト数 | 範囲 | 内容 |
|---|---|---|
| 1バイト | 0x00〜0x7F | ASCII |
| 2バイト | 0xA1〜0xFE, 0xA1〜0xFE | JIS X 0208 |
| 3バイト | 0x8F + 2バイト | JIS X 0212(補助漢字) |
EUC-JPはShift_JISと異なり、2バイト目の範囲がASCII領域と重複しないため、バイト列の走査が安全。
ISO-2022-JP
エスケープシーケンスで文字集合を切り替える方式。電子メール(MIME)で標準的に使われてきた。7ビットクリーンであり、8ビット目を使用しない。
ESC $ B → JIS X 0208に切り替え(日本語モード)
ESC ( B → ASCIIに切り替え(英語モード)
現在は新規採用されることはほぼなく、UTF-8が推奨される。
日本語文字コード比較表
| 特性 | Shift_JIS | EUC-JP | ISO-2022-JP | UTF-8 |
|---|---|---|---|---|
| 最大バイト数/文字 | 2 | 3 | 2(+エスケープ) | 3〜4 |
| ASCII互換 | あり | あり | あり(7ビット) | あり |
| UNIX系での利用 | 少 | 多(歴史的) | メール | 標準 |
| Windowsでの利用 | 標準(CP932) | 少 | 少 | 増加中 |
| 絵文字対応 | なし | なし | なし | あり |
BOM(Byte Order Mark)
**BOM(Byte Order Mark)**は、テキストファイルの先頭に配置される特殊なバイト列で、符号化方式とバイトオーダーを示す。UnicodeのコードポイントU+FEFFを使用する。
| 符号化方式 | BOMバイト列 |
|---|---|
| UTF-8 | EF BB BF |
| UTF-16 BE | FE FF |
| UTF-16 LE | FF FE |
| UTF-32 BE | 00 00 FE FF |
| UTF-32 LE | FF FE 00 00 |
UTF-8のBOM
UTF-8にはエンディアンの問題がないため、BOMは本来不要。しかし、Windowsのメモ帳(旧バージョン)などは「UTF-8 with BOM」で保存する。
# ファイル先頭のBOMを確認する
xxd file.txt | head -1
# BOM付きUTF-8の場合: 00000000: efbb bf...
# BOMを除去する(Linux/macOS)
sed -i '1s/^\xEF\xBB\xBF//' file.txt
BOMの注意点:
- シェルスクリプトの
#!/bin/bashの前にBOMがあると実行エラーになる - PHPファイルの先頭にBOMがあると、ヘッダー送信前に出力が発生しエラーになる
- JSONはRFC 8259でBOMを付与すべきでないと規定されている
- CSVファイルにBOMを付けるとExcelが正しくUTF-8として認識する
文字化けの原因と対策
文字化けは、書き手と読み手で異なる符号化方式を使用した場合に発生する。
典型的なパターン
| 現象 | 原因 |
|---|---|
æ–‡å— のようなラテン文字の羅列 | UTF-8のテキストをISO-8859-1/Latin-1として解釈 |
文字化ã?? のような化け方 | UTF-8のテキストをShift_JISとして解釈 |
•¶Žš‰»‚¯ のような記号列 | Shift_JISのテキストをUTF-8として解釈 |
文字 のような数値参照 | 文字実体参照がデコードされていない |
? や □(豆腐) | 対象の文字が表示フォントに存在しない |
? に置換される | 変換先の文字コードに該当文字が存在しない |
対策
# ファイルの文字コードを判定する
file -i document.txt
# text/plain; charset=utf-8
# nkfで文字コードを判定する(日本語特化)
nkf --guess document.txt
# UTF-8 (LF)
# Shift_JIS → UTF-8に変換する
nkf -w --overwrite document.txt
# iconv で変換する
iconv -f SHIFT_JIS -t UTF-8 input.txt > output.txt
HTMLでの文字コード指定
HTMLでは <meta charset> で文書の文字エンコーディングを宣言する。
<!DOCTYPE html>
<html lang="ja">
<head>
<!-- HTML5 -->
<meta charset="UTF-8">
<!-- HTML4(旧形式、非推奨) -->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
指定のルール
<meta charset>はHTMLファイルの先頭1024バイト以内に記述する必要がある- HTTPレスポンスヘッダーの
Content-Typeでもエンコーディングを指定できる - HTTPヘッダーとHTMLの
<meta>の両方で指定されている場合、HTTPヘッダーが優先される - WHATWGの仕様ではUTF-8が推奨されている
Content-Type: text/html; charset=UTF-8
HTTPヘッダーとmetaタグの優先順位
1. BOM(存在する場合)
2. HTTP Content-Type ヘッダー
3. <meta charset> / <meta http-equiv="Content-Type">
4. ブラウザの自動検出(不確実)
プログラミング言語での文字列内部表現
JavaScript / TypeScript
JavaScriptの文字列はUTF-16で内部的に表現される。length プロパティはUTF-16のコード単位数を返すため、BMP外の文字では直感に反する結果になる。
// BMP内の文字
'あ'.length // 1
'あ'.charCodeAt(0) // 12354 (0x3042)
// BMP外の文字(サロゲートペア)
'😀'.length // 2(UTF-16コード単位が2つ)
'😀'.charCodeAt(0) // 55357 (0xD83D) — 上位サロゲート
'😀'.charCodeAt(1) // 56832 (0xDE00) — 下位サロゲート
// コードポイント単位で扱う方法
'😀'.codePointAt(0) // 128512 (0x1F600)
[...'😀'].length // 1(スプレッド構文はコードポイント単位で展開)
Array.from('😀').length // 1
// 文字数を正確にカウントする
function countChars(str) {
return [...str].length
}
// コードポイントから文字を生成する
String.fromCodePoint(0x1F600) // '😀'
注意: 結合文字やZWJ(Zero Width Joiner)シーケンスを含む絵文字(例: 👨👩👧👦)は、コードポイント単位で分割しても正しい文字数にならない。Intl.Segmenter を使う。
// 書記素クラスタ単位でカウントする
const segmenter = new Intl.Segmenter('ja', { granularity: 'grapheme' })
const segments = [...segmenter.segment('👨👩👧👦')]
segments.length // 1
Python
Python 3の str 型はUnicodeコードポイントの列。内部的にはPEP 393により、文字列に含まれる最大のコードポイントに応じてLatin-1(1バイト)、UCS-2(2バイト)、UCS-4(4バイト)のいずれかで格納される。
# lenはコードポイント数を返す
len('あ') # 1
len('😀') # 1(Pythonはコードポイント単位)
# ordでコードポイントを取得する
ord('あ') # 12354 (0x3042)
ord('😀') # 128512 (0x1F600)
# chrでコードポイントから文字を生成する
chr(0x3042) # 'あ'
chr(0x1F600) # '😀'
# bytesとstrの相互変換
'あいう'.encode('utf-8') # b'\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86'
b'\xe3\x81\x82'.decode('utf-8') # 'あ'
# エンコード時にエラーハンドリング
'テスト'.encode('ascii', errors='replace') # b'???'
'テスト'.encode('ascii', errors='ignore') # b''
'テスト'.encode('ascii', errors='xmlcharrefreplace') # b'テスト'
# デコード時にエラーハンドリング
b'\x82\xa0'.decode('utf-8', errors='replace') # '��'(不正なUTF-8バイト列)
Java
Javaの char は16ビット(UTF-16コード単位)。String.length() はUTF-16コード単位数を返す。コードポイント単位での操作には codePointAt() / codePointCount() を使用する。
String s = "😀";
s.length() // 2(UTF-16コード単位)
s.codePointCount(0, s.length()) // 1(コードポイント数)
s.codePointAt(0) // 128512 (0x1F600)
Go
Goの string はバイト列(UTF-8)。len() はバイト数を返す。文字(ルーン)単位での操作には []rune 変換または utf8.RuneCountInString() を使用する。
s := "あ"
len(s) // 3(UTF-8で3バイト)
utf8.RuneCountInString(s) // 1(ルーン数)
[]rune(s) // [12354]
言語別の文字列内部表現比較
| 言語 | 内部表現 | lengthが返す値 | BMP外の文字 |
|---|---|---|---|
| JavaScript | UTF-16 | UTF-16コード単位数 | サロゲートペアで2カウント |
| Python 3 | UCS(可変) | コードポイント数 | 1カウント |
| Java | UTF-16 | UTF-16コード単位数 | サロゲートペアで2カウント |
| Go | UTF-8 | バイト数 | 3〜4カウント |
| Rust | UTF-8 | バイト数 | 3〜4カウント |
| C# | UTF-16 | UTF-16コード単位数 | サロゲートペアで2カウント |
参考リンク
- The Unicode Standard — Unicode公式仕様
- Unicode Code Charts — コードポイント一覧表
- RFC 3629 - UTF-8 — UTF-8の仕様(IETF)
- RFC 2781 - UTF-16 — UTF-16の仕様(IETF)
- RFC 8259 - The JSON Data Interchange Format — JSONでのエンコーディング規定
- MDN - TextEncoder — Web APIでのエンコーディング
- MDN - Intl.Segmenter — 書記素クラスタ分割
- WHATWG Encoding Standard — Webプラットフォームのエンコーディング仕様
- PEP 393 - Flexible String Representation — Python 3の文字列内部表現