C#でのUTF-8 BOMの処理仕様

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-80xEF 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-8Encoding.UTF8BOM出力あり
    UTF8Encoding(false/true)第1引数: BOM有かを指定
    UTF16LEEncoding.UnicodeBOM出力あり
    UnicodeEncoding(false, false/true)引数でエンディアン・BOM有無を指定(※1)
    UTF16BEEncoding.BigEndianUnicodeBOM出力あり
    UnicodeEncoding(true, false/true)引数でエンディアン・BOM有無を指定(※1)
    UTF32LEEncoding.UTF32BOM出力あり
    UTF32Encoding(false, false/true)引数でエンディアン・BOM有無を指定(※1)
    UTF32BEUTF32Encoding(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有無の指定の影響はなく、正常に読み取りできます。
  • UTF-8のBOMありファイルを各クラス・メソッドで読み込んだ場合の結果は次の通りです。
    こちらも同様です。

ファイル書き込み時

文字列を前提とするファイル書き込みの場合、既定でBOMは出力しませんが、引数で指定されたエンコーディングクラスに応じてBOM出力を制御できます。

  • 文字列操作を対象としたクラス・メソッドでファイル書き込みを行う場合、既定でBOMは出力されません。
  • BOMを付与するエンコーディングクラスを指定すると、BOMが出力されます。

バイト列・文字列変換時

  • EncodingクラスのGetBytes(), GetString()を使用して、バイト列・文字列間の変換が可能です。
  • BOMを含まないバイト列は、GetString()で文字列に変換できます。
  • BOMを含むバイト列から文字列に変換すると、意図しない文字(不可視)が含まれ、処理系によってはエラーを引き起こす場合があります。
    • Encoding.Unicode, Encoding.UTF32等の他のエンコーディングクラスを使用した場合でも同様に意図しない文字(”\ufeff”)が含まれます。
    • 前述の例では、”xyz”と”\ufeffxyz”を文字列比較すると不一致になりますが、表示上はどちらも”xyz”となるので、原因に気付きづらいバグになる可能性があります。
  • Encoding.UTF8はBOM出力, UTF8EncodingはコンストラクタでBOM出力有無を選択できますが、GetBytes(), GetString()の動作に影響を与えません。

BOMの削除方法

  • バイト列や文字列からBOMを除外するサンプルです。
    byte[], stringの拡張メソッドとして定義しており、”bytes.StripUtf8Bom()”, “str.StripUtf8Bom()”のように使用する想定です。
  • なお、このような処理は暫定的な回避策です。
    バイト列の元となるデータを読み込む際にBOMを正常に処理できるStreamReader()等を使用して、このような状況を回避すべきだと思います。実装を始める前に、一度本質的な原因を見直して、対応を再検討することをお薦めします。