UnicodeとBOM
いまや、ごく限られたケースを除いて、新規テキストファイルは思考停止にUTF-8で保存するのがベストプラクティスとなっており、文字エンコーディングにまつわるトラブルは昔ほど日常的でなくなったように思う。
しかし、同じUTF-8でもBOMの有無が問題になることがしばしばあるので、ここにまとめておきたい。
Windowsユーザーであれば、以下のメモ帳の「文字コード」の違いが分かるようになることが、この記事の目標である。
バイトオーダー
文字エンコーディングに限らず、データのエンコーディング一般において、1つの値から複数のバイトが得られる場合、それらをどのような順序で並べるかはそれ自体がルール化されるべきことである。
例えば、1,000を16ビット符号無し整数としてエンコードすると、上位バイトが03
、下位バイトがe8
となるが、これら2バイトを、
03 e8
と、上位から並べるか、
e8 03
と、下位から並べるかは、それ自体がエンコーディングにおける選択肢の一つである。
前者の並べ方をビッグエンディアン(BE)、後者をリトルエンディアン(LE)と言う。
TCP/IPなどネットワーク分野ではBEが、ファイルシステムなどそれ以外の場面ではLEが使われることが多い。
UTFとバイトオーダー
ところで、Unicodeのエンコーディングには、UTF-8, UTF-16, UTF-32などがあり、
それぞれ「🍺(BEER MUG)」を次のように符号化する。1
エンコーディング | 符号単位の列 |
---|---|
UTF-8 |
f0 9f 8d ba
|
UTF-16 |
d83c df7a
|
UTF-32 | 0001f37a |
ここで、右列の各ブロックは 符号単位 の16進表記である。
これらのUTF系エンコーディングは、文字(コードポイント)を直接バイト列に結びつけるのではなく、符号単位という整数の列に変換するものとして規定されている。
8, 16, 32という数値が表しているのは、この符号単位1つあたりのビット数である。
すると、UTF-16やUTF-32の場合、個々の符号単位がマルチバイトであるため、前述のバイトオーダーが問題となる。
バイトオーダーの選択によって、それぞれが2つの「文字 → バイト列」変換を生み出すわけである。
対照的に、符号単位が8ビット = 1バイトであるUTF-8では、バイトオーダーは問題にならない。
符号単位そのものの並べ方は、各UTFそれ自体が規定しているということに注意してほしい。
結果として、最終的に得られるバイト列の候補は次のようになる。
エンコーディング | バイト列 |
---|---|
UTF-8 |
f0 9f 8d ba
|
UTF-16 + BE |
d8 3c df 7a
|
UTF-16 + LE |
3c d8 7a df
|
UTF-32 + BE |
00 01 f3 7a
|
UTF-32 + LE |
7a f3 01 00
|
BOM
バイトオーダーを区別するために(あるいはそもそもUTFでエンコードされていることを表すために)、一種のメタ情報として、テキストの先頭に バイトオーダーマーク(BOM) を付加することがある。
これは、コードポイントU+FEFF
で表される文字で、UTF-16, UTF-32では、次のバイト列になる。
エンコーディング | BOM(バイト列) |
---|---|
UTF-16 + BE |
fe ff
|
UTF-16 + LE |
ff fe
|
UTF-32 + BE |
00 00 fe ff
|
UTF-32 + LE |
ff fe 00 00
|
BOMの利用は必須ではないが、これがあるとアプリケーションはエンコーディングをバイトオーダーまで機械的に判別することができる。
しかし反面、バイトオーダーそのものに加えて、BOMを付けるか否かというさらなる選択肢により、エンコーディングを一層多義的にしている点は否めない。2
UTF-8 と BOM
上述の通り、各符号単位が1バイトに収まるUTF-8にバイトオーダーの概念はないのであるが、UTF-8であることを示すために、やはり先頭に「BOM」(あるいは「プリアンブル」)を付加することがある。3
この扱い、あるいはその是非は、プラットフォームによっても若干異なる。
一般的には、これはUTF-8の特徴であるASCII互換性を損なうため、忌避される傾向にある。
また、多くのUnixシェルは、BOMがあるとshebangを認識しない。
他方で、一部のMicrosoft製アプリケーションは、あえてBOM付きのUTF-8を扱おうとするようだ。4
たとえばExcelは、BOMの無いCSVをCP932(Shift JIS)でデコードしようとする。
その他、Visual Studioにテキストファイルを新規作成させるとBOMが付されていたりする。
(この辺の経緯を知っている人は教えてほしい。)
.NETでBOMを制御する
最後に、幾分具体的な例であるが、.NETアプリケーションにおいてUTF-8書き出し時のBOMを制御する方法を記す。
他のUTFについても、バイトオーダー自体の指定が加わる点を除いて同様である。
.NETでは各エンコーディングはSystem.Text.Encoding
のインスタンスとして表現される。
特にUTF-8は、その子クラスであるSystem.Text.UTF8Encoding
のインスタンスであり、このコンストラクタはbool
型のencoderShouldEmitUTF8Identifier
を受け取る。 5
これはその名の通り「エンコード時にUTF-8識別子(=BOM)を吐き出すか」を制御するフラグである。
デフォルトはfalse
であるが、既存のインスタンスであるEncoding.UTF8
ではtrue
に設定されているので、BOM無しにするには自身でインスタンスを生成する必要がある。
var usesBom = false;
var utf8WithoutBom = new UTF8Encoding(usesBom);
あとは、このインスタンスをSystem.IO.StreamWriter
なりに渡してやればよい。
using var writer = new StreamWriter("file.txt", utf8WithoutBom);
// ...
関連する注意点として、String.GetBytes
の結果は、上記のフラグにかかわらず常にBOMを含まない。6
そのため、Excelなどに配慮してBOM付きのUTF-8を書き出したい場合で、かつWriter
を介さずにバイト列を直接扱う場合、次のように自力で先頭にBOMを付加する必要がある。
var bytes = text.GetBytes(encoding);
var bytesWithBom = encoding.GetPreamble().concat(bytes).ToArray();
Encoding.GetPreamble
は、上記のフラグに応じてBOM(バイト列)ないし空配列を返す。
-
UTF-16を2単位(サロゲートペア)にしたいので、日常的な文字を網羅している基本多言語面(BMP)ではなく、あえて追加多言語面から持ってきた。 ↩
-
「UTF-16 BE」、「UTF-16 LE」のように言ったときに、特にBOMを伴わないUTF-16 + BE/LEの変換を指すことがある。(たとえばWikipedia)
またその場合、BOMを伴うものは、BEでもLEでも単に「UTF-16」という。前者においてバイトオーダーはエンコーディングというルールの一部であり、後者ではデータの一部(ルールは1つ)であると考えれば、理解できる用語法ではある。
もちろん、その逆の意味で使われることもあるし、「UTF-16 BE with BOM」のように明示することも多い。
冒頭のメモ帳では「UTF-16 BE」、「UTF-16 LE」はBOM付である。 ↩ -
この場合、バイト列としては
EF
BB
BF
となる。
他のケース同様、U+FEFF
をUTF-8に従って変換した結果である。 ↩ -
そういったアプリケーションでは、よく「署名付きUTF-8」(UTF-8 with signature)という呼び名を使う。
こう言うと何かセキュリティ絡みのようにも聞こえるが、単なるプリアンブルにすぎない。 ↩ -
https://learn.microsoft.com/en-us/dotnet/api/system.text.utf8encoding
リファレンスでは第2引数の
throwOnInvalidBytes
を明示的にtrue
にすることが推奨されているが、本題ではないので省略。 ↩ -
この動作自体は、得られたバイト列をさらに連結するような用途を考えれば理解できる。
BOMはあくまでファイルの先頭に一度だけ付加するものである。 ↩