UTF8/UTF-16/UTF-32等のUnicode系の符号化でBOMを使用することができますが、ここでは主にUTF-8を前提としたBOM処理について説明します。
目次
ダイジェスト
- UTF-8ファイル(BOMあり/なし)に対して正常に処理できるクラス・メソッドの例を次に示します。
処理 処理対象のUTF-8ファイル BOMなし BOMあり ファイル読取り File.ReadAllText()
StreamReader()ファイル書込み File.WriteAllText()
StreamWriter()File.WriteAllText(.., Encoding)
StreamWriter(.., Encoding)バイト列の文字列化 Encoding.GetString() Encoding.GetString()
⇒ BOMを誤変換- 読み込み系のメソッドではBOMの有無に関わらずUTF-8を正常に読み込めます。
- 書込み系でメソッドでは既定でBOMを出力しませんが、引数でEncodingクラスを指定することでBOMの付与有無を制御できます。例えばEncoding.UTF8を指定するとBOMを付与します。UTF8Encodingクラスの場合、UTF8Encoding(true)はBOMを付与し、UTF8Encoding(false)の場合はBOMを付与しません。
- バイト列からの文字列生成、文字列からのバイト列生成時にEncodingクラスを使用します。この際、UTF8Encoding(true)/UTF8Encoding(false)を指定しても、BOMの付与有無は制御できません。
- バイナリ(バイト列)を処理対象とするFileStreamやFile.ReadAllBytes()等のクラス・メソッドではUTF-8といった文字符号化方式の考えはありません。そのため、BOMが含まれるバイト列を文字列化すると、BOM(‘\ufeff’という不可視の文字)を含む文字列が生成されてしまいます。これは、意図しない動作やエラーを引き起こす場合があるので、注意が必要です。
- バイト列からBOMを除外する場合、先頭の数倍とを削除することでBOMを削除できます。文字列にBOMが含まれる場合、string.TrimStart()でBOMを削除できます。具体的なサンプルはこちらで紹介しています。
- なお、C#では文字列はUTF-16で表現されます。詳細はString型のリファレンスをご覧ください。(stringはStringのエイリアスであり、string型の実体はStringになります。)
- ここで紹介している内容はWindows10 + Visual Studio 2022(.NET6)で検証しています。サンプルのコードはこちらをご覧ください。
UTF8/16/32 BOMとは
- UTF-8やUTF-16等のUnicode系のファイル(文字符号化方式)では、ファイル種類やデータの並び順(ビッグエンディアン・リトルエンディアン)を識別するためのバイト順マーク(BOM: Byte Order Mark)をファイル先頭に付与することができます。
- BOMは文字符号化方式毎に決まった特定の文字(バイト列)であり、文字符号化方式毎のBOMの値は次の通りです。
符号化方式 エンディアン区別 BOM(10進数形式) UTF-8 - 0xEF 0xBB 0xBF (239 187 191) UTF-16 ビッグエンディアン 0xFE 0xFF ( 254 255 ) リトルエンディアン 0xFF 0xFE (255 254) UTF-16BE (付加できない) UTF-16LE (付加できない) UTF-32 ビッグエンディアン 0x00 0x00 0xFE 0xFF (0 0 254 255) リトルエンディアン 0xFF 0xFE 0x00 0x00 (255 254 0 0) UTF-32BE (付加できない) UTF-32LE (付加できない) - UTF-8では文字に応じて可変長になるためエンディアンの区別はありません。そのため、UTF-8の場合は、データの並び順ではなくUTF-8でエンコードされていることを示すためにBOMが使用されます。
- UTF-16では、BOMがなく処理系の指定がない場合、ビッグエンディアンを使用するルールになっています。
- BOMを付与するかどうかは処理系に依存するが、BOMを使用しない処理系でBOMを付与したファイルを読み込むとエラーになる場合があります。
C#でのBOMの取り扱い
EncodingクラスとBOM
- 文字符号化方式に対応するEncodingクラスは次の通りです。
- Encodingのプロパティとして提供されるUTF8, Unicode等のEncodingクラスはBOM出力ありです。
- BOM有無の指定はファイル入出力メソッドの引数で指定した場合のみ効果があり、GetString() / GetBytes()には効果はありません。
文字符号化方式 エンコーディングクラス 備考 UTF-8 Encoding.UTF8 BOM出力あり UTF8Encoding(false/true) 第1引数: BOM有かを指定 UTF16LE Encoding.Unicode BOM出力あり UnicodeEncoding(false, false/true) 引数でエンディアン・BOM有無を指定(※1) UTF16BE Encoding.BigEndianUnicode BOM出力あり UnicodeEncoding(true, false/true) 引数でエンディアン・BOM有無を指定(※1) UTF32LE Encoding.UTF32 BOM出力あり UTF32Encoding(false, false/true) 引数でエンディアン・BOM有無を指定(※1) UTF32BE UTF32Encoding(true, false/true) 引数でエンディアン・BOM有無を指定(※1) ※1:第1引数: ビッグエンディアンかを指定, 第2引数: BOM有かを指定
- EncodingクラスとPreamble
- Preambleは「序文、前置き」等のようにコンテンツの先頭に置かれるもの、という意味です。Encodingクラスの場合、そのクラスが表現する文字符号化方式のBOM内容を意味しています。
- Encodingクラスでは、Preambleプロパティ、またはGetPreamble()メソッドを使用して、その文字符号化方式に対応するBOM内容を取得することができます。PreambleプロパティはReadOnlySpan
型、GetPreamble()はbyte[]型でBOM内容を返却します。 - Encodingクラスのコンストラクタで指定するBOM有無の指定で、Preambleプロパティ・GetPreamble()メソッドが返却するBOM内容が変わります。例えば、UTF8Encoding(true)の場合、GetPreamble()の返却値は”{0xef, 0xbb, 0xbf}“となります。UTF8Encoding(false)の場合、GetPreamble()の返却値は”{}“(空配列)になります。
- 文字列を前提としたファイル入出力クラス・メソッドでは、引数で前述のエンコーディングクラスを指定することで、ファイル入出力時の文字符号化方式やBOM有無を指定できます。(BOMの有無の指定は書き込み時のみ有効)
- BOMが付与されたファイル(バイト列)をEncoding.UTF8やEncoding.Uincode等を使って文字列に変換すると、BOMが意図しない不可視の文字(‘\ufeff’)に変換されてしまいます。これは、意図しない動作やエラーを引き起こす場合があるので、注意が必要です。
ファイル読み取り時
文字列を前提とするファイル読み取りクラス・メソッドの場合、ファイルのBOM有無、クラス・メソッドの引数によるBOM有無指定、に関わらず正常にファイルを読み取り可能です。
- UTF-8のBOMなしファイルを各クラス・メソッドで読み込んだ場合の結果は次の通りです。
EncodingクラスによるBOM有無の指定の影響はなく、正常に読み取りできます。1234567891011121314151617// "xyz_*.txt"は、"xyz"を指定の文字符号化方式で保存したファイル// ("xyz"はUTF-8で{0x78, 0x79, 0x7a}のバイト列)var str1 = File.ReadAllText("xyz_utf8.txt");var bytes1 = Encoding.UTF8.GetBytes(str1); // {0x78, 0x79, 0x7a}using var sr2 = new StreamReader("xyz_utf8.txt");var str2 = sr2.ReadToEnd();var bytes2 = Encoding.UTF8.GetBytes(str2); // {0x78, 0x79, 0x7a}using var sr3 = new StreamReader("xyz_utf8.txt", new UTF8Encoding(false));string str3 = sr3.ReadToEnd();var bytes3 = Encoding.UTF8.GetBytes(str3); // {0x78, 0x79, 0x7a}using var sr4 = new StreamReader("xyz_utf8.txt", new UTF8Encoding(true));string str4 = sr4.ReadToEnd();var bytes4 = Encoding.UTF8.GetBytes(str4); // {0x78, 0x79, 0x7a} - UTF-8のBOMありファイルを各クラス・メソッドで読み込んだ場合の結果は次の通りです。
こちらも同様です。1234567891011121314var str1 = File.ReadAllText("xyz_utf8_bom.txt");var bytes1 = Encoding.UTF8.GetBytes(str1); // {0x78, 0x79, 0x7a}using var sr2 = new StreamReader("xyz_utf8_bom.txt");var str2 = sr2.ReadToEnd();var bytes2 = Encoding.UTF8.GetBytes(str2); // {0x78, 0x79, 0x7a}using var sr3 = new StreamReader("xyz_utf8_bom.txt", new UTF8Encoding(false));string str3 = sr3.ReadToEnd();var bytes3 = Encoding.UTF8.GetBytes(str3); // {0x78, 0x79, 0x7a}using var sr4 = new StreamReader("xyz_utf8_bom.txt", new UTF8Encoding(true));string str4 = sr4.ReadToEnd();var bytes4 = Encoding.UTF8.GetBytes(str4); // {0x78, 0x79, 0x7a}
ファイル書き込み時
文字列を前提とするファイル書き込みの場合、既定でBOMは出力しませんが、引数で指定されたエンコーディングクラスに応じてBOM出力を制御できます。
- 文字列操作を対象としたクラス・メソッドでファイル書き込みを行う場合、既定でBOMは出力されません。1234567File.WriteAllText("output.txt", "xyz");var bytes1 = File.ReadAllBytes("output.txt"); // {0x78, 0x79, 0x7a}using var sw2 = new StreamWriter("output.txt");sw2.Write("xyz");sw2.Close();var bytes2 = File.ReadAllBytes("output.txt"); // {0x78, 0x79, 0x7a}
- BOMを付与するエンコーディングクラスを指定すると、BOMが出力されます。1234567891011121314151617181920212223File.WriteAllText("output.txt", "xyz", Encoding.UTF8);var bytes1 = File.ReadAllBytes("output.txt"); // {0xef, 0xbb, 0xbf, 0x78, 0x79, 0x7a}File.WriteAllText("output.txt", "xyz", new UTF8Encoding(true));var bytes2 = File.ReadAllBytes("output.txt"); // {0xef, 0xbb, 0xbf, 0x78, 0x79, 0x7a}File.WriteAllText("output.txt", "xyz", new UTF8Encoding(false));var bytes3 = File.ReadAllBytes("output.txt"); // {0x78, 0x79, 0x7a}using var sw4 = new StreamWriter("output.txt", false, Encoding.UTF8);sw4.Write("xyz");sw4.Close();var bytes4 = File.ReadAllBytes("output.txt"); // {0xef, 0xbb, 0xbf, 0x78, 0x79, 0x7a}using var sw5 = new StreamWriter("output.txt", false, new UTF8Encoding(true));sw5.Write("xyz");sw5.Close();var bytes5 = File.ReadAllBytes("output.txt"); // {0xef, 0xbb, 0xbf, 0x78, 0x79, 0x7a}using var sw6 = new StreamWriter("output.txt", false, new UTF8Encoding(false));sw6.Write("xyz");sw6.Close();var bytes6 = File.ReadAllBytes("output.txt"); // {0x78, 0x79, 0x7a}
バイト列・文字列変換時
- EncodingクラスのGetBytes(), GetString()を使用して、バイト列・文字列間の変換が可能です。
- BOMを含まないバイト列は、GetString()で文字列に変換できます。123var image = File.ReadAllBytes("xyz_utf8.txt");var str1 = Encoding.UTF8.GetString(image);var bytes1 = Encoding.UTF8.GetBytes(str1); // {0x78, 0x79, 0x7a}
- BOMを含むバイト列から文字列に変換すると、意図しない文字(不可視)が含まれ、処理系によってはエラーを引き起こす場合があります。123var image = File.ReadAllBytes("xyz_utf8_bom.txt");var str1 = Encoding.UTF8.GetString(image); // "\ufeffxyz" ※誤変換var bytes1 = Encoding.UTF8.GetBytes(str1); // {0xef, 0xbb, 0xbf, 0x78, 0x79, 0x7a}
- Encoding.Unicode, Encoding.UTF32等の他のエンコーディングクラスを使用した場合でも同様に意図しない文字(”\ufeff”)が含まれます。
- 前述の例では、”xyz”と”\ufeffxyz”を文字列比較すると不一致になりますが、表示上はどちらも”xyz”となるので、原因に気付きづらいバグになる可能性があります。
- Encoding.UTF8はBOM出力, UTF8EncodingはコンストラクタでBOM出力有無を選択できますが、GetBytes(), GetString()の動作に影響を与えません。123456789101112131415var bytes1 = File.ReadAllBytes("xyz_utf8.txt");var n1 = Encoding.UTF8.GetString(bytes1); // "xyz"var n2 = new UTF8Encoding(true).GetString(bytes1); // "xyz"var n3 = new UTF8Encoding(false).GetString(bytes1); // "xyz"var n4 = Encoding.UTF8.GetBytes(n1); // {0x78, 0x79, 0x7a}var n5 = new UTF8Encoding(true).GetBytes(n2); // {0x78, 0x79, 0x7a}var n6 = new UTF8Encoding(false).GetBytes(n3); // {0x78, 0x79, 0x7a}var bytes2 = File.ReadAllBytes("xyz_utf8_bom.txt");var b1 = Encoding.UTF8.GetString(bytes2); // "\ufeffxyz" ※誤変換var b2 = new UTF8Encoding(true).GetString(bytes2); // "\ufeffxyz" ※誤変換var b3 = new UTF8Encoding(false).GetString(bytes2); // "\ufeffxyz" ※誤変換var b4 = Encoding.UTF8.GetBytes(b1); // {0x78, 0x79, 0x7a}var b5 = new UTF8Encoding(true).GetBytes(b2); // {0x78, 0x79, 0x7a}var b6 = new UTF8Encoding(false).GetBytes(b3); // {0x78, 0x79, 0x7a}
BOMの削除方法
- バイト列や文字列からBOMを除外するサンプルです。
byte[], stringの拡張メソッドとして定義しており、”bytes.StripUtf8Bom()”, “str.StripUtf8Bom()”のように使用する想定です。12345678910111213141516171819public static class ExtensionExamples{public static byte[] StripUtf8Bom(this byte[]? bytes){_ = bytes ?? throw new ArgumentNullException(nameof(bytes));var bom = Encoding.UTF8.GetPreamble();return bom.Length <= bytes.Length && bom.SequenceEqual(bytes[..bom.Length])? bytes[bom.Length..] : bytes;}public static string StripUtf8Bom(this string? str){_ = str ?? throw new ArgumentNullException(nameof(str));var encoding = Encoding.UTF8;var bomChars = encoding.GetString(encoding.Preamble).ToCharArray(); // '\ufeff'return str.TrimStart(bomChars);}... - なお、このような処理は暫定的な回避策です。
バイト列の元となるデータを読み込む際にBOMを正常に処理できるStreamReader()等を使用して、このような状況を回避すべきだと思います。実装を始める前に、一度本質的な原因を見直して、対応を再検討することをお薦めします。