プログラミング素人のはてなブログ

プログラミングも電気回路も専門外の技術屋の末端が勉強したことや作品をアウトプットするブログ。コードに間違いなど見つけられたら、気軽にコメントください。 C#、Python3、ラズパイなど。

テキストファイルの文字コードを判別する

文字コードを正しく判別しないと文字化けする

C#でテキストファイル(.txt)からテキストを読み込むときに文字コードを正しく判別できないことによって文字化けすることがあります。
多くのテキストファイルはBOM(Byte Order Mark)という、先頭の数Byteに文字コード指定が記述されています。

この仕組みを利用してDOBONに文字コードの識別サンプルコードがあります。
dobon.net

public static System.Text.Encoding DetectEncodingFromBOM(byte[] bytes)
{
    if (bytes.Length < 2)
    {
        return null;
    }
    if ((bytes[0] == 0xfe) && (bytes[1] == 0xff))
    {
        //UTF-16 BE
        return new System.Text.UnicodeEncoding(true, true);
    }
    if ((bytes[0] == 0xff) && (bytes[1] == 0xfe))
    {
        if ((4 <= bytes.Length) &&
            (bytes[2] == 0x00) && (bytes[3] == 0x00))
        {
            //UTF-32 LE
            return new System.Text.UTF32Encoding(false, true);
        }
        //UTF-16 LE
        return new System.Text.UnicodeEncoding(false, true);
    }
    if (bytes.Length < 3)
    {
        return null;
    }
    if ((bytes[0] == 0xef) && (bytes[1] == 0xbb) && (bytes[2] == 0xbf))
    {
        //UTF-8
        return new System.Text.UTF8Encoding(true, true);
    }
    if (bytes.Length < 4)
    {
        return null;
    }
    if ((bytes[0] == 0x00) && (bytes[1] == 0x00) &&
        (bytes[2] == 0xfe) && (bytes[3] == 0xff))
    {
        //UTF-32 BE
        return new System.Text.UTF32Encoding(true, true);
    }

    return null;
}

これを用いると、代表的な文字コードは以下のように識別されます。

文字コード 識別結果
UTF-16LE
Unicode
utf-16
UTF-16BE utf-16BE
UTF-8N
Shift_JIS
EUC
null

