0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C言語で文字コードを判定して文字化けを理解する

Last updated at Posted at 2025-09-25

目的

テキストエディタがテキストファイルを開く際にどのようにして文字コードを識別しているのかを調べます。

平仮名を比較

以下の文字列を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
[壊れたデータ] 不正または未知のエンコード
test.c
#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;
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?