やりたいこと
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,
コード全文
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
で変更されるかもしれませんので、そこは実装に合わせて変えてください。
//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/csv
のcsv.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も自作したものを動かして、実際のケースでテスト書くか
- うわ、これで設定したダブルクォーテーションまでエスケープされてるやんけ!
- ここまでは良さそうだけど、解決まではいけないらしい
- いや、SetCSVWriterでwriterの生成関数を登録することは可能なんだけど、関数の引数が
- gocsvのdecodeは"の扱いうまいし要素単位のデータが取れるぞ!
- そしたら上でやったダブルクォーテーションのコードをcolumn単位で取り出してあげればエスケープなしの文字が取れるな!これを使って
[]byte
に書き込んで再構築すればいいんじゃないか? - うん、できるな。文字列中の"もエスケープが抜けてるからそれを追加しよう!
- よし!うまく行ったぜ!一休みしたらgocsvにPR出して解決だ!
- 、、、あれ、
CSVWriter
いじったの無意味じゃね?
- そしたら上でやったダブルクォーテーションのコードをcolumn単位で取り出してあげればエスケープなしの文字が取れるな!これを使って
- gocsvで
という、結果無駄に終わる公式コードのデバッグと修正を行ったサタデーナイトフィーバーでした
頑張った痕跡をPR出そうか迷ったんですが、ユースケースがゼロになったので出す価値ないなと思い保留。代わりにこの記事とGistを書き留めるに至りました
参考
CSVファイルの一般的書式 (RFC4180 日本語訳)
4.1.6 CSV形式の記述規則
golangでCSV出力時、要素をダブルクオートでくくることができない
gocsv 公式