カテゴリー
[Awk & Jq活用講座] CSVファイルで読み出した文字列を複雑なルールで置換したい
※ 当ページには【広告/PR】を含む場合があります。
2021/03/19
シェルコマンドで単純に文字列置換を行うとなると、もっとも簡単なのは文字リテラルを使ったり、sedコマンドを利用したりします。
一般的にCSV形式の文字列置換を行う場合には、行数・列数の情報が必要になってくるケースもあるので、最初からawkかjqを使う方が色々と応用が効きます。
なお、AwkかJqかどちらを使っても良いのですが、AwkではShell標準のコマンドでそのまま使えますが、Jqはサードパーティ製のプログラムですので利用するには別途インストールが必要です。
基礎テク〜CSVをファイルから/ヒアドキュメントから読み出す
ここではExcelシートをcsv形式でテキストデータで保存したファイルをシェルコマンドで処理するような作業を想定していますが、シェルコマンドでファイルを読み込むためのもっとも単純な方法は
cat
$ cat hoge.csv
#👇hoge.csvファイルの中身
1,2,3,4,5
6,7,8,9,10
わざわざスクリプトのテスト用のcsvファイルを生成することの面倒な方は、ヒアドキュメントの使ってあたかもファイルがあるかのようにデータを扱えることが可能です。
$ cat << EOF
1,2,3,4,5
6,7,8,9,10
EOF
#👇csvデータとして取り扱える
1,2,3,4,5
6,7,8,9,10
本記事中では、判例として紹介するcsvデータをヒアドキュメントで生成することを多用していますが、実際使う場合には何か実体のあるcsvファイルが存在していて、それをcatしているものとみなしてください。
ついでに言いますと、ヒアドキュメントを利用したデータの引き出しにも2パターンありまして、以下のパターンは変数
CSV_DATA
echo
$ CSV_DATA=$(cat << EOF
1,2,3,4
5,6,7,8
EOF
)
#👇改行を含む文字列のechoでは変数をダブルクオーテーション(")で囲う必要がある
$ echo "$CSV_DATA" | awk -F"," '{ print $0; }'
1,2,3,4
5,6,7,8
データを前方から読み込んでいるので、パイプライン以降の処理が見やすくなります。
もう一つは以下のように処理のコマンドにヒアドキュメントを直接入力する方法です。
$ awk -F"," '{ print $0; }' << EOF
1,2,3,4
5,6,7,8
EOF
こちらはスクリプトの行数が少なくスッキリと書けるのですが、データを入力している位置が後方になるので、どんなデータを食わせているのかイメージしづらい場合があります。
どちらのテクニックも使いどころによっては便利ですので、覚えておくと良いと思います。
Awkによる置換の基礎
まずは
Awk
ちなみにAwkにはいくつか種類があり、文法・用法が違います。 基本的に本記事ではgawk(GNU awk)を使うように統一していますのでご了承ください。
Awkで文字列を置換する場合には、
gsub
用法:
gsub(<置換前の文字パターン>,<置換後の文字パターン>,<対象文字列>)
文字列の置き換えには他にも最初に出現した一回目の文字だけを置き換える
sub
では以下でgsubによる簡単な置き換えを試してみます。
$ awk -F"," '
BEGIN { OFS="," }
{
#一列目の文字($1)を / から - に置き換え
gsub("/", "-", $1);
print $1, $2, $3;
}' << EOF
2021/04/06,出張,山田
2021/04/08,会議,佐藤
2021/04/17,リモートワーク,鈴木
EOF
#👇出力
2021-04-06,出張,山田
2021-04-08,会議,佐藤
2021-04-17,リモートワーク,鈴木
gsubでは正規表現も文字パターンに利用できます。
$ awk -F"," '
BEGIN { OFS="," }
{
gsub(/[0-9]{4}\/[0-9]{2}\/[0-9]{2}/, "----/--/--(未定)", $1);
print $1, $2, $3;
}' << EOF
2021/04/06,出張,山田
2021/04/08,会議,佐藤
2021/04/17,リモートワーク,鈴木
EOF
#👇出力
----/--/--(未定),出張,山田
----/--/--(未定),会議,佐藤
----/--/--(未定),リモートワーク,鈴木
さらにパターンマッチングを含む複雑な正規表現ルールでの置き換えになる場合、最初からn個目まで引っかかった箇所を指定して置き換えできる
gensub
用法:
gensub(<置換前の文字パターン>,<置換後の文字パターン>,<n番目 or "g"で全て置き換え>,<対象文字列>)
以下は正規表現の部分でグループ
(...)
\\1
\\2
$ awk -F"," '
BEGIN { OFS="," }
{
date_ = gensub(/[0-9]{4}\/([0-9]{2})\/([0-9]{2})/, "\\1月\\2日", "g", $1);
print date_, $2, $3;
}' << EOF
2021/04/06,出張,山田
2021/04/08,会議,佐藤
2021/04/17,リモートワーク,鈴木
EOF
#👇出力
04月06日,出張,山田
04月08日,会議,佐藤
04月17日,リモートワーク,鈴木
gensubを取り扱う際の注意のポイントとしては、gsub関数は対象文字列(第3引数)を直接置き換えるような(破壊的)操作でしたが、gensub関数は対象文字列(第4引数)の値は変化させず(非破壊的)に、関数の返り値で置き換え後の結果を返すように作用します。
Jqによる置換の基礎
特にcurlとの組合せで相性が良く、サーバーからのレスポンスなどをそのままデータ抽出・再成形などが容易に行なえますのでクラウドサービスなどからcsvデータを収集して、処理を行うときにはAwkよりも便利な場合があります。
最初にcsvをjson化してみます。
$ jq -sR '
#👇mapを使うために配列化のために([...])でラップする
[
#👇改行位置で配列に変換し、中身を取り出す
split("\n")[]
#👇空文字を弾く
| select(length > 0)
#👇コンマ切りで配列に変換する
| split(",")
]
#👇mapで配列の中身をjson要素に変換する
| map({"日付": .[0], "予定": .[1], "担当": .[2]})
' << EOF
2021/04/06,出張,山田
2021/04/08,会議,佐藤
2021/04/17,リモートワーク,鈴木
EOF
#👇出力
[
{
"日付": "2021/04/06",
"予定": "出張",
"担当": "山田"
},
{
"日付": "2021/04/08",
"予定": "会議",
"担当": "佐藤"
},
{
"日付": "2021/04/17",
"予定": "リモートワーク",
"担当": "鈴木"
}
]
jqコマンドのオプションで
-R
生のcsvからjsonに仕訳るには、とりわけ
-s
さて、jqでの文字の置き換えは
$ jq -sR '
[
split("\n")[]
| select(length > 0)
| split(",")
]
| map(.[0] |= gsub("/"; "-"))
| map({"日付": .[0], "予定": .[1], "担当": .[2]})
' << EOF
2021/04/06,出張,山田
2021/04/08,会議,佐藤
2021/04/17,リモートワーク,鈴木
EOF
#👇出力
[
{
"日付": "2021-04-06",
"予定": "出張",
"担当": "山田"
},
{
"日付": "2021-04-08",
"予定": "会議",
"担当": "佐藤"
},
{
"日付": "2021-04-17",
"予定": "リモートワーク",
"担当": "鈴木"
}
]
もし複雑な置き換えルールが必要ならば、
$ jq -sR '
[
split("\n")[]
| select(length > 0)
| split(",")
]
| map(.[0] |= (capture("[0-9]{4}/(?<mon>[0-9]{2})/(?<day>[0-9]{2})") as $capt | $capt.mon + "月" + $capt.day + "日"))
| map({"日付": .[0], "予定": .[1], "担当": .[2]})
' << EOF
2021/04/06,出張,山田
2021/04/08,会議,佐藤
2021/04/17,リモートワーク,鈴木
EOF
#👇出力
[
{
"日付": "04月06日",
"予定": "出張",
"担当": "山田"
},
{
"日付": "04月08日",
"予定": "会議",
"担当": "佐藤"
},
{
"日付": "04月17日",
"予定": "リモートワーク",
"担当": "鈴木"
}
]
中身を変形したら再びcsv形式の出力に戻します。
jsonからcsvにシリアライズする場合には、csvとして保存したい要素で二次元配列化してから
@csv
例えば先程の得られたJSON形式の結果をつかった例でいうと、
$ CONVERTED_JSON=$(cat << EOF
[
{
"日付": "04月06日",
"予定": "出張",
"担当": "山田"
},
{
"日付": "04月08日",
"予定": "会議",
"担当": "佐藤"
},
{
"日付": "04月17日",
"予定": "リモートワーク",
"担当": "鈴木"
}
]
EOF
)
$ echo "$CONVERTED_JSON" | jq -r '.[] | [.["日付"], .["予定"], .["担当"]] | @csv' | sed 's/"//g'
#👇出力
04月06日,出張,山田
04月08日,会議,佐藤
04月17日,リモートワーク,鈴木
のような感じです。
なお最終的に出力される文字列についてくるダブルクオーテーションが外れないので、sedで
"
応用 〜 置換ルールをCSVに登録して、文字列を一括置換する
置き換えルールとして、以下のように1列目に置換前、2列目に置換後の変換をすることにします。
1,壱
2,弐
3,参
4,肆
5,伍
6,陸
7,漆
8,捌
9,玖
そして、以下のようなcsvの一列目だけをこのルールで置換したい、という課題があるとします。
1-4-3,4
7-5-2,3
9-2-3,6
7-4-1,9
1-4-7,8
awkの場合
では、awkでこの置換をするなら以下のようになるでしょう。
$ cat << EOF > replace_rule.csv
1,壱
2,弐
3,参
4,肆
5,伍
6,陸
7,漆
8,捌
9,玖
EOF
$ cat << EOF > example.csv
1-4-3,4
7-5-2,3
9-2-3,6
7-4-1,9
1-4-7,8
EOF
$ awk -F "," '
BEGIN {
OFS=",";
count=0;
}
F == 0 {
before_arr[count] = $1;
after_arr[count] = $2;
count++;
next;
}
{
for (i = 0; i < count; i++ ) {
gsub(before_arr[i], after_arr[i], $1);
}
print $1, $2;
}
' F=0 replace_rule.csv F=1 example.csv
#👇置換した結果
壱-肆-参,4
漆-伍-弐,3
玖-弐-参,6
漆-肆-壱,9
壱-肆-漆,8
このテクニックでは以前の記事で解説した
ポイントとしては、awkの読み出すファイルに
F
jqの場合
jqではイタレーションとしてforeachやwhileなどが使えるのですが、基本的にワインライナーで簡潔に書けることが魅力のjqにおいてはforeachやwhileで複雑なループを行うと何ともコードが見辛くなってしまいます。
ですので、ここは
for...in
csv_data=$(jq -sR '
[ split("\n")[] | select(length > 0) | split(",") ]
' << EOF
1-4-3,4
7-5-2,3
9-2-3,6
7-4-1,9
1-4-7,8
EOF
)
replacing_map=$(jq -sR '
[ split("\n")[] | select(length > 0) | split(",") ]
| map({"before": .[0], "after": .[1]})
' << EOF
1,壱
2,弐
3,参
4,肆
5,伍
6,陸
7,漆
8,捌
9,玖
EOF
)
for item in $(echo "$replacing_map" | jq -c .[]); do
csv_data=$(echo "$csv_data" | jq -s --argjson item "$item" '
(.[][][0] |= gsub($item.before; $item.after))[]
')
done
echo "$csv_data" | jq -r '.[] | @csv' | sed 's/"//g'
#👇置換した結果
壱-肆-参,4
漆-伍-弐,3
玖-弐-参,6
漆-肆-壱,9
壱-肆-漆,8
ここでのポイントはjqでの配列の使いこなしです。 特に、
(配列)[]
[[1],[2],[3]] > [1] [2] [3]
また、
--argjson
$item
まとめ
以上、AwkとJqでのcsvデータの文字列のアレコレの細かなテクニックを具体例をもって見ていきました。
ローカルに保存したcsvファイルならばAwkの方を使う方が便利ですし、curlなどでサーバーからREST方式でcsvデータを取得するのであればJqの方が効率の良いデータハンドリングができます。
参考サイト
以下はヘッダ名を最初の一行目に持つcsvファイルからfrom_entriesメソッドを使ってjsonへ変換する場合の方法です。
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー