LoginSignup
9
3

More than 1 year has passed since last update.

Golangでcsvの要素をダブルクオートでくくる

Last updated at Posted at 2020-10-25

やりたいこと

CSVファイルの一般的書式 (RFC4180 日本語訳)より

(レコード中の) 各フィールドは、それぞれダブルクォーテーションで囲んでも囲わなくてもよい

とあるので、プロジェクトによってはダブルクォーテーションで囲っておこうぜ!ってなることもあると思います。私のところもそうでした。

なのでタイトル通り**golangでcsvの各要素をダブルクォーテーションでくくろう!**というのが、今回やりたいことになります。

しかし、golangの標準csv encoderだと、必ず"にはエスケープ文字として"が付くため、"column""""column""" になってしまうんですよね。さてどうしたもんか

結論

基本はgocsvを利用します。
素直に使うとこんなencode処理、こういう出力になります。

client_id,client_name,client_age
12,John,21
13,Fred,
14,James,32
15,Danny,
コード全文
base.go
package main

import (
	"fmt"
	"bytes"

	"github.com/gocarina/gocsv"
)

type Client struct { // Our example struct, you can use "-" to ignore a field
	Id      string `csv:"client_id"`
	Name    string `csv:"client_name"`
	Age     string `csv:"client_age"`
	NotUsed string `csv:"-"`
}

//base data
func basedata() []*Client {
	clients := []*Client{}
	clients = append(clients, &Client{Id: "12", Name: "John", Age: "21"}) // Add clients
	clients = append(clients, &Client{Id: "13", Name: "Fred"})
	clients = append(clients, &Client{Id: "14", Name: "James", Age: "32"})
	clients = append(clients, &Client{Id: "15", Name: "Danny"})
	return clients
}

func main() {
	clients := basedata()
	out := bytes.Buffer{}
	err := gocsv.Marshal(&clients, &out) // Get all clients as CSV string
	if err != nil {
		panic(err)
	}
	csvContent := out.Bytes()
	fmt.Println(string(csvContent)) // Display all clients as CSV string

}

encode前のこちらの各要素にいくら"をくっつけてもMarshalで"""に変えられてしまうので、発想を変えます。
一旦encodeしたbyte列をdecodeすると要素毎にstringで取れるので、そこに"をくっつけます。

gocsvでは区切り文字や改行コードの制御ができるので、そこから取ってきます。

デフォルトでは gocsv.DefaultCSVWriter を実行して取得できる gocsv.SafeCSVWriter の値なので、,。改行コードは¥nです。

こちらの情報はgocsv.SetCSVWriterで変更されるかもしれませんので、そこは実装に合わせて変えてください。

サンプルはこちら、処理を抜粋。Gist

base.go
//Marshalの第二引数をそのままこちらに渡す
func convert(b *bytes.Buffer) ([]byte, error) {
        //retry to decode, to check ""
        reader := csv.NewReader(b)
        reader.LazyQuotes = true
        lines, err := reader.ReadAll()
        if err != nil {
                return []byte{}, err
        }

        //rewrite to add "", escape \"
        bytes := make([]byte, 0, len(b.Bytes())*2)

        //If you update writer by SetCSVWriter, please change the delimiter which you use
        delimiter := ','
        //If you update writer by SetCSVWriter, please change the crlf which you use
        for _, line := range lines {
                for i, part := range line {
                        if i != 0 {
                                bytes = append(bytes, byte(delimiter))
                        }
                        bytes = append(bytes, []byte(escape(part))...)
                }
                bytes = append(bytes, byte('\r'))
        }
        return bytes, nil
}

func escape(part string) string {
        //"XXX" => XXX
        escapeStr := strings.Replace(part, "\"", "\"\"", -1)
        return "\"" + escapeStr + "\""
}

実行結果。tagはそのままにしたい場合は、convert内のlinesループを調整して最初をスキップすればOKです

"client_id","client_name","client_age"
"12","John","21"
"13","Fred",""
"14","James","32"
"15","Danny",""
おまけ
## おまけ 結論に至るまでの経緯

golangでのcsv実装に苦しんだおっさんのサタデーナイトをお楽しみください

きっかけ: 「csvのダブルクォーテーションが扱えてません!」という話があったので、調査をした

参考記事をヒントに、エンコーダーのどこかでラップしてあげるのが手なんだろうなーと思いみていく
以下泥沼調査の流れになります

  • gocsv内でSetCSVWriterを利用して、io.Writerや encoding/csvcsv.Writerは上書きできないのだろうか?
    • io.Writerはbyte列の制御を行うので、ここに自作をかますのはもうcsvのパーサーを独自自作するのと変わらない。却下
    • encoding/csv側はinterfaceではなく内部で定義した構造体をやりとりしてるので、golangの仕様的に上書きは無理
    • →この案はダメ!
  • 逆にgocsvが使っているwriterを変えて、色々処理を実行して"追加ができないか?
    • gocsvでCSVWriterというinterfaceを定義しているぞ!使えそう!
      • いや、SetCSVWriterでwriterの生成関数を登録することは可能なんだけど、関数の引数がCSVWriterじゃなくて*SafeCSVWriter。型が合わない。。。
      • ん、でもgocsvの使い方としてはSafeCSVWriterをCSVWriterのinterface以外で使ってないぞ!CSVWriterに定義変えても動くのでは?
      • forkしてCSVWriter上書き試してみよう。お、いけるやん!
      • ダブルクォーテーションのサンプルコードも詰め込んじゃお!公式取り入れてくれたら最高だよなー、まあいいや。CSVWriterも自作したものを動かして、実際のケースでテスト書くか
      • うわ、これで設定したダブルクォーテーションまでエスケープされてるやんけ!
      • ここまでは良さそうだけど、解決まではいけないらしい
    • gocsvのdecodeは"の扱いうまいし要素単位のデータが取れるぞ!
      • そしたら上でやったダブルクォーテーションのコードをcolumn単位で取り出してあげればエスケープなしの文字が取れるな!これを使って[]byteに書き込んで再構築すればいいんじゃないか?
      • うん、できるな。文字列中の"もエスケープが抜けてるからそれを追加しよう!
      • よし!うまく行ったぜ!一休みしたらgocsvにPR出して解決だ!
      • 、、、あれ、CSVWriterいじったの無意味じゃね?

という、結果無駄に終わる公式コードのデバッグと修正を行ったサタデーナイトフィーバーでした

頑張った痕跡をPR出そうか迷ったんですが、ユースケースがゼロになったので出す価値ないなと思い保留。代わりにこの記事とGistを書き留めるに至りました

参考

CSVファイルの一般的書式 (RFC4180 日本語訳)
4.1.6 CSV形式の記述規則
golangでCSV出力時、要素をダブルクオートでくくることができない
gocsv 公式

9
3
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
9
3