10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GoでBOMを考慮したio.Readerを扱う

Last updated at Posted at 2023-07-21

GoでBOM付きのUTF-8ファイルを扱う必要があったので、その時に知ったテクニックを共有したいと思います。

具体的に言えば、Excelでファイル形式を「CSV UTF-8 (コンマ区切り) (.csv)」として保存した際のCSVファイルを扱った時の話です。
(Excelや少し前のメモ帳などでUTF-8としてファイルを保存すると、BOM付きのUTF-8ファイルになります。1

BOM(Byte Order Mark)とは

BOM(Byte Order Mark)というのは、Unicode系の符号化方式(UTF-8やUTF-16など)の場合に、どの符号化方式であるかが分かるように先頭に付ける数バイトのデータのことです。
より詳しい説明についてはWikipediaのバイト順マークのページなどに譲ります。

このBOMというのはテキストデータの一部ではないので、プログラムで処理する時には、BOMということを認識した上で適切に読み飛ばす必要があります。

また厄介なことに、BOMがついていないケース(特にUTF-8)というのもあるため、一般的にはBOMがあってもなくてもうまく読み込めるような実装を求められることも多いでしょう。

BOMを考慮してファイルを読み込む

さて、GoでBOMを考慮してファイルを読み込むにはどうしたらよいでしょうか。

以下のようにtransform.NewReaderunicode.BOMOverrideを利用すると、BOMを考慮したio.Reader(Goにおける入力ストリームのインターフェース)を簡単に得ることができます。2

// fallback := transform.Nop
fallback := unicode.UTF8.NewDecoder()
r := transform.NewReader(input, unicode.BOMOverride(fallback))

このio.Readerを使うと、次のような動作になります。

  • BOMがない場合: fallbackで指定したDecoder(この場合はUTF-8)で読み込む
    • transform.Nopを指定した場合は、何も変換されずにそのまま読み込む
  • BOMがある場合: (BOMを解釈して)UTF-8, UTF-16BE, UTF-16LEを自動認識して読み込む
    • 読み込まれた文字列はUTF-8に変換される

BOMがあってもなくても気にせずに読み込める上に、UTF-16にも対応できます。

この方式の良いところは、bufioなどを使って難しいコードを書かなくてよく、fallbackの部分も引数に直接書いてしまえば、実質一行で対応できるところです。

補足: fallbackについて

fallbackの部分には今回はunicode.UTF8.NewDecoder()を指定しましたが、何も変換処理をしないTransformerであるtransform.Nopを使ってもよいと思います。

その場合は、何も変換されずにそのままのバイト列として読み込まれるので、BOMなしのUTF-8であれば、そのままUTF-8として扱えます。
しかし、Shift_JISなどUTF-8以外のものを読み込んだ場合には、そのままShift_JISなどのバイト列として読み込まれてしまいます。

一方で、unicode.UTF8.NewDecoder()を指定した場合は、UTF-8以外のデータを読み込ませると、UTF-8に変換できない値については、文字化け時によく見かける「�」(U+FFFD)に変換されて、UTF-8として妥当な文字列として読み込まれます。

どちらも想定外のエンコーディングという点では同じですが、前者が得体の知れないバイト列として読み込まれるのに対して、後者は見た目上は文字化けしているとはいえUTF-8のデータとしては正しいバイト列として読み込まれる点が異なります。

このあとの処理で、読み込んだデータが(謎のバイナリ値ではなく)妥当な文字列であると想定してコードを書くなら、後者のほうが安全かなと個人的には思いました(どういう処理をするか、要件にもよると思います)。

完全なサンプルコード

上記のコードを利用したサンプルコードです。
処理内容に意味はありませんが、得られたio.Readerをそのまま気にせず扱えることがわかるかと思います。

package main

import (
	"encoding/csv"
	"fmt"
	"io"
	"os"

	"golang.org/x/text/encoding/unicode"
	"golang.org/x/text/transform"
)

func main() {
	// 標準入力か引数で指定されたファイルを入力とする
	input := os.Stdin
	if len(os.Args) > 1 {
		f, err := os.Open(os.Args[1])
		if err != nil {
			panic(err)
		}
		defer f.Close()
		input = f
	}

	// fallback := transform.Nop
	fallback := unicode.UTF8.NewDecoder()
	r := transform.NewReader(input, unicode.BOMOverride(fallback))

	csvr := csv.NewReader(r)
	for {
		record, err := csvr.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			panic(err)
		}

		fmt.Println(record)
	}
}

さいごに

巷には以下のような面倒な処理を自作するサンプルコードやライブラリがたくさんあるのですが、今回色々調べてみて簡単に書けることがわかったので、この記事を書きました。

巷の手法

  • bufioを使って先頭3バイトをPeekで覗く
  • BOMに一致したら、そのエンコーディングのReaderを作って返す
  • 一致しなかったら、そのままbufio.Readerを返す
  1. 最近のメモ帳はBOMなしUTF-8が標準になったようです。

  2. 共にgolang.org/x/textモジュールのものです。

10
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?