📚

Re: Goでcsvを操作するための基本的な知識

2022/04/22に公開

はじめに

https://zenn.dev/syo_yamamoto/articles/1fb502ef862490

Println で標準出力してみると以下のように表示されるかと思います。(SHIFT-JIS形式なのでmacでみると文字化けしていますがひとまず置いておきます)

日本では一般的に CSV ファイルは Shift_JIS でエンコードされている事が多いです。Go 言語は内部のエンコーディングが UTF-8 なので、Shift_JIS な CSV ファイルを読み込むと文字化けします。

そこで便利なのが

エンコーディングの変換は golang.org/x/text/transform が便利です。このパッケージと、golang.org/x/text/encoding/japanese を使う事で、os.Open で開いたファイルがさも初めから UTF-8 であるかの様に扱う事ができます。

どんな風に扱うか

japanese パッケージには japanese.ShiftJISjapanese.EUCJPjapanese.ISO2022JP という変数が宣言されています。これらは encoding.Encoding というインタフェースを実装しています。

// Encoding is a character set encoding that can be transformed to and from
// UTF-8.
type Encoding interface {
	// NewDecoder returns a Decoder.
	NewDecoder() *Decoder

	// NewEncoder returns an Encoder.
	NewEncoder() *Encoder
}

Encoder や Decoder はバイト列を変換するメソッド、EncodeDecode を持っています。

func (d *Decoder) Bytes(b []byte) ([]byte, error) {
	b, _, err := transform.Bytes(d, b)
	if err != nil {
		return nil, err
	}
	return b, nil
}
func (e *Encoder) Bytes(b []byte) ([]byte, error) {
	b, _, err := transform.Bytes(e, b)
	if err != nil {
		return nil, err
	}
	return b, nil
}

このデコーダを使って、io.Reader を生成してくれるのが transform.NewReader です。具体的には以下の様にして ShiftJIS 専用の io.Reader を作れます。

transform.NewReader(f, japanese.ShiftJIS.NewDecoder())

元の記事の CSV の読み込みであれば以下の様に実装出来ます。

package main

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

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

func main() {
	f, err := os.Open("28HYOGO.CSV")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	r := csv.NewReader(transform.NewReader(f, japanese.ShiftJIS.NewDecoder()))
	for {
		records, err := r.Read()
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(records)
	}
}

簡単ですね。

僕は Go のストリーム指向が好き

僕が初めに Go に興味を持ったのは、Go がストリーム指向だからです。ファイルから全体のコンテンツをバイト列に読み込み、それを渡して CSV をパースする、そんなライブラリもたまに見ますが、メモリが無駄に使われます。Go はストリーム指向なので、io.Reader を使い効率的に入力を扱えます。

例えば上記のソースコードであれば、いくら CSV ファイルの行数が長くてもメモリ不足になる事はありません。

ここが僕が Go を好きになった理由です。encoding/json も同様に、入力が io.Reader になっている事に気付いた人もいるかもしれませんね。

なら zip も読んだらいいんじゃね?

そうです。Go には archive/zip があるので zip ファイルを展開する事なく、CSV を読み込めます。

package main

import (
	"archive/zip"
	"encoding/csv"
	"fmt"
	"log"
	"path/filepath"
	"strings"

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

func readCSV(file *zip.File) error {
	r, err := file.Open()
	if err != nil {
		return err
	}
	defer r.Close()

	cr := csv.NewReader(transform.NewReader(r, japanese.ShiftJIS.NewDecoder()))
	for {
		records, err := cr.Read()
		if err != nil {
			return err
		}
		fmt.Println(records)
	}
}

func main() {
	zf, err := zip.OpenReader("28hyogo.zip")
	if err != nil {
		log.Fatal(err)
	}
	defer zf.Close()

	for _, file := range zf.File {
		if strings.ToLower(filepath.Ext(file.Name)) != ".csv" {
			continue
		}

		err := readCSV(file)
		if err != nil {
			log.Fatal(err)
		}
	}
}

おわりに

Go がストリーム指向な点を、こんな深夜にお伝えしたくなったので書いてみました。ぜひ、メモリ効率の良い処理を実装しましょう。

Discussion