GoでCSVファイルを処理するCLIのツール(csvt)を作りました

CSVファイルを処理するcsvtというCLIのツールをGoで作りました。
作り始める時点で用意しようと思っていた機能が揃ったので紹介します。
(2021-12-07追記) その後欲しい機能が増えて、サブコマンドが倍増したので、現在の情報で更新します。

csvt

csvtはCSVファイルを処理するCLIのツールです。
Goで実装しています。

Goの勉強のために個人で作ったものですが、今所属するプロジェクト内でも利用してもらっています。

下記のようなサブコマンドを用意しており、CSVを編集するうえでたいていのことは、これらコマンドで事足りるようになりました。

  • add 列を追加します。
    固定値や他の列の値のコピー、テンプレートを使った値の生成(複数列の値を結合したものとか)も出来ます。
  • choose 列を選択して新しいCSVファイルを作成します。
  • concat 2つのCSVファイルを結合します。
  • count レコード数をカウントします。
    列を指定して値が存在するものをカウントすることもできます。
  • exclude 他のCSVファイルに存在する行を除外します。
  • filter 条件に一致する行に絞り込みます。
    条件には正規表現や他の列との比較も書くことができます。
  • head 先頭の数行を表示します。
  • header CSVファイルのヘッダを表示します。
  • include 他のCSVファイルに存在する行に絞り込みます。
  • join 指定した列をキーとして、CSVファイルのレコードを結合します。
    EXCELのVLOOKUPに似ています。
  • remove 列を削除します。
  • rename 列名(ヘッダ)を変更します。
  • replace CSVファイルの値を正規表現を使って置換します。
  • slice 指定した範囲の行を切り出します。
  • sort 指定した列の値でソートします。
    複数の列を使ってソートすることもできます。
  • split 指定された行数毎に分割します。
  • transform CSVファイルのフォーマットを変更します。
    デリミタ、セパレータ、クォート、文字コードなどを変更することが出来ます。
  • unique 指定した列の値を利用して重複するレコードを取り除きます。

一番大変だった気に入っているコマンドはjoinです。
ExcelのVOOKUP的なことができます。

$ csvt join -1 INPUT1 -2 INPUT2 -c COLUMN -o OUTPUT
Usage:
  csvt join [flags]

Flags:
  -1, --first string     First CSV file path.
  -2, --second string    Second CSV file path.
  -c, --column string    Name of the column to use for joining.
      --column2 string   (optional) Name of the column to use for joining in the second CSV file. Specify if different from the first CSV file.
  -o, --output string    Output CSV file path.
      --usingfile        (optional) Use temporary files for joining. Use this when joining large files that will not fit in memory.
      --norecord         (optional) No error even if there is no record corresponding to sencod CSV.
  -h, --help             help for join

例えば、input1.csvとして下記内容のCSVファイルと、

UserID,Name,Age,CompanyID
1,"Taro, Yamada",10,2
2,Hanako,21,1
3,Smith,30,2
4,Jun,22,4

input2.csvとして下記内容のCSVファイルを用意して、

CompanyID,CompanyName
1,CompanyA
2,CompanyB
3,CompanyC
4,"AAA Inc"

CompanyIDの値を使って結合します。

$ csvt join -1 input1.csv -2 input2.csv -c CompanyID -o output.csv

できあがったoutput.csvは下記のような内容になります。
input1.csvを基準として、input2.csvの内容を足していくようなイメージです。

UserID,Name,Age,CompanyID,CompanyName
1,"Taro, Yamada",10,2,CompanyB
2,Hanako,21,1,CompanyA
3,Smith,30,2,CompanyB
4,Jun,22,4,AAA Inc

--usingfile というオプションを利用すると、メモリにファイル全体を載せることなく結合するので、どんな大きなファイルでも問題なく処理できます。(数GBのCSVファイルでも使用メモリは数十MB)

他のコマンドも含め、詳しい利用方法はREADMEをご参照ください。

csvt でサポートするフォーマット

csvt の特徴的なところとして、柔軟にフォーマットが指定できるというところがあります。

CSV、TSVといった良くあるフォーマットだけでなく、マイナーなフォーマットにも対応するために、フォーマットとして下記が指定できるようになっています。
全てのサブコマンド共通のフラグです。

Global Flags:
      --delim string      (optional) CSV delimiter. The default is ','
      --quote string      (optional) CSV quote. The default is '"'
      --sep string        (optional) CSV record separator. The default is CRLF.
      --allquote          (optional) Always quote CSV fields. The default is to quote only the necessary fields.
      --encoding string   (optional) CSV encoding. The default is utf-8. Supported encodings: utf-8, shift_jis, euc-jp
      --bom               (optional) CSV with BOM. When reading, the BOM will be automatically removed without this flag.

技術的なところ

Cobra によるサブコマンドの実装

Cobra を利用することで、CLIでのサブコマンドの実装が簡単にできました。

cobra add で追加される雛形だと、サブコマンド毎のテストが書きずらかったので変えています。
root側にサブコマンドに関する処理を追加しなければならないのが煩雑ですが、テスト毎にサブコマンドを生成したかったのでやもえずです。 (ほんとはもっと良い方法があるのかもしれない、、)

フラグをソートしたくなかったり、エラーメッセージの表示タイミング変えたりとか、、いろいろ調べることもありましたが、Cobraに関する情報はたくさんあったので、やり方が見つからないといったようなことはありませんでした。

Bolt を利用することで大きなmapをメモリから追い出す

joinsortコマンドでは、CSVのレコードを全部読み込んだうえで処理しなければならず、それをメモリに載せてしまうと、巨大なCSVファイルを処理するときにメモリが足りなくなりかねません。

キーバリューストアのBoltを使うことで、mapをメモリの外(ファイル)に追い出すような実装も用意し、オプションで切り替えられるようにしました。

BoltはAPIがシンプルなのもあってか、迷うことなく簡単に利用することができました。

ASCII Table Writer

headコマンドでCSVの先頭数行を表形式で表示したかったので、何か良いライブラリが無いかなと調べたところ、ASCII Table Writerというライブラリを見つけました。

全角文字にもちゃんと対応しているので、日本語で崩れるといったようなこともなく、簡単に表形式での表示が実現できました。

go-customcsv

細かいフォーマットのカスタマイズが encoding/csv だと出来ないので、別途CSVパーサを書いてそれを利用するようにしました。

他のCSVライブラリもいくつか確認しましたが、要件満たしている&良く使われてそうでこれなら安心そう、、ってのが見当たらなかったので、Goの勉強も兼ねてということで、、