(BE:Big Endian、LE:Littel Endian

さらにこのコードを使うと以下のように文字コードを判別して読み込むことが出来ます。

//テキストファイルを開く
byte[] bs = System.IO.File.ReadAllBytes(textBoxFilePass.Text);
System.Text.Encoding enc = DetectEncodingFromBOM(bs);

if (enc != null)
{    //文字コードを特定できた
    //行ごとの配列として、テキストファイルの中身をすべて読み込む
    textlines = System.IO.File.ReadAllLines(textBoxFilePass.Text, enc);
}
else
{
    //文字コードを特定できなかった
    //Shift_JIS・EUCはここにくるが、デフォルトはUTF-8と解釈するので読めない
    textlines = System.IO.File.ReadAllLines(textBoxFilePass.Text); // UTF-8N (Bomなし)
}

しかし、実はSystem.IO.File.ReadAllLinesに自動判別が組み込まれていて、BOMありは

textlines = System.IO.File.ReadAllLines(textBoxFilePass.Text);

これだけで読み込めます。

BOMなしテキストファイルをC#で「正しく」読む

Shift_JISWindowsではかなり一般的だと思っていましたが、少し前にWindowsのメモ帳もデフォルトがUTF-8Nになったようです。
ここで、UTF-8Nという文字コードはBOMなしのUTF-8です。
(どうやら日本語などを書き込んだときのみUTF-8Nになり、そうでないときはShift_JIS?BOMなしになる。)

あまり使われませんが、ASCIIもBOMなしです。
メモ帳などでは文字コードANSIと表記する場合がありますが、ほぼ同じと思ってよさそうです。

文字コードではANSIコードとASCIIコードという言葉があって紛らわしいことがあります。ASCIIはANSIが定める文字コードでAmerican Standard Code for Information Interchange のことです。つまりASCIIはANSIの定める規格のひとつなのです。

http://www.daido-it.ac.jp/~oishi/HT191/ht191.html

これらのBOMなしテキストファイル(UTF-8N、Shift_JIS)をC#で正しく読み込むことを考えます。
ついでにASCIIについていうと、C#ではデフォルトはUTF-8と解釈しますが、ASCIIはUTF-8と解釈しても読み込むことが出来ます。
ASCIIは雑に言えば、半角のアルファベットと数字と記号であり文字コードのうち最も基本的で、ASCIIに使われる文字は他の文字コードでそのまま使われています。
つまりあらゆる文字コードに対して下位互換であるといえ、この為どの文字コードとして解釈しても読めるのです。
https://www.k-cube.co.jp/wakaba/server/ascii_code.html

本題にもどってUTF-8NとShift_JISの区別ですが、これについては明確な識別法が見つかりませんでした。
しかし、日本語環境という前提で以下のように考えることで「だいたい」対応できそうです。

日本語の通常の文章では必ず一定割合のひらがな・カタカナが含まれます。
よって、Byte列をShift_JISとみなして読み込んだとき「ひらがな・カタカナ」と一致する割合が7%より大きければShift_JISと判定します。(閾値は精査していません)
charset.7jp.net

Shift_JISでひらがなは0x82 0x9fから0x82 0xf1、カタカナは0x83 0x40から0x83 0x96ですのでこれは以下のようになります。

private bool isShiftJis(byte[] bs)
{
    int count = 0;

    for (int i = 0; i < bs.Length - 1; i++)
    {
        if (bs[i] == 0x82 && (0x9f <= bs[i + 1] || bs[i + 1] <= 0xf1))
        {
            //S-JISのひらがな
            count += 1;
        }
        else if(bs[i] == 0x83 && (0x40 <= bs[i + 1] || bs[i + 1] <= 0x96))
        {
            //S-JISのカタカナ
            count += 1;
        }
    }
    if ((double)count / (double)bs.Length > 0.07) return true;
    else return false;
}

これをもちいて最終的に↓のようになります。

//テキストファイルを開く
byte[] bs = System.IO.File.ReadAllBytes(textBoxFilePass.Text);
System.Text.Encoding enc = DetectEncodingFromBOM(bs);

if (enc != null)
{
    textlines = System.IO.File.ReadAllLines(textBoxFilePass.Text); // BOMあり         
}
else if (isShiftJis(bs))
{
    //S-Jis
    //行ごとの配列として、テキストファイルの中身をすべて読み込む
    textlines = "".Split();
    System.IO.StreamReader sr = new System.IO.StreamReader(textBoxFilePass.Text, System.Text.Encoding.GetEncoding("shift_jis"));
    textlines = System.IO.File.ReadAllLines(textBoxFilePass.Text, System.Text.Encoding.GetEncoding("shift_jis"));
}
else
{
    //UTF-8N
    textlines = System.IO.File.ReadAllLines(textBoxFilePass.Text);
}

まとめ

このようにして「だいたい」文字コードに対応できました。
もう少し複雑になるが精度の高いUTF-8Shift_JISの判定方法もあるようですがバイナリの読み方の勉強もかねて作ってみました。

こちらの書籍を参考にしました。
こちらにはShift_JISEUCの判別方法は書かれていますが「可能性が高い」という以上のものではないと書かれています。
どちらかの文字コードにしかない文字が含まれている場合のみ確定できます。

(実はEUCは最初に少し触れたのみで無視していますが、今はあまり使われないかな?ということで。)
これで判別できないときは、メモ帳などでUTF-8(BOMあり)などを指定して保存しなおしてもらうしかないのではないかと思います。