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をメモリから追い出す
join
やsort
コマンドでは、CSVのレコードを全部読み込んだうえで処理しなければならず、それをメモリに載せてしまうと、巨大なCSVファイルを処理するときにメモリが足りなくなりかねません。
キーバリューストアのBoltを使うことで、map
をメモリの外(ファイル)に追い出すような実装も用意し、オプションで切り替えられるようにしました。
BoltはAPIがシンプルなのもあってか、迷うことなく簡単に利用することができました。
ASCII Table Writer
head
コマンドでCSVの先頭数行を表形式で表示したかったので、何か良いライブラリが無いかなと調べたところ、ASCII Table Writerというライブラリを見つけました。
全角文字にもちゃんと対応しているので、日本語で崩れるといったようなこともなく、簡単に表形式での表示が実現できました。
go-customcsv
細かいフォーマットのカスタマイズが encoding/csv
だと出来ないので、別途CSVパーサを書いてそれを利用するようにしました。
他のCSVライブラリもいくつか確認しましたが、要件満たしている&良く使われてそうでこれなら安心そう、、ってのが見当たらなかったので、Goの勉強も兼ねてということで、、