目的
テキストエディタがテキストファイルを開く際にどのようにして文字コードを識別しているのかを調べます。
平仮名を比較
以下の文字列をShift-JIS
及びUTF8
として保存します。
あいうえお
Shift-JIS
82 A0 82 A2 82 A4 82 A6 82 A8
文字 | Shift JIS (16進数) | Shift JIS (バイナリ) |
---|---|---|
あ | 82A0 | 10000010 10100000 |
い | 82A2 | 10000010 10100010 |
う | 82A4 | 10000010 10100100 |
え | 82A6 | 10000010 10100110 |
お | 82A8 | 10000010 10101000 |
UTF8
E3 81 82 E3 81 84 E3 81 86 E3 81 88 E3 81 8A
文字 | UTF-8 (16進数) | UTF-8 (バイナリ) |
---|---|---|
あ | E3 81 82 | 11100011 10000001 10000010 |
い | E3 81 84 | 11100011 10000001 10000100 |
う | E3 81 86 | 11100011 10000001 10000110 |
え | E3 81 88 | 11100011 10000001 10001000 |
お | E3 81 8A | 11100011 10000001 10001010 |
英字を比較
UTF8もShift-JISもASCIIコードを元に拡張したもので、ASCIIコードの範囲内であればどちらのバイナリも同じになります。
(これが文字化けした時に英語だけは正常に表示できる所以です)
よって英字しかない場合はどちらのつもりで保存しているかは判別できません。
Shift-JIS
61 69 75 65 6F
UTF8
61 69 75 65 6F
文字 | 16進数 | バイナリ |
---|---|---|
a | 61 | 01100001 |
i | 69 | 01101001 |
u | 75 | 01110101 |
e | 65 | 01100101 |
o | 6F | 01101111 |
文字化けの仕組み
以下のような文字列をUTF8として保存します。
株式会社
その後Shift-JISとして開くと以下のように文字化けします。
譬ェ蠑丈シ夂、セ
これは何故でしょうか?
バイナリを調べてみましょう。
株式会社
をUTF8で保存するとバイナリは以下のようになります。
E6 A0 AA E5 BC 8F E4 BC 9A E7 A4 BE
UTF8では以下のように解釈されます。
E6 A0 AA
→ 株
E5 BC 8F
→ 式
E4 BC 9A
→ 会
E7 A4 BE
→ 社
UTF-8のバイト列をShift-JISの規則で「間違って区切った結果、偶然Shift-JISの有効なコード範囲に合致してしまい、このような文字列が表示されたという訳です。
E6 A0
→ 譬
AA
→ ェ
E5 BC
→ 蠑
8F E4
→ 丈
BC
→ シ
9A E7
→ 夂
A4
→ 、
BE
→ セ
文字化けはデータが壊れたわけではなく、エディタが間違った文字コードで解釈しているだけです。
宛も、ラテン語の文章を英語だと思い込んで読んでしまい、意味が通らず、たまに綴りが一致する単語だけを英語として読んでしまうようなものです。
テキストエディタはどうやって判別しているのか?
UTF-8の特徴
先頭に BOM と呼ばれる EF BB BF
のビット列があれば UTF-8 と判定できます。
BOM は必須ではなく、無い場合はバイト列の規則から UTF-8 かどうかを判別します。
エディタによっては、UTF-8 保存時に BOM を付けるかどうか選択できます。
UTF-8 は 1~4 バイトで 1 文字を表現します。
BOM が無い場合でも、バイト列の規則(先頭バイトと続きバイトのビット列)を確認することで、UTF-8 かどうかをある程度判断できます。
バイト数 | ビットパターン | 備考 |
---|---|---|
1バイト | 0xxxxxxx | ASCII文字 |
2バイト | 110xxxxx 10xxxxxx | 先頭+続き |
3バイト | 1110xxxx 10xxxxxx 10xxxxxx | 先頭+2バイト続き |
4バイト | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 先頭+3バイト続き |
Shift-JISの特徴
Shift-JIS は 1~2 バイトで 1 文字を表現します。
バイト数 | ビットパターン | 備考 |
---|---|---|
1バイト | 0xxxxxxx | ASCII文字 |
2バイト | 1バイト目: 10000001〜10011111 または 11100000〜11101111 2バイト目: 01000000〜01111110 または 10000000〜11111100 |
日本語文字 |
日本語文字(漢字・ひらがな・カタカナ)は2バイトで表されます。
簡単な判別コード
「UTF-8 っぽいか / Shift-JIS っぽいか」を判別するCコードを書いてみます。
testuser@CasaOS:~/test$ gcc test.c -o test
testuser@CasaOS:~/test$ ./test
[UTF-8 BOM付き] UTF-8 (BOM付き)
[UTF-8 BOMなし] UTF-8 (BOMなし)
[Shift-JIS] Shift-JIS
[壊れたデータ] 不正または未知のエンコード
#include <stdio.h>
#include <stdbool.h>
bool has_utf8_bom(const unsigned char *data, size_t len) {
return (len >= 3 &&
data[0] == 0xEF &&
data[1] == 0xBB &&
data[2] == 0xBF);
}
/**
* is_valid_utf8
* -------------------------------
* UTF-8 かどうかを判定する関数
*
* UTF-8 のバイトパターン規則:
*
* バイト数 | 先頭バイトビットパターン | 16進数範囲 | 続きバイトビットパターン | 続きバイト16進数範囲
* -------- | --------------------------- | ---------------- | ---------------------- | ------------------
* 1バイト | 0xxxxxxx | 0x00 ~ 0x7F | - | -
* 2バイト | 110xxxxx | 0xC0 ~ 0xDF | 10xxxxxx | 0x80 ~ 0xBF
* 3バイト | 1110xxxx | 0xE0 ~ 0xEF | 10xxxxxx | 0x80 ~ 0xBF
* | | | 10xxxxxx | 0x80 ~ 0xBF
* 4バイト | 11110xxx | 0xF0 ~ 0xF7 | 10xxxxxx | 0x80 ~ 0xBF
* | | | 10xxxxxx | 0x80 ~ 0xBF
* | | | 10xxxxxx | 0x80 ~ 0xBF
*
* 判定方法:
* 1. 先頭バイトの値で文字バイト数を判定
* 2. 必要な続きバイトが存在するか確認
* 3. 続きバイトがすべて 10xxxxxx (0x80~0xBF) であるか確認
* 4. 上記に違反した場合は false を返す
*
* @param data バイト列
* @param len バイト列の長さ
* @return UTF-8 なら true, それ以外は false
*/
bool is_valid_utf8(const unsigned char *data, size_t len) {
size_t i = 0;
while (i < len) {
unsigned char c = data[i];
if (c <= 0x7F) { // 1バイト文字 0xxxxxxx
i++;
} else if ((c & 0xE0) == 0xC0) { // 2バイト文字の開始(110xxxxx)かを確認 0xC0 = 1100 0000
if (i+1 >= len) return false; // 続きバイトが存在しない
if ((data[i+1] & 0xC0) != 0x80) return false; // 続きバイトが 10xxxxxx かを確認
i += 2;
} else if ((c & 0xF0) == 0xE0) { // 3バイト文字の開始(1110xxxx)かを確認 0xE0 = 1110 1111
if (i+2 >= len) return false;
if ((data[i+1] & 0xC0) != 0x80) return false; // 同上
if ((data[i+2] & 0xC0) != 0x80) return false; // 同上
i += 3;
} else if ((c & 0xF8) == 0xF0) { // 4バイト文字の開始(11110xxx)かを確認 0xF0 = 11110 0000
if (i+3 >= len) return false;
if ((data[i+1] & 0xC0) != 0x80) return false; // 同上
if ((data[i+2] & 0xC0) != 0x80) return false; // 同上
if ((data[i+3] & 0xC0) != 0x80) return false; // 同上
i += 4;
} else {
return false;
}
}
return true;
}
/**
* is_valid_sjis
* -------------------------------
* Shift-JIS かどうかを判定する関数
*
* Shift-JIS のバイトパターン規則:
*
* バイト数 | 先頭バイトビットパターン | 16進数範囲 | 続きバイトビットパターン | 続きバイト16進数範囲
* -------- | ------------------------------- | ----------------- | ---------------------- | ------------------
* 1バイト | 0xxxxxxx (ASCII) | 0x00 ~ 0x7F | - | -
* 1バイト | 半角カナ (JIS X 0201 カナ) | 0xA1 ~ 0xDF | - | -
* 2バイト | 先頭バイト | 0x81 ~ 0x9F | 2バイト目: 0x40~0x7E または 0x80~0xFC | 0x40 ~ 0x7E, 0x80 ~ 0xFC
* | | 0xE0 ~ 0xEF | 同上 | 同上
*
* 判定方法:
* 1. バイト値で1バイト文字か2バイト文字の先頭か判定
* 2. 2バイト文字の場合は続きバイトが存在するか確認
* 3. 続きバイトが規則内の値か確認
* 4. 上記に違反した場合は false を返す
*
* @param data バイト列
* @param len バイト列の長さ
* @return Shift-JIS なら true, それ以外は false
*/
bool is_valid_sjis(const unsigned char *data, size_t len) {
size_t i = 0;
while (i < len) {
unsigned char c = data[i];
if ((c <= 0x7F) || (c >= 0xA1 && c <= 0xDF)) {
// 1バイト文字(ASCII または 半角カナ)
i++;
} else if ((c >= 0x81 && c <= 0x9F) || (c >= 0xE0 && c <= 0xEF)) {
// 2バイト文字の先頭
if (i+1 >= len) return false; // 続きバイトが存在しない
unsigned char c2 = data[i+1];
if (!((c2 >= 0x40 && c2 <= 0x7E) || (c2 >= 0x80 && c2 <= 0xFC))) //2バイト文字の範囲外
return false;
i += 2;
} else {
return false;
}
}
return true;
}
void test(const char *label, const unsigned char *data, size_t len) {
printf("[%s] ", label);
if (has_utf8_bom(data, len)) {
printf("UTF-8 (BOM付き)\n");
} else if (is_valid_utf8(data, len)) {
printf("UTF-8 (BOMなし)\n");
} else if (is_valid_sjis(data, len)) {
printf("Shift-JIS\n");
} else {
printf("不正または未知のエンコード\n");
}
}
int main() {
// 1. UTF-8 (BOM付き) (株式会社)
unsigned char utf8_bom[] = {
0xEF, 0xBB, 0xBF,
0xE6,0xA0,0xAA,0xE5,0xBC,0x8F,0xE4,0xBC,0x9A,0xE7,0xA4,0xBE
};
// 2. UTF-8 (BOMなし) (株式会社)
unsigned char utf8_nobom[] = {
0xE6,0xA0,0xAA,0xE5,0xBC,0x8F,0xE4,0xBC,0x9A,0xE7,0xA4,0xBE
};
// 3. Shift-JIS (株式会社)
unsigned char sjis_data[] = {
0x8A,0xBF,0x8E,0x96,0x89,0xEF,0x8E,0x91,0x8E,0x96
};
// 4. 壊れたデータ(UTF-8不正)
unsigned char broken[] = {
0xE6,0xA0,
0xFF
};
test("UTF-8 BOM付き", utf8_bom, sizeof(utf8_bom));
test("UTF-8 BOMなし", utf8_nobom, sizeof(utf8_nobom));
test("Shift-JIS", sjis_data, sizeof(sjis_data));
test("壊れたデータ", broken, sizeof(broken));
return 0;
}