Go言語(golang)でcsvの読み書き Reader/Writer
go言語の encoding/csv パッケージの使い方。
CSVファイルの読み書きと、オプション、エラーハンドリングについて書いてます。
目次
- 一行ずつ読み込む(Read)
- 一度にすべて読み込む(ReadAll)
- 一行ずつ書き込む(Write)
- すべて一度に書き込む(WriteAll)
- CSVを構造体にマッピングする
- UTF-8 BOM付きで書き込む
- UTF-16 で書き込む
- Shift-JIS(ANSI) で書き込む
一行ずつ読み込む(Read)
package main import ( "encoding/csv" "fmt" "io" "log" "strings" ) func main() { s := `名前,年齢,身長,体重 Tanaka,31,190cm,97kg Suzuki,46,180cm,79kg Matsui,45,188cm,95kg ` r := csv.NewReader(strings.NewReader(s)) for { record, err := r.Read() if err == io.EOF { break } if err != nil { log.Fatal(err) } // recordは配列 fmt.Printf("%#v\n", record) // []string{"名前", "年齢", "身長", "体重"} // []string{"Tanaka", "31", "190cm", "97kg"} // []string{"Suzuki", "46", "180cm", "79kg"} // []string{"Matsui", "45", "188cm", "95kg"} } }
ファイルから読み込む
ファイルから読み込む場合は csv.NewReader
に *os.File
を渡す
package main import ( "encoding/csv" "log" "os" ) func main() { f, err := os.Open("file.csv") if err != nil { log.Fatal(err) } r := csv.NewReader(f) for { record, err := r.Read() if err == io.EOF { break } if err != nil { log.Fatal(err) } fmt.Println(record) } }
csv.Readerのオプション
- Comma: 区切り文字を変更
- Comment: コメント行の先頭になる文字を指定
- FieldsPerRecord: 各行のフィールド数を指定
- LazyQuotes: ダブルクオートを厳密にチェックするかどうか
- TrimLeadingSpace: 先頭の空白文字を無視する
- ReuseRecord: スライスの再利用
- TrailingComma: Deprecated(非推奨)
package main import ( "encoding/csv" "fmt" "io" "log" "strings" ) func main() { s := `名前;年齢;身長;体重 # コメント行 Tanaka;31;190cm;97kg # コメント行 Suzuki;46;180cm;79kg # コメント行 Matsui;45;188cm;95kg ` r := csv.NewReader(strings.NewReader(s)) r.Comma = ';' // 区切り文字を , から ; に変更 r.Comment = '#' // 先頭が # の場合はコメント行として扱う r.FieldsPerRecord = 4 // 各行のフィールド数。多くても少なくてもエラーになる r.LazyQuotes = true // true の場合、"" が値の途中に "180"cm のようになっていてもエラーにならない r.TrimLeadingSpace = true // true の場合は、先頭の空白文字を無視する r.ReuseRecord = true // true の場合は、Read で戻ってくるスライスを次回再利用する。パフォーマンスが上がる for { record, err := r.Read() if err == io.EOF { break } if err != nil { log.Fatal(err) } // recordは配列 fmt.Printf("%#v\n", record) // []string{"名前", "年齢", "身長", "体重"} // []string{"Tanaka", "31", "190cm", "97kg"} // []string{"Suzuki", "46", "180cm", "79kg"} // []string{"Matsui", "45", "188cm", "95kg"} } }
エラーハンドリング(csv.ParseError)
package main import ( "encoding/csv" "fmt" "log" "strings" ) func main() { s := `名前,年齢,身長,体重 Tanaka,31,190cm,97kg Suzuki,46,180cm,79kg Matsui,45,188cm,95kg ` r := csv.NewReader(strings.NewReader(s)) records, err := r.ReadAll() if err != nil { if e, ok := err.(*csv.ParseError); ok { n := 0 switch e.Err { case csv.ErrBareQuote: // ダブルクオート途中で使用されていて LazyQuotes を true にしていない場合のエラー // 例えば、 Tan"aka,31,190cm,97kg のように 途中に " がある場合 n = 1 case csv.ErrQuote: // 先頭がダブルクオートで始まっていて、末尾がダブルクオートになっていない場合のエラー // 例えば、 "Tanaka,31,190cm,97kg のように閉じるための " がない場合 n = 2 case csv.ErrFieldCount: // FieldsPerRecordで指定した数と異なる場合のエラー n = 3 } log.Fatal("\nエラー: ", n, "\n", e.Err, "\nStartLine:", e.StartLine, "\nLine:", e.Line, "\nColumn:", e.Column) } log.Fatal(err) } fmt.Println(records) }
読み込んだShift-JISファイルをUTF8にする
encoding/japanese
と text/transform
が必要なのでダウンロードする
$ go get golang.org/x/text/encoding/japanese golang.org/x/text/transform
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("file-sjis.csv") if err != nil { log.Fatal(err) } r := csv.NewReader(transform.NewReader(f, japanese.ShiftJIS.NewDecoder())) for { record, err := r.Read() if err == io.EOF { break } if err != nil { log.Fatal(err) } fmt.Println(record) } }
一度にすべて読み込む(ReadAll)
package main import ( "encoding/csv" "fmt" "log" "strings" ) func main() { s := `名前,年齢,身長,体重 Tanaka,31,190cm,97kg Suzuki,46,180cm,79kg Matsui,45,188cm,95kg ` r := csv.NewReader(strings.NewReader(s)) record, err := r.ReadAll() if err != nil { log.Fatal(err) } fmt.Printf("%#v\n", record) // [][]string{ // []string{"名前", "年齢", "身長", "体重"}, // []string{"Tanaka", "31", "190cm", "97kg"}, // []string{"Suzuki", "46", "180cm", "79kg"}, // []string{"Matsui", "45", "188cm", "95kg"}, // } }
一行ずつ書き込む(Write)
package main import ( "encoding/csv" "log" "os" ) func main() { records := [][]string{ []string{"名前", "年齢", "身長", "体重"}, []string{"Tanaka", "31", "190cm", "97kg"}, []string{"Suzuki", "46", "180cm", "79kg"}, []string{"Matsui", "45", "188cm", "95kg"}, } f, err := os.Create("file.csv") if err != nil { log.Fatal(err) } w := csv.NewWriter(f) // オプション w.Camma = ',' // デフォルトはカンマ区切りで出力される。変更する場合はこの rune 文字を変更する w.UseCRLF = true // 改行文字を CRLF(\r\n) にする for _, record := range records { if err := w.Write(record); err != nil { log.Fatal(err) } } w.Flush() // バッファに残っているデータをすべて書き込む if err := w.Error(); err != nil { log.Fatal(err) } }
書き込み結果
$ cat file.csv 名前,年齢,身長,体重 Tanaka,31,190cm,97kg Suzuki,46,180cm,79kg Matsui,45,188cm,95kg
すべて一度に書き込む(WriteAll)
package main import ( "encoding/csv" "log" "os" ) func main() { records := [][]string{ []string{"名前", "年齢", "身長", "体重"}, []string{"Tanaka", "31", "190cm", "97kg"}, []string{"Suzuki", "46", "180cm", "79kg"}, []string{"Matsui", "45", "188cm", "95kg"}, } f, err := os.Create("file.csv") if err != nil { log.Fatal(err) } w := csv.NewWriter(f) w.WriteAll(records) w.Flush() if err := w.Error(); err != nil { log.Fatal(err) } }
CSVを構造体にマッピングする
以下のように書いてみたけど、reflect 使うとさらに面倒そう。
なので gocsv (gocsvのgodocページ) を使えば、読み込んだCSVを構造体にマッピングできて、
その逆で構造体からCSVに変換するのも楽にできそう。
package main import ( "encoding/csv" "fmt" "io" "log" "strconv" "strings" ) type People struct { Name string Age int Height string Weight string } func main() { s := `名前,年齢,身長,体重 Tanaka,31,190cm,97kg Suzuki,46,180cm,79kg Matsui,45,188cm,95kg ` r := csv.NewReader(strings.NewReader(s)) var p []People for i := 0; ; i++ { record, err := r.Read() if err == io.EOF { break } if err != nil { log.Fatal(err) } if i == 0 { continue } var people People for i, v := range record { switch i { case 0: people.Name = v case 1: people.Age, err = strconv.Atoi(v) if err != nil { log.Fatal(err) } case 2: people.Height = v case 3: people.Weight = v } } p = append(p, people) } fmt.Println(p) }
UTF-8 BOM付きで書き込む
package main import ( "encoding/csv" "log" "os" ) func main() { records := [][]string{ []string{"名前", "年齢", "身長", "体重"}, []string{"Tanaka", "31", "190cm", "97kg"}, []string{"Suzuki", "46", "180cm", "79kg"}, []string{"Matsui", "45", "188cm", "95kg"}, } f, err := os.Create("file.csv") if err != nil { log.Fatal(err) } defer f.Close() // ファイルの先頭に EF, BB, BF を書き込む f.Write([]byte{0xEF, 0xBB, 0xBF}) w := csv.NewWriter(f) w.WriteAll(records) w.Flush() if err := w.Error(); err != nil { log.Fatal(err) } }
UTF-16 で書き込む
package main import ( "encoding/csv" "log" "os" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" ) func main() { records := [][]string{ []string{"名前", "年齢", "身長", "体重"}, []string{"Tanaka", "31", "190cm", "97kg"}, []string{"Suzuki", "46", "180cm", "79kg"}, []string{"Matsui", "45", "188cm", "95kg"}, } f, err := os.Create("file.csv") if err != nil { log.Fatal(err) } defer f.Close() // ビッグエンディアンは unicode.BigEndian // リトルエンディアンは unicode.LittleEndian // // BOM付きにする場合は unicode.UseBOM // BOMなしは unicode.IgnoreBOM // // 0xFE, 0xFF UTF-16BE (Big Endian) のBOM // 0xFF, 0xFE UTF-16LE (Little Endian) のBOM w := csv.NewWriter(transform.NewWriter(f, unicode.UTF16(unicode.BigEndian, unicode.UseBOM).NewEncoder())) w.WriteAll(records) w.Flush() if err := w.Error(); err != nil { log.Fatal(err) } }
Shift-JIS(ANSI) で書き込む
package main import ( "encoding/csv" "log" "os" "golang.org/x/text/encoding/japanese" "golang.org/x/text/transform" ) func main() { records := [][]string{ []string{"名前", "年齢", "身長", "体重"}, []string{"Tanaka", "31", "190cm", "97kg"}, []string{"Suzuki", "46", "180cm", "79kg"}, []string{"Matsui", "45", "188cm", "95kg"}, } f, err := os.Create("file.csv") if err != nil { log.Fatal(err) } defer f.Close() w = csv.NewWriter(transform.NewWriter(fp, japanese.ShiftJIS.NewEncoder())) w.WriteAll(records) w.Flush() if err := w.Error(); err != nil { log.Fatal(err) } }