GoでCSVのファイルを読み込んで情報を保存する

この記事は [Howtelevision Advent Calendar 2022](https://qiita.com/advent-calendar/2022/howtelevision) 18日目の記事です。

17日目は [mayuko_okamoto](https://qiita.com/mayuko_okamoto) さんによる[「数字アレルギーのド文系が元エンジニアに教えを乞うてRedash使ってみた」](https://qiita.com/mayuko_okamoto/items/739292b8d36f0416d399) でした。

今月12月にハウテレビジョンに入社しました川原です。主に外資就活ドットコムの開発をしています。
弊社が運営する外資就活ドットコムではバックエンドにGoを採用しています。そこで個人的な練習としてGoを用いてCSVファイルの処理を実装してみました。

## やったこと

あらかじめダウンロードしておいたクレジットカード明細のファイルを用意し、プロジェクト直下のディレクトリで`go run`するとクレジットカードの明細からほしい情報を読み込んでDBに保存するものです。

```
% go run main.go
{"ID":"","Date":"2022/11/04","StoreName":"ｉＤ／ご利用分","Amount":"80","Month":"12"}
{"ID":"","Date":"2022/11/04","StoreName":"ｉＤ／ご利用分","Amount":"160","Month":"12"}
{"ID":"","Date":"2022/11/04","StoreName":"ＡＭＡＺＯＮ．ＣＯ．ＪＰ","Amount":"3080","Month":"12"}
{"ID":"","Date":"2022/11/08","StoreName":"ＡＭＡＺＯＮ．ＣＯ．ＪＰ","Amount":"784","Month":"12"}
{"ID":"","Date":"2022/11/10","StoreName":"ｉＤ／ご利用分","Amount":"113","Month":"12"}
...
...
```

DB で確認できる↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236992/2ced2048-4f61-3bfc-4862-645dcdef46ad.png)


## 処理について
行ったことは次の３点です。 
- CSV ファイルを読み込む
- 読み込んだ CSV ファイルのデータから変数に値を渡す
- 値を DB に書き込む

保存したい情報の構造体を定義しておきます。
今回は
- 支払い日時
- 店名
- 金額
- 支払い月

を明細のためのPaymentDetailオブジェクトのプロパティに持たせています。

```go
type PaymentDetail struct {
	ID        string
	Date      string
	StoreName string
	Amount    string
	Month     string
}
```

明細の CSV ファイルをプロジェクト内の `./data` ディレクトリ配下に配置しておき、ここからデータを取得することにします。任意の path が指定できれば何でも良さそうです。

```go
	files, _ := ioutil.ReadDir("./data")
	var filePathList []string
	for _, file := range files {
		if filepath.Ext(file.Name()) == ".csv" {
			filePathList = append(filePathList, "./data/"+file.Name())
		}
	}
```

下記のコードで CSV から一行ずつ取得した明細データは`["2022/7/11", "翌月一括", "ドコモご利用料金", "8692"]`のような形式だったので、 `key` が何番目かで判定してオブジェクトに詰めていくことにしました。
CSV ファイルから取得した2行目の配列の２番目の値が明細の月がわかるものだったのでそこで月の値を取得しています。
1行ごとに一つのオブジェクトに渡して insert しています。(結果的にやったことはこれだけなのでオブジェクトを経由する必要がなかったかもしれないです)

```go
    for _, v := range filePathList {
		f, err := os.Open(v)
		if err != nil {
			panic(err.Error())
		}

		r := csv.NewReader(transform.NewReader(f, japanese.ShiftJIS.NewDecoder()))
		r.LazyQuotes = true
		r.FieldsPerRecord = -1
		var month string

		for i := 0; ; i++ {
			record, err := r.Read()
			if err == io.EOF {
				break
			}
			if err != nil {
				panic(err.Error())
			}

			if i == 1 {
				month = record[2][4:6]
			}
			// 最初の 3 レコードは明細ではないので
			if i < 3 {
				continue
			}
			// 0: 利用日, 1: 支払い方法, 2: 店名, 3: 金額
			paymentDetail := PaymentDetail{Date: record[0], StoreName: record[2], Amount: record[3], Month: month}
			if _, err := db.Exec(
				`insert payment_details (date, store_name, amount, month) values (?, ?, ?, ?)`,
				paymentDetail.Date, paymentDetail.StoreName, paymentDetail.Amount, paymentDetail.Month,
			); err != nil {
				panic(err.Error())
			}
			defer db.Close()
			paymentDetails = append(paymentDetails, paymentDetail)
			if err := json.NewEncoder(os.Stdout).Encode(paymentDetail); err != nil {
				panic(err.Error())
			}
		}
	}
```

処理は全てmain関数内に収めてしまってますがだいたい100行くらいでした。 Go は行数が多くなるイメージでしたが簡単な処理なら Go のビルトインライブラリのみでもこれくらいに収まりました。

ちなみに現状のツールだと手動でファイルを持ってこないといけない実用面では MoneyForward とか使った方が明らかに良さそうですね。（PC 上でのみ明細の管理が完結するならありかもしれない…？）

## 最後に
ここまでありがとうございました。この記事へのご感想や誤りのご指摘などありましたらお気軽にお寄せください。
また、ハウテレビジョンでは一緒に働いてくれるエンジニアを募集しています。

https://recruit.howtelevision.co.jp/

## 参考
https://golang.hateblo.jp/entry/golang-encoding-csv
https://zenn.dev/syo_yamamoto/articles/1fb502ef862490