woshidan's blog

そんなことよりコードにダイブ。

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は入っていない場合があるので、インストールすること

参考

現場からは以上です。