golangの日記

Go言語を中心にプログラミングについてのブログ

Go言語(golang)でcsvの読み書き Reader/Writer

golang.png


go言語の encoding/csv パッケージの使い方。
CSVファイルの読み書きと、オプション、エラーハンドリングについて書いてます。





目次



一行ずつ読み込む(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/japanesetext/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)
    }
}