はじめに
CSVを処理するワンライナーを書くことはよくあるが、要件が複雑になるとスクリプトを書いた方が良い場面も多々ある。でもやっぱりワンライナーで済ませたい。そんな思いから今日もワンライナーを考える。
普段はsedで加工することが多いが、
- 時刻変換
を行おうとすると、sedだけでなく、awkでも難しかった。
理由は後方参照した値を組み込み関数などに引き渡す方法がないためだ。
また
- 任意のカラムに対して
という条件で処理しようとすると、awkでも難しかった。
文字列処理の得意なperlでも検討したが、rubyが短く、読みやすく記述できたので、備忘録がてら紹介する。
サンプルCSVファイルの作成
% for offset in `seq 5`; do date -v${offset}d "+%Y-%m-%d %H:%M:%S,%s,%s,%s"; done | tee 5days.csv
2018-08-01 16:19:09,1533107949,1533107949,1533107949
2018-08-02 16:19:09,1533194349,1533194349,1533194349
2018-08-03 16:19:09,1533280749,1533280749,1533280749
2018-08-04 16:19:09,1533367149,1533367149,1533367149
2018-08-05 16:19:09,1533453549,1533453549,1533453549
出来上がったワンライナー
tlist変数に変換したいカラム番号を配列形式で格納する。
例:1カラム目のみ
下に行くほど短く表現できる。
工夫なし
% cat 5days.csv | ruby -e 'tlist=[1]; while l=gets do; puts (l.split(/,/).map.with_index{|c, i|; tlist.include?(i) ? Time.at(c.to_i).strftime("%Y/%m/%d %H:%M:%S") : c}).join(","); end'
2018-08-01 16:19:09,2018/08/01 16:19:09,1533107949,1533107949
2018-08-02 16:19:09,2018/08/02 16:19:09,1533194349,1533194349
2018-08-03 16:19:09,2018/08/03 16:19:09,1533280749,1533280749
2018-08-04 16:19:09,2018/08/04 16:19:09,1533367149,1533367149
2018-08-05 16:19:09,2018/08/05 16:19:09,1533453549,1533453549
暗黙の$_変数を利用
% cat 5days.csv | ruby -e 'tlist=[1]; while gets do; puts ($_.split(/,/).map.with_index{|c, i|; tlist.include?(i) ? Time.at(c.to_i).strftime("%Y/%m/%d %H:%M:%S") : c}).join(","); end'
2018-08-01 16:19:09,2018/08/01 16:19:09,1533107949,1533107949
2018-08-02 16:19:09,2018/08/02 16:19:09,1533194349,1533194349
2018-08-03 16:19:09,2018/08/03 16:19:09,1533280749,1533280749
2018-08-04 16:19:09,2018/08/04 16:19:09,1533367149,1533367149
2018-08-05 16:19:09,2018/08/05 16:19:09,1533453549,1533453549
起動オプションフル活用
-nオプションで標準入力の読み込みを省略し、-Fと-aオプションでCSVをカラム毎に分離する。
% cat 5days.csv | ruby -F, -nae 'tlist=[1]; puts ($F.map.with_index{|c, i| tlist.include?(i) ? Time.at(c.to_i).strftime("%Y/%m/%d %H:%M:%S") : c}).join(",")'
2018-08-01 16:19:09,2018/08/01 16:19:09,1533107949,1533107949
2018-08-02 16:19:09,2018/08/02 16:19:09,1533194349,1533194349
2018-08-03 16:19:09,2018/08/03 16:19:09,1533280749,1533280749
2018-08-04 16:19:09,2018/08/04 16:19:09,1533367149,1533367149
2018-08-05 16:19:09,2018/08/05 16:19:09,1533453549,1533453549
例:1、3カラム目
% cat 5days.csv | ruby -F, -nae 'tlist=[1,3]; puts ($F.map.with_index{|c, i| tlist.include?(i) ? Time.at(c.to_i).strftime("%Y/%m/%d %H:%M:%S") : c}).join(",")'
2018-08-01 16:19:09,2018/08/01 16:19:09,1533107949,2018/08/01 16:19:09
2018-08-02 16:19:09,2018/08/02 16:19:09,1533194349,2018/08/02 16:19:09
2018-08-03 16:19:09,2018/08/03 16:19:09,1533280749,2018/08/03 16:19:09
2018-08-04 16:19:09,2018/08/04 16:19:09,1533367149,2018/08/04 16:19:09
2018-08-05 16:19:09,2018/08/05 16:19:09,1533453549,2018/08/05 16:19:09
おまけ
もし10桁の数字を一律エポック秒として扱って良い場合は置換だけで十分。-pオプションにより、レコード毎の処理の最後にputs $_
を補完する。
% cat 5days.csv | ruby -pe '$_.gsub!(/([0-9]{10})/){ Time.at($1.to_i).strftime("%Y/%m/%d %H:%M:%S")}'
2018-08-01 16:19:09,2018/08/01 16:19:09,2018/08/01 16:19:09,2018/08/01 16:19:09
2018-08-02 16:19:09,2018/08/02 16:19:09,2018/08/02 16:19:09,2018/08/02 16:19:09
2018-08-03 16:19:09,2018/08/03 16:19:09,2018/08/03 16:19:09,2018/08/03 16:19:09
2018-08-04 16:19:09,2018/08/04 16:19:09,2018/08/04 16:19:09,2018/08/04 16:19:09
2018-08-05 16:19:09,2018/08/05 16:19:09,2018/08/05 16:19:09,2018/08/05 16:19:09
ちなみにperlでやるとこうなった。
% cat 5days.csv | perl -e 'while(<>){ foreach $t (/([0-9]{10})/g){ ($s,$m,$h,$day,$mon,$year,$wd,$yd,$i) = localtime($t); $dt = sprintf("%04d/%02d/%02d %02d:%02d:%02d",$year+1900, $mon+1, $day, $h, $m, $s); s/$t/$dt/}; print}'
2018-08-01 16:19:09,2018/08/01 16:19:09,2018/08/01 16:19:09,2018/08/01 16:19:09
2018-08-02 16:19:09,2018/08/02 16:19:09,2018/08/02 16:19:09,2018/08/02 16:19:09
2018-08-03 16:19:09,2018/08/03 16:19:09,2018/08/03 16:19:09,2018/08/03 16:19:09
2018-08-04 16:19:09,2018/08/04 16:19:09,2018/08/04 16:19:09,2018/08/04 16:19:09
2018-08-05 16:19:09,2018/08/05 16:19:09,2018/08/05 16:19:09,2018/08/05 16:19:09
時刻情報を扱わないような変換だと、省略記法が駆使できるperlが優位な場面もあるけど、今回はrubyに軍配が上がる形になった。
おまけ2
ヘッダがあるファイルを想定すると、文字列以外の場合のみ変換するという条件もつけたいところ。
% cat 5days.csv | ruby -F, -nae 'tlist=[1,3]; puts ($F.map.with_index{|c, i| (Integer(c) rescue false)&&tlist.include?(i) ? Time.at(c.to_i).strftime("%Y/%m/%d %H:%M:%S") : c}).join(",")'
大分読みにくい!