文字コード入門 - 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数字(09
0x41〜0x5A英大文字(AZ
0x61〜0x7A英小文字(az
0x7FDEL(削除)

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+FFFFBMP(Basic Multilingual Plane)ラテン文字、CJK統合漢字、ひらがな等
第1面U+10000〜U+1FFFFSMP(Supplementary Multilingual Plane)絵文字、古代文字、音楽記号
第2面U+20000〜U+2FFFFSIP(Supplementary Ideographic Plane)CJK統合漢字拡張B以降
第3〜13面U+30000〜U+DFFFF未割り当てまたは予約
第14面U+E0000〜U+EFFFFSSP(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+007F1バイト0xxxxxxx7ビット
U+0080〜U+07FF2バイト110xxxxx 10xxxxxx11ビット
U+0800〜U+FFFF3バイト1110xxxx 10xxxxxx 10xxxxxx16ビット
U+10000〜U+10FFFF4バイト11110xxx 10xxxxxx 10xxxxxx 10xxxxxx21ビット

エンコード例

「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
  • 自己同期性: バイト列の途中から読み始めても、先頭バイトと継続バイトの区別ができる(0xxxxxxx or 11xxxxxx が先頭、10xxxxxx が継続)
  • ソート順: バイト列の辞書順がコードポイントの昇順と一致する
  • エンディアン非依存: バイト単位の処理のためBOMが不要

UTF-16

Unicodeの符号化方式の1つ。BMP内の文字は2バイト、BMP外の文字はサロゲートペアにより4バイトで表現する。

コードポイント範囲バイト数表現方法
U+0000〜U+FFFF2バイトコードポイントをそのまま16ビットで格納
U+10000〜U+10FFFF4バイトサロゲートペア(上位 + 下位)

UTF-16にはバイトオーダーの問題があり、UTF-16BE(ビッグエンディアン)とUTF-16LE(リトルエンディアン)の2種類がある。Windows内部やJava、JavaScript(ES2015以前)の文字列内部表現として使われている。

UTF-32

全てのコードポイントを固定長の4バイト(32ビット)で表現する。ランダムアクセスが容易だが、メモリ効率が悪い。実用上はほぼ使われない。

コードポイント範囲バイト数表現方法
U+0000〜U+10FFFF4バイトコードポイントをそのまま32ビットで格納

UTF比較表

特性UTF-8UTF-16UTF-32
最小バイト数124
最大バイト数444
ASCII互換ありなしなし
エンディアン問題なしありあり
BOM不要(付与可能)推奨推奨
主な用途ファイル保存、Web、通信Windows/Java内部内部処理(稀)

日本語の文字コード

Shift_JIS

MicrosoftとASCII社が共同開発した符号化方式。JIS X 0201(半角カナ含む)とJIS X 0208(漢字等)を1〜2バイトの可変長で符号化する。

バイト数範囲内容
1バイト0x00〜0x7FASCII
1バイト0xA1〜0xDF半角カタカナ
2バイト第1バイト: 0x81〜0x9F, 0xE0〜0xEFJIS X 0208の漢字・記号等

Windowsでは**CP932(Windows-31J)**と呼ばれる拡張版が使われ、NEC特殊文字やIBM拡張文字が追加されている。Shift_JISとCP932は厳密には異なるが、実務上は混同されることが多い。

注意点: \(0x5C)がバックスラッシュと円記号の両方に使われる問題がある。また、2バイト目に0x5Cを含む漢字(「表」「ソ」「能」等)がパス区切りやエスケープ文字と誤認される問題が有名。

EUC-JP

UNIX環境で広く使われた符号化方式。JIS X 0208をベースとする。

バイト数範囲内容
1バイト0x00〜0x7FASCII
2バイト0xA1〜0xFE, 0xA1〜0xFEJIS 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_JISEUC-JPISO-2022-JPUTF-8
最大バイト数/文字232(+エスケープ)3〜4
ASCII互換ありありあり(7ビット)あり
UNIX系での利用多(歴史的)メール標準
Windowsでの利用標準(CP932)増加中
絵文字対応なしなしなしあり

BOM(Byte Order Mark)

**BOM(Byte Order Mark)**は、テキストファイルの先頭に配置される特殊なバイト列で、符号化方式とバイトオーダーを示す。UnicodeのコードポイントU+FEFFを使用する。

符号化方式BOMバイト列
UTF-8EF BB BF
UTF-16 BEFE FF
UTF-16 LEFF FE
UTF-32 BE00 00 FE FF
UTF-32 LEFF 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'&#12486;&#12473;&#12488;'

# デコード時にエラーハンドリング
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外の文字
JavaScriptUTF-16UTF-16コード単位数サロゲートペアで2カウント
Python 3UCS(可変)コードポイント数1カウント
JavaUTF-16UTF-16コード単位数サロゲートペアで2カウント
GoUTF-8バイト数3〜4カウント
RustUTF-8バイト数3〜4カウント
C#UTF-16UTF-16コード単位数サロゲートペアで2カウント

参考リンク