文字コードを正しく判別しないと文字化けする
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_JISはWindowsではかなり一般的だと思っていましたが、少し前に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-8とShift_JISの判定方法もあるようですがバイナリの読み方の勉強もかねて作ってみました。
こちらにはShift_JISとEUCの判別方法は書かれていますが「可能性が高い」という以上のものではないと書かれています。
どちらかの文字コードにしかない文字が含まれている場合のみ確定できます。
(実はEUCは最初に少し触れたのみで無視していますが、今はあまり使われないかな?ということで。)
これで判別できないときは、メモ帳などでUTF-8(BOMあり)などを指定して保存しなおしてもらうしかないのではないかと思います。