はじめに
こんにちは、TIG真野です。シェルスクリプト連載の2日目です。
シェルスクリプトなのにAWKってちょっと違うんじゃない? って思われる方も多いと思いますが、この連載におけるレギュレーションではsed, AWKもOKという優しいルール故、見逃しください。
この記事ではCSVデータをAWKで処理する例をいくつか紹介します。
awkとは
AWKとはやカンマなどで区切られたテキストデータの処理が得意なスクリプト言語です。awkコマンドはよくawsコマンドとtypoしますが最後はkです。こちらの方が25年くらい歴史があります。
由来はアルフレッド・エイホ、ピーター・ワインバーガー、ブライアン・カーニハンの3人の苗字の頭文で、日本だとオークと呼びます。なんというか由来がレジェンドですね。個人的にはカーニハン先生はプログラミング作法で学んだ過去から、先生の敬称必須です。
awkの基本的な構文はパターンとアクションです。pattern { action }
といった形で、patternが条件、actionが加工処理や表示項目の選択といった指定を行います。
バージョン
本記事ではGNU Awkの以下のバージョンで動作させました。
$ awk -V |
使い方例
では早速AWKでCSVデータを処理していきましょう。
処理対象はフューチャー技術ブログから生成したCSVをサンプルに用います。
title,categories,tags,date,char_count |
項目の抽出
最初にCSVの1つ目と2つ目の項目を取得します。
$ awk -F',' '{print $1,$2}' posts.csv | head -n 3 |
-F
で区切り文字を指定、’{print $1,$2}’ の$1, $2は列番号の指定です。出力時の区切り文字ですが、何も指定しない場合は半角スペースで出力されます。出力後の区切り文字を指定したい場合は、OFS(Output Record Separator)というawk組み込みの変数で指定します。試しに出力区切りを<------->
にします。半角スペースから変わることが確認できます。<------->
を,
にすれば出力もCSVにできます。
$ awk -F',' 'OFS="<------->" {print $1,$2}' posts.csv | head -n 3 |
出力項目に $0
を指定した場合は全項目をの指定となります。
$ awk -F',' '{print $0}' posts.csv | head -n 3 |
項目の抽出ルールは大体抑えられたと思います。
最初の行(CSVヘッダ行)を排除
CSVを扱うと、最初のヘッダ行を無視して処理したい場合はがあります。その場合はNR(Number of Records)変数で条件指定します。NR != 1
で1行目以外を出力する条件を追加となります。
$ awk -F',' 'NR!=1 {print $0}' posts.csv | head -n 3 |
無事CSVヘッダ行を飛ばすことができました。
ある条件の行だけ抽出
入力CSVのchar_count
は記事の文字数です。50,000 文字以上の記事数を抽出します。
この場合は $5>50000
という条件を先頭に追加します。CSVヘッダを排除する NR != 1
と &&
で組み合わせると良いでしょう。
$ awk -F',' 'NR!=1 && $5>50000 {print $0}' posts.csv |
フューチャー技術ブログで、5万文字以上の記事が3つもあったことに驚きました。
重複した行を抽出
CSVである項目が重複していないかチェックしたいとします。
こういった重複行を含んだCSVファイルを作成します。
title,category,tag,date,char_count |
awk -F',' 'seen[$1]++' duplicated.csv |
マジック感がありますが、'seen[$1]++'
で重複された行(2つ目)が出力されました。
- seenは今回定義した連想配列(Map)で、そこに重複をチェックしたい項目を指定します。awkでは変数を初期化する必要が無いです
- 重複検査対象を、行全体とするのであれば
$0
です。タイトル項目をチェックするのであれば$1
を指定します ++
はインクリメント演算子で、実行する度に+1
されます。操作は変数アクセスされた後に行われます- awkでは、ゼロ以外の数値または空でない文字列値はtrueなので、2回目以降に登場した場合にのみtrueになり、重複行が出力されます。
{print $0}
部分はまるごと省略も可能。awk -F',' 'seen[$1]++ {print $0}' duplicated.csv
と同義
逆に重複行を排除(2つ目を削除)したい場合は、 !
演算子を追加します。
awk -F',' '!seen[$1]++' duplicated.csv |
sort
と uniq
コマンドでも同様の操作は可能ですが、CSVのある項目細かに、条件を組み合わせてを指定する場合はawkも有効だと思います(もちろん、ファイルをまず sort
しなくても済むというメリットもあります)
複数のCSVファイルを1ファイル結合する
ファイルを単純に結合するのであれば、cat(もとの意味は連結するを意味するconcatenate)を利用することが多いと思いますが、各ファイルにCSVヘッダーがあると、2ファイル名以降のヘッダ行を削除する必要があり厄介です。
$ cat divide1.csv |
これもawkであればシンプルに処理できます。
$ awk 'NR==1 || FNR!=1' divide*.csv |
こちらも一見マジック感がありますが、内実は簡単です。 NR
は Number of Recordsのことで、全体を通しての連番です。FNRはFile単位の連番です。NR==1
が全体を通して1行目であるCSVヘッダ行のこと、FNR=!1
が 各ファイルの1行目であるCSVヘッダ以外 であることを示します。NR==1 || FNR!=1
のOR条件で、最初のCSVヘッダ1行目であるか、各ファイルの1行目でないの場合に出力するという条件になります。
複数ファイルをawkで扱うと、FNR
は割と便利なので存在を覚えておくと便利かもしれません。
空行を除く
grep -v '^$'
で瞬殺な気がしますが、awkでも空行を除外したい場合があります。理由は他の抽出処理と組み合わせる時に必要になることもあるためです。
こういった空行を含むデータを用意します。
$ cat emptyline.csv |
$ awk 'NF' emptyline.csv |
'NF'
だけで空行を排除できました
NF
は今回初めて登場しましたが、Number of Fieldsの略です- 空行の場合は、フィールド数が0なこと。またawkでは0の判定結果がfalseになります。
{print $0}
は省略可能なためです
一応同じ意味のコマンドもあげておきます。
awk 'NF!=0' emptyline.csv |
CSV項目の中に区切り文字が入っている場合
CSVは項目の中に区切り文字を含んでいる(区切り文字がカンマの場合、項目中にカンマがある)場合、囲い文字(クォート)でラップするのがRFC 4180などで定義されています。awkでこういった項目中に区切り文字を含んでいる場合はどうすればよいでしょうか?
例えば、Vue.js最初の難関、「props down, event up」を初心者にわかるように解説してみた という記事は、以下のようにタイトルの中身で分割されてしまいます。
$ awk -F',' '{print $1}' posts.csv | grep Vue.js最初の |
コレを回避するには組み込み変数のFPAT(Fields PATtern)を使います。
$ awk -v FPAT='([^,]+)|(\"[^\"]+\")' '{print $1}' posts.csv | grep Vue.js最初の難 |
-v
はawkへの変数受け渡しようのオプションです。今回はFPATの変数を渡しますFPAT
はawk組み込み関数なので-v
で書き換えると、項目分割に影響を与えることができます([^,]+)|(\"[^\"]+\")
は正規表現で、[^,]+
でカンマを除くという意味。(\"[^\"]+\")
でダブルクォートで囲まれていて内部にダブルクォートを含まないという意味です。|
でOR条件になりますFPAT
を渡す場合は、-F
の区切り文字指定は不要です
これで、項目中のカンマに対応できました。ちなみに、RFC 4180の仕様では、クォートで囲まれた場合で、項目の中身にクォートがある場合、二重クォートでエスケープ(""
)するというルールがあります。この場合はうまく動きません。
$ cat quote.csv |
正規表現でガンバることも可能かもしれませんが、この場合は次で説明するcsvkitなどの活用を考えたほうが良いかもしれません。
改行コードを含む場合
先程説明した、FPATでも改行コードを含んでいる場合はうまく処理ができません。もし対応する必要がある場合はcsvkitのようなコマンドをインストールするか、各言語のCSVパーサーを利用することを推奨します。
例えばGo言語だと標準でRFC 4180に対応したパッケージを用意してくれているので簡単に対応できます。
0埋め
awkでは、フォーマット付きのprintf
が利用できます。例えば3桁までの0埋めの場合は%03d
を利用します。
$ awk -v FPAT='([^,]+)|(\"[^\"]+\")' '{printf("%03d %s\n", NR, $1)}' posts.csv | head -n 3 |
GNU AWKのフォーマット記述はこちらを参考ください。
csvqで良いのでは?
CSVファイルに対しての高度な抽出条件や、集計処理に関してはCSVファイルに対してSQLを実行できる csvq
を使うのも有効だと思います。
例えば、文字数50,000を超える記事は以下のようにSQLに慣れた人なら一瞬で理解できる構文で取得可能です。
$ csvq 'select title from posts where char_count > 50000' |
一方で、空行を含んだCSVファイルには脆弱な部分があります。
$ csvq 'select title from emptyline where char_count > 50000' |
そのため、データ分析の前処理などにawkを活用すると良い使い分けになると思います。
まとめ
いざという時に覚えておくと便利なawkのTipsでした。awscliのawsコマンドと間違えちゃう人も安心です。
- awkは簡単な記述によって強力な結果を生み出すことができる
- sedやgrepなどと合わせてawkを使えるようになると、あまり行儀が良くないCSVに対するデータクレンジングに便利
- 集計などの分析は必要に応じて
csvq
などと使い、awkは前処理に用いるなど使い分けが大事
明日は中本光さんの今さらながらfindパイセンについてまとめてみたについてです。
補足
同じようなテキスト処理であるgrepや、Git grep について懇切丁寧に説明した記事です、すごいです。