テキストファイルを保存するときに何気なく目にする「UTF-8」とかいう文字コードが、実際にはどのような仕組み?仕様?で動いているのかが気になったので調べてみました。
タマに目にするUnicodeというのも絡めて書いていきます。UTF-8の符号化を行う簡単なコードも書いてみます。
UnicodeとUTF-8
Unicode
文字コードの規格。どのような文字があるか(文字集合)や、どのようにデータとして変換するか(符号化)などを決めている。Unicodeに含まれている全ての文字は、コードポイント(符号位置)というIDみたいなものが割り振られている。
UTF-8
Unicodeで定められている符号化方式(文字をコンピュータで扱えるデータに変換するための方式)の一つ。他にUTF-16、UTF-32等の符号化方式がある。
Unicodeと各符号化方式
Unicodeでは、「あ」という文字に「U+3042」というコードポイントを与えています。これを実際にコンピュータ上で扱うには「e38182」というデータに変換します(UTF-8を使用する場合)。
コードポイントと、各符号化方式で符号化したデータの比較は以下のようになります。(※UTF-16、UTF-32はビッグエンディアン)
文字 | コードポイント | UTF-8 | UTF-16 | UTF-32 |
---|---|---|---|---|
1 | U+0031 | 31 | 00 31 | 00 00 00 31 |
A | U+0041 | 41 | 00 41 | 00 00 00 41 |
Ă | U+0102 | c4 82 | 01 02 | 00 00 01 02 |
あ | U+3042 | e3 81 82 | 30 42 | 00 00 30 42 |
丈 | U+4E08 | e4 b8 88 | 4e 08 | 00 00 4e 08 |
U+1D49C | f0 9d 92 9c | d8 35 dc 9c | 00 01 d4 9c | |
U+2000B | f0 a0 80 8b | d8 40 dc 0b | 00 02 00 0b |
UTF-32はコードポイントをそのまま4バイト(16進数で8桁)にしたものです。変換方法がとても単純ですがデータ量が多くなるのであまり使用されていません。
UTF-16は2バイト(16進数で4桁)のデータ(1、A、Ă、あ、丈)と4バイトのデータ(、)があります。コードポイントが4桁までの文字は2バイトになり、5桁以上の文字は4バイトになります(サロゲートペアという仕組みを使用する)。
UTF-8では、文字が1バイトから4バイトまでのデータに変換されます。他の方式と比べて変換方法が複雑そうに見えます。一般的に利用されています。
UTF-8の符号化方法
UTF-8が実際にどのような符号化方法を行っているのかを見るために、先ほどの表で出てきたUTF-8のデータの各バイトをビット列に変換してみます。
文字 | UTF-8のデータ | バイト | 1バイト目 | 2バイト目 | 3バイト目 | 4バイト目 |
---|---|---|---|---|---|---|
1 | 31 | 1byte | 00110001 | |||
A | 41 | 1byte | 01000001 | |||
Ă | c4 82 | 2byte | 11000100 | 10000010 | ||
あ | e3 81 82 | 3byte | 11100011 | 10000001 | 10000010 | |
丈 | e4 b8 88 | 3byte | 11100100 | 10111000 | 10001000 | |
f0 9d 92 9c | 4byte | 11110000 | 10011101 | 10010010 | 10011100 | |
f0 a0 80 8b | 4byte | 11110000 | 10100000 | 10000000 | 10001011 |
赤い部分はUTF-8での固定部分で各文字で共通になります。青い部分は文字ごとに変化します。UTF-8で符号化したデータには以下の規則があります。
- 1バイト文字 = 先頭ビットが0で固定
- 2バイト文字 = 1バイト目は「110」から始まり、2バイト目は「10」から始まる
- 3バイト文字 = 1バイト目は「1110」から始まり、2バイト目以降は「10」から始まる
- 4バイト文字 = 1バイト目は「11110」から始まり、2バイト目以降は「10」から始まる
この表の青い部分をそれぞれつなげてみます。つなげる時に、バイト単位(8ビット)にするため足りないビットを補います。
それを16進数に変換したものとコードポイントを比較すると一致することが分ります。
文字 | 青い部分をつなげて足りないビットを補う | 16進数に変換 | コードポイント |
---|---|---|---|
1 | 00110001 | 31 | U+0031 |
A | 01000001 | 41 | U+0041 |
Ă | 00000001 00000010 | 01 02 | U+0102 |
あ | 00110000 01000010 | 30 42 | U+3042 |
丈 | 01001110 00001000 | 4e 08 | U+4E08 |
00000001 11010100 10011100 | 01 c4 9c | U+1D49C | |
00000010 00000000 00001011 | 02 00 0b | U+2000B |
UnicodeからUTF-8に変換するには、逆の操作を行います。
コードポイントをUTF-8に変換するときに、何バイトの文字になるかはコードポイントの範囲で決まります。UTF-8では固定ビットが存在するので、有効ビット数の範囲内で文字を表現できます。
UTF-8のバイト数 | 1byte目 | 2byte目 | 3byte目 | 4byte目 | 有効ビット数 | コードポイントの範囲 |
---|---|---|---|---|---|---|
1byte文字 | 00000000 | 7 | U+0000~U+007F | |||
2byte文字 | 11000000 | 10000000 | 11 | U+0080~U+07FF | ||
3byte文字 | 11100000 | 10000000 | 10000000 | 16 | U+0800~U+FFFF | |
4byte文字 | 11110000 | 10000000 | 10000000 | 10000000 | 21 | U+10000~U+10FFFF |
プログラム化してみる
上記の仕様をPHPでプログラムしてみました。
UTF-8の文字からコードポイントを取得します。
/**
* UTF-8文字からコードポイントを取得する
* @param string UTF-8文字
* @return string U+xxxx
*/
function utf8ToCp($str)
{
// 文字列だけを扱う
if (!is_string($str)) {
return false;
}
// 先頭1文字だけが対象
$ch = mb_substr($str, 0, 1, 'UTF-8');
if ($ch === '') {
return false;
}
// 16進数のコードに変換
$chHex = bin2hex($ch);
// 長さで分ける
if (strlen($chHex) <= 2) { // 1バイト文字 $byte1 = sprintf('%08s', base_convert($chHex, 16, 2)); // 先頭ビットの確認 // UTF-8の仕様に合わない場合はfalseを返す if (0 !== strpos($byte1, '0')) { return false; } // コードポイントのビット表現 $bin = $byte1; } elseif (strlen($chHex) == 4) { // 2バイト文字 $byte1 = base_convert(substr($chHex, 0, 2), 16, 2); $byte2 = base_convert(substr($chHex, 2, 2), 16, 2); if (0 !== strpos($byte1, '110') || 0 !== strpos($byte2, '10')) { return false; } $bin = substr($byte1, 3, 5) . substr($byte2, 2, 6); } elseif (strlen($chHex) == 6) { // 3バイト文字 $byte1 = base_convert(substr($chHex, 0, 2), 16, 2); $byte2 = base_convert(substr($chHex, 2, 2), 16, 2); $byte3 = base_convert(substr($chHex, 4, 2), 16, 2); if (0 !== strpos($byte1, '1110') || 0 !== strpos($byte2, '10') || 0 !== strpos($byte3, '10')) { return false; } $bin = substr($byte1, 4, 4) . substr($byte2, 2, 6) . substr($byte3, 2, 6); } elseif (strlen($chHex) == 8) { // 4バイト文字 $byte1 = base_convert(substr($chHex, 0, 2), 16, 2); $byte2 = base_convert(substr($chHex, 2, 2), 16, 2); $byte3 = base_convert(substr($chHex, 4, 2), 16, 2); $byte4 = base_convert(substr($chHex, 6, 2), 16, 2); if (0 !== strpos($byte1, '11110') || 0 !== strpos($byte2, '10') || 0 !== strpos($byte3, '10') || 0 !== strpos($byte4, '10')) { return false; } $bin = substr($byte1, 5, 3) . substr($byte2, 2, 6) . substr($byte3, 2, 6) . substr($byte4, 2, 6); } else { return false; } // コードポイントを16進数に変換 $cpHex = sprintf('%04s', base_convert($bin, 2, 16)); return "U+{$cpHex}"; } // 実行する echo utf8ToCp('A'); ==> U+0041
echo utf8ToCp('¶');
==> U+00b6
echo utf8ToCp('あ');
==> U+3042
今度はコードポイントからUTF-8に変換します。
/**
* コードポイントからUTF-8に変換す
* @param string U+xxxx
* @return string UTF-8文字
*/
function cpToUtf8($cp)
{
// 数字部分だけ抜き出す
if (!preg_match('/^U+([0-9a-fA-F]{1,6})$/', $cp, $matches)) {
return false;
}
$cpHex = $matches[1];
$cpDec = hexdec($cpHex);
// コードポイントの範囲でバイト数を決定
if ($cpDec <= 0x7f) {
// 1バイト文字(有効桁数7ビット)
$cpBin = base_convert($cpHex, 16, 2);
$bin = sprintf('%08s', $cpBin);
} elseif ($cpDec <= 0x7ff) {
// 2バイト文字(有効桁数11ビット)
$cpBin = sprintf('%011s', base_convert($cpHex, 16, 2));
$bin = sprintf('110%05s', substr($cpBin, 0, 5))
. sprintf('10%06s', substr($cpBin, 5, 6));
} elseif ($cpDec <= 0xffff) {
// 3バイト文字(有効桁数16ビット)
$cpBin = sprintf('%016s', base_convert($cpHex, 16, 2));
$bin = sprintf('1110%04s', substr($cpBin, 0, 4))
. sprintf('10%06s', substr($cpBin, 4, 6))
. sprintf('10%06s', substr($cpBin, 10, 6));
} elseif ($cpDec <= 0x10ffff) { // 4バイト文字(有効桁数21ビット) $cpBin = sprintf('%021s', base_convert($cpHex, 16, 2)); $bin = sprintf('11110%03s', substr($cpBin, 0, 3)) . sprintf('10%06s', substr($cpBin, 3, 6)) . sprintf('10%06s', substr($cpBin, 9, 6)) . sprintf('10%06s', substr($cpBin, 15, 6)); } else { return false; } $hex = base_convert($bin, 2, 16); $char = pack('H*', $hex); return $char; } // 実行する echo cpToUtf8('U+41'); ==> A
echo cpToUtf8('U+3042');
==> あ
PHPでは文字とHTML文字参照を相互に変換する関数が用意されているので、実際のプログラムではそれを利用するのがラクでしょう。
// 文字を文字参照に変換する
echo mb_encode_numericentity('あいう', [0x0, 0x10ffff, 0, 0xffffff], 'UTF-8', true);
==> あいう
// 文字参照を文字に変換する
echo mb_decode_numericentity('あいう', [0x0, 0x10ffff, 0, 0xffffff], 'UTF-8');
==> あいう