Re: Goでcsvを操作するための基本的な知識
はじめに
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.ShiftJIS
や japanese.EUCJP
、japanese.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 はバイト列を変換するメソッド、Encode
や Decode
を持っています。
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