そびえ立つ n 万行の csv を Go 言語で簡単クッキング

そびえ立つ n 万行の csv を Go 言語で簡単クッキング|ANDPAD Advent Calendar 2022はじめまして、バックエンドエンジニアをしています武山 (bushiyama) です。
この記事は ANDPAD Advent Calendar 2022 の 1日目の記事です。

そんなルールはありませんが、アドベンドカレンダー 2022 初日なので初級な内容で暖めていきたいと存じます。

お題

非エンジニア職「ちょっとこの csv を集計したいんだけど、件数が多すぎなので助けて

わたし「おk

そもそも、システム上で集計できるよう諸々設計するのが最善ではあります。

しかしどんなに完璧なシステムを作っても、エンジニアには生涯で3回ぐらいは n 万行の CSV に立ち向かわなければならない日が来ます。

これは、わたしの直近の戦いの記録である。

なにはなくとも、まず要件整理

Excel などで開ければ vlookup と sum で簡単にとれるようですが、件数が多すぎて無理なよくあるやつです。

要望を整理して、以下のような工程に落とし込みました。

  1. ソースデータと keylist とデータを抽出し ※それぞれ別データソース
  2. ソースデータと keylist を関連付け
  3. 10万行超えのデータから上記 key でデータ抽出し
  4. ソースデータに紐づけた集計結果を知りたい

どう解決したか

これだけであれば、いくつも手段があると思います。

データベースに import するような量でもないので、今回は仕事でもお世話になっている Go 言語で集計プログラムをサクッと書いてみました。

https://github.com/bushiyama/gonyogonyo

プログラム解説

難しい内容は特に有りませんが、 map や struct など型の性質を利用した、基本に忠実なプログラミングになったかなと思います。

1. データ抽出

データ抽出については、それぞれ個別にそれぞれの手段で export しています。 一般的に、 export コマンドや api など手動で行う手段が用意されていると思いますので素直に利用しました。

プログラミングも可能ですが認証周りなどわりと面倒ですし、スポット対応なので割愛。 頻繁に行う場合はプログラムへ組み込んでシステム化するとよいですね。

2. ソースデータと keylist の関連付け

それぞれ別のデータソースですが、特定の情報で関連付けられる要件です。 ついでに集計用変数とします。

   // ソースデータ
    ret, err := initResult()
// ... 省略
// 集計結果の格納用変数の型(struct)を結果出力用のpropertiesを添えて
type Results struct {
    Source Source `yaml:"source"`
}
// ... 省略
func initResult() (*Results, error) {
// ... 省略
    // 事前に長さが特定できるので容量は指定します 重複は許容するので長さは指定しません。
    sourceList := make([]Source, 0, len(files))
    for _, f := range files {
// ... 省略
        sourceList = append(sourceList, c) // 正しく使えばappend()はわたしたちの味方です
    }
    // [0]指定なのは、複数sourceを同時に処理するとロジックが複雑になるので日和ったようです
    // マスタが2種類だったのでデータを分けて2回実行しました
    return &Results{Source: sourceList[0]}, nil

3. データの仮想 KVS 化

データを map 型変数へ読み込み仮想 KVS としました。 map ですので、データ要件を確認し key 重複には注意。

4. key の抽出・集計用変数へのデータ格納

ソースデータから key を抽出してlist型変数にもたせます。

5. 集計・出力

人間に理解しやすいよう、グルーピングの単位で集計して出力します。

// ... 省略
    ret. summarize()
    dir, _ := os.Getwd()
    path := filepath.Join(dir, "result.yaml")
    ret.marshal(path)
// ... 省略
func (r *Results) summarize() {
    for _, n := range r.Source.Names {
        for _, f := range n.FileSums {
            r.Source.Sum += f.Sum
            val := r.Source.Names[n.Id]
            val.Sum += f.Sum
            r.Source.Names[n.Id] = val
        }
    }
    for _, n := range r.Source.Names {
        val := r.Source.Names[n.Id]
        val.SumStr = humanize.Bytes(uint64(n.Sum))
        r.Source.Names[n.Id] = val
    }
    r.Source.SumStr = humanize.Bytes(uint64(r.Source.Sum))
}

func (r *Results) marshal(path string) error {
    b, err := yaml.Marshal(r)
    if err != nil {
        return err
    }
    f, err := os.Create(path)
    if err != nil {
        return err
    }
    if _, err := f.Write(b); err != nil {
        return err
    }
    if err := f.Close(); err != nil {
        return err
    }
    return nil
}

完走した感想

遥か以前の戦いでは、 シェルを書いてみたり、 localDB をたてて SQL でゴニョゴニョしてみたりした記憶がありましたが、 今回は過去最高のパフォーマンス体験ができました。

「今の自分は!過去最高の自分!」

などと、島本和彦の漫画にありそうなセリフを吐きたい気分になれました。

シェルも SQL も別に悪い手段ではないのですが、 なにかあった際に、 Go などのプログラミング言語ではテストプログラムを書いて検証するのが楽に出来るという利点もあります。 今回は書いていませんが 、もうちょっと重たいプログラミングだと TDD 開発すると捗りますね。

おわりに

当社アンドパッドではエンジニアを積極採用中です!
詳しくは下記リンクをチェック!

engineer.andpad.co.jp

hrmos.co

明日はわたしと同じボードチームのエンジニアの soe-j がエンジニアライフのQOLを上げるライフハックブログを公開してくれるらしいです。 メリークリスマス!