CSVからCSVを作るためのシェル芸のいくらかについてメモ
列の順番を入れ替えたい
awkを使えばよい。
aaa,ddd,fff,bbb,ccc,eee
のような行があったとき、
aaa,bbb,ccc,ddd,eee,fff
のように列を入れ替えたいとすると
$ echo 'aaa,ddd,fff,bbb,ccc,eee' | awk -F ',' '{ print $1 "," $4 "," $5 "," $2 "," $6 "," $3 }' aaa,bbb,ccc,ddd,eee,fff
CSVにダブルクオーテーションがついている項目と付いていない項目が混じっている
aaa,"bbb",ccc
上の行をすべて、""がついているようにしたいとする。扱う列はまあなんとか手で処理できる個数だとすると
$ echo 'aaa,"bbb",ccc' | gawk -v FPAT='([^,]+)' '{print "\"" $1 "\"," "\""$2"\"," "\""$3"\""}' | sed -e 's/""/"/g' "aaa","bbb","ccc"
gawkの -v FPAT pattarn
で1列分の要素として扱われるパターンを指定できるので、これで ,
以外の文字列を指定。
余分についた "
はsedで簡単に取り除けるので、各列に "
を追加する。
こんな感じ。念のため、もともと "bbb"
の列の中に ""
のような文字列がないか、
$ echo 'aaa,"bbb",ccc' | cut -d ',' -f 2 | grep '""'
のようにして探しておくとよい(なお、上はa列にはややこしい文字列が入ってこないことを仮定している)。
CSVの列の中に,が含まれている項目がある
これもgawkの -v FPAT pattarn
が使える。
$ echo '"aaa","b,b,b","ccc"' | gawk -v FPAT='(\"[^\"]+\")' '{ print $2 " " $3 }' "b,b,b" "ccc"
上のパターンと組み合わさった条件のCSVの場合、上のパターンと組み合わせればよい。
$ echo 'aaa,"b,b,b",ccc' | gawk -v FPAT='([^,]+)|(\"[^\"]+\")' '{print "\"" $1 "\"," "\""$2"\"," "\""$3"\""}' | sed -e 's/""/"/g' "aaa","b,b,b","ccc"
もっとややこしいパターンが入っていてうまく入れ替えられない場合
取り出したい列の近くの特徴的な列を利用して、tr + grep とかsedで頑張る。
aaa,bbb,,"cccc,ddddd","e,ff,g","State",hhh,ii
の場合、"State" の部分が何種類かの固定値であることがわかっているのであれば、
$ echo 'aaa,bbb,,"cccc,ddddd","e,ff,g","State",hhh,ii' | tr ',' '\n' | grep -n1 "State" | sed -n '3,3 p' | cut -d '-' -f 2 hhh
となる。
tr ',' "\n"
で雑にセパレータで行を分けてしまう- 行単位でわかりやすい列をgrepで前後も出力するようにして検索する
- わかりやすい列から目当ての列が何個かを踏まえて、
sed -n '開始行,終了行 p'
で抜き出す grep -n数字
の影響で取り出した列は数字-
から始まっているのでcut
を使って最初から2番目の値を目当ての値として取り出す
$ echo 'aaa,bbb,,"cccc,ddddd","e,ff,g","State",hhh,ii' | tr ',' '\n' aaa bbb "cccc ddddd" "e ff g" "State" hhh ii $ echo 'aaa,bbb,,"cccc,ddddd","e,ff,g","State",hhh,ii' | tr ',' '\n' | grep -n1 "State" 8-g" 9:"State" 10-hhh $ echo 'aaa,bbb,,"cccc,ddddd","e,ff,g","State",hhh,ii' | tr ',' '\n' | grep -n1 "State" | sed -n '3,3 p' 10-hhh $ echo 'aaa,bbb,,"cccc,ddddd","e,ff,g","State",hhh,ii' | tr ',' '\n' | grep -n1 "State" | sed -n '3,3 p' | cut -d '-' -f 2 hhh
もしある列の値を見て別の値を割り振りたい
is_current_year="false" target_date="2017/01/02" echo $target_date | grep '2018' 1>/dev/null && is_current_year="true" echo $is_current_year # false is_current_year="false" target_date="2018/02/03" echo $target_date | grep '2018' 1>/dev/null && is_current_year="true" echo $is_current_year # true
grepである列の値が特定のパターンに合致するか調べて、合致する場合はその列の値を表す変数を上書きする、みたいな感じ。
別のCSVのデータと結合したいが、別のCSVからデータを探してくる時間を少しでも短くしたい
世の中には別々のデータストアに入っているデータがそれぞれCSVでしか出力できないため、CSV同士でデータを結合しなければいけないという時がある(ないほうがよい)。
で、片方のCSVからデータを取り出すとき、必要な行を少しでも早く取り出したいとき、grepに -m
オプションをつけると必要な数だけ合致する行を見つけたらその場でreturnしてくれる。
# 社員データ.csv から誰でもいいので営業部の社員を1人だけ出力したい grep -m 1 営業部 社員データ.csv
その他留意事項
- 改行の扱いなどが環境によって異なる場合があるのでバッチ処理を行わせるサーバでコマンドの結果を改めて確認すること
- 3つ以上特殊な値があったら1つずつ値を取り出して、sedで置換していくほうが正確性はたかそう
- gawkは入っていない場合があるので、インストールすること
参考
現場からは以上です。