フューチャー技術ブログ

CSVと親しくなるAWK術

はじめに

こんにちは、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
GNU Awk 5.0.1, API: 2.0 (GNU MPFR 4.0.2, GNU MP 6.2.0)

使い方例

では早速AWKでCSVデータを処理していきましょう。

処理対象はフューチャー技術ブログから生成したCSVをサンプルに用います。

posts.csv
title,categories,tags,date,char_count
ごあいさつ,Culture,TechBlog,2016-02-16 09:02:14,691
LT大会(前編),Culture,LT,2016-02-17 09:09:12,9914
LT大会(後編),Culture,LT|登壇資料,2016-02-18 11:31:15,13214
Startup_Geeks#1,Culture,Conference|開催レポート,2016-03-23 13:49:26,9404
ハッカソン道中記~あの日入った温泉の効果を僕はまだ知らない~,IoT,IoT|DesignThinking|Hackathon,2016-04-06 14:37:37,9050
第二回LT大会の報告,Culture,LT|登壇資料,2016-04-13 14:37:37,12247
...
  1. 項目の抽出
  2. 最初の行(CSVヘッダ行)を排除
  3. ある条件の行だけ抽出
  4. 複数のCSVファイルを1ファイル結合する
  5. 空行を除く
  6. CSV項目の中に区切り文字が入っている場合
  7. 改行コードを含む場合
  8. 0埋め

項目の抽出

最初にCSVの1つ目と2つ目の項目を取得します。

項目の抽出
$ awk -F',' '{print $1,$2}' posts.csv | head -n 3
title category
ごあいさつ Culture
LT大会(前編) Culture

-Fで区切り文字を指定、’{print $1,$2}’ の$1, $2は列番号の指定です。出力時の区切り文字ですが、何も指定しない場合は半角スペースで出力されます。出力後の区切り文字を指定したい場合は、OFS(Output Record Separator)というawk組み込みの変数で指定します。試しに出力区切りを<------->にします。半角スペースから変わることが確認できます。<------->,にすれば出力もCSVにすることができます。

OFS指定
$ awk -F',' 'OFS="<------->" {print $1,$2}' posts.csv | head -n 3
title<------->category
ごあいさつ<------->Culture
LT大会(前編)<------->Culture

出力項目に $0 を指定した場合は全項目をの指定となります。

$ awk -F',' '{print $0}' posts.csv | head -n 3
title,category,tag,date,char_count
ごあいさつ,Culture,TechBlog,2016-02-16 09:02:14,691
LT大会(前編),Culture,LT,2016-02-17 09:09:12,9914

項目の抽出ルールは大体抑えられたと思います。

最初の行(CSVヘッダ行)を排除

CSVを扱うと、最初のヘッダ行を無視して処理したい場合はがあります。その場合はNR(Number of Records)変数で条件指定します。
NR != 1 で1行目以外を出力する条件を追加となります。

$ awk -F',' 'NR!=1 {print $0}' posts.csv | head -n 3
ごあいさつ,Culture,TechBlog,2016-02-16 09:02:14,691
LT大会(前編),Culture,LT,2016-02-17 09:09:12,9914
LT大会(後編),Culture,LT|登壇資料,2016-02-18 11:31:15,13214

無事CSVヘッダ行を飛ばすことができました。

ある条件の行だけ抽出

入力CSVのchar_countは記事の文字数です。50,000 文字以上の記事数を抽出します。
この場合は $5>50000 という条件を先頭に追加します。CSVヘッダを排除する NR != 1&& で組み合わせると良いでしょう。

$ awk -F',' 'NR!=1 && $5>50000 {print $0}' posts.csv
title,category,tag,date,char_count
Amazon Redshiftの仕様を調べてみた,Infrastructure,AWS,2019/06/25 09:00:00,66162
Goを学ぶときにつまずきやすいポイントFAQ,Programming,Go|コードレビュー|入門,2019/07/13 10:00,75169
春の入門祭り 🌸 #01 Goのテストに入門してみよう!,Programming,Go|初心者向け|テスト,2020/06/01 09:41:23,56000

フューチャー技術ブログで、5万文字以上の記事が3つもあったことに驚きました。

重複した行を抽出

CSVである項目が重複していないかチェックしたいとします。

こういった重複行を含んだCSVファイルを作成します。

duplicated.csv
title,category,tag,date,char_count
ごあいさつ,Culture,TechBlog,2016-02-16 09:02:14,691
ハッカソン道中記~あの日入った温泉の効果を僕はまだ知らない~,IoT,IoT|DesignThinking|Hackathon,2016-04-06 14:37:37,9050
ハッカソン道中記~あの日入った温泉の効果を僕はまだ知らない~,Dummy,Dummy,2016-04-07 15:00:00,9050
第二回LT大会の報告,Culture,LT|登壇資料,2016-04-13 14:37:37,12247
重複した行を抽出
awk -F',' 'seen[$1]++' duplicated.csv
ハッカソン道中記~あの日入った温泉の効果を僕はまだ知らない~,Dummy,Dummy,2016-04-07 15:00:00,9050

マジック感がありますが、'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
title,category,tag,date,char_count
ごあいさつ,Culture,TechBlog,2016-02-16 09:02:14,691
ハッカソン道中記~あの日入った温泉の効果を僕はまだ知らない~,IoT,IoT|DesignThinking|Hackathon,2016-04-06 14:37:37,9050
第二回LT大会の報告,Culture,LT|登壇資料,2016-04-13 14:37:37,12247

sortuniq コマンドでも同様の操作は可能ですが、CSVのある項目細かに、条件を組み合わせてを指定する場合はawkも有効だと思います。(もちろん、ファイルをまず sortしなくても済むというメリットもあります)

複数のCSVファイルを1ファイル結合する

ファイルを単純に結合するのであれば、cat(もとの意味は連結するを意味するconcatenate)を利用することが多いと思いますが、各ファイルにCSVヘッダーがあると、2ファイル名以降のヘッダ行を削除する必要があり厄介です。

入力データ
$ cat divide1.csv
title,category,tag,date,char_count
ごあいさつ,Culture,TechBlog,2016-02-16 09:02:14,691

$ cat divide2.csv
title,category,tag,date,char_count
ハッカソン道中記~あの日入った温泉の効果を僕はまだ知らない~,IoT,IoT|DesignThinking|Hackathon,2016-04-06 14:37:37,9050

$ cat divide3.csv
title,category,tag,date,char_count
第二回LT大会の報告,Culture,LT|登壇資料,2016-04-13 14:37:37,12247

これもawkであればシンプルに処理できます。

$ awk 'NR==1 || FNR!=1' divide*.csv
title,category,tag,date,char_count
ごあいさつ,Culture,TechBlog,2016-02-16 09:02:14,691
ハッカソン道中記~あの日入った温泉の効果を僕はまだ知らない~,IoT,IoT|DesignThinking|Hackathon,2016-04-06 14:37:37,9050
第二回LT大会の報告,Culture,LT|登壇資料,2016-04-13 14:37:37,12247

こちらも一見マジック感がありますが、内実は簡単です。 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でも空行を除外したい場合があります。理由は他の抽出処理と組み合わせる時に必要になることもあるためです。

こういった空行を含むデータを用意します。

emptyline.csv
$ cat emptyline.csv
title,category,tag,date,char_count
ごあいさつ,Culture,TechBlog,2016-02-16 09:02:14,691
ハッカソン道中記~あの日入った温泉の効果を僕はまだ知らない~,IoT,IoT|DesignThinking|Hackathon,2016-04-06 14:37:37,9050

第二回LT大会の報告,Culture,LT|登壇資料,2016-04-13 14:37:37,12247
$ awk 'NF' emptyline.csv
title,category,tag,date,char_count
ごあいさつ,Culture,TechBlog,2016-02-16 09:02:14,691
ハッカソン道中記~あの日入った温泉の効果を僕はまだ知らない~,IoT,IoT|DesignThinking|Hackathon,2016-04-06 14:37:37,9050
第二回LT大会の報告,Culture,LT|登壇資料,2016-04-13 14:37:37,12247

'NF'だけで空行を排除できました

  • NFは今回初めて登場しましたが、Number of Fieldsの略です
  • 空行の場合は、フィールド数が0なこと。またawkでは0の判定結果がfalseになります。
  • {print $0} は省略可能なためです

一応同じ意味のコマンドもあげておきます。

同義なコマンド
$ awk 'NF!=0' emptyline.csv
$ awk 'NF!=0{print $0}' emptyline.csv
$ awk '$0!=""' emptyline.csv

CSV項目の中に区切り文字が入っている場合

CSVは項目の中に区切り文字を含んでいる(区切り文字がカンマの場合、項目中にカンマがある)場合、囲い文字(クォート)でラップするのがRFC 4180などで定義されています。awkでこういった項目中に区切り文字を含んでいる場合はどうすればよいでしょうか?

例えば、Vue.js最初の難関、「props down, event up」を初心者にわかるように解説してみた という記事は、以下のようにタイトルの中身で分割されてしまいます。

カンマをタイトルに持つ行
$ awk -F',' '{print $1}' posts.csv | grep Vue.js最初の
"Vue.js最初の難関、「props down

コレを回避するには組み込み変数のFPAT(Fields PATtern)を使います。

FPAT導入例
$ awk -v FPAT='([^,]+)|(\"[^\"]+\")' '{print $1}' posts.csv | grep Vue.js最初の難
"Vue.js最初の難関、「props down, event up」を初心者にわかるように解説してみた"
  • -vはawkへの変数受け渡しようのオプションです。今回はFPATの変数を渡します
  • FPAT はawk組み込み関数なので -v で書き換えると、項目分割に影響を与えることができます
  • ([^,]+)|(\"[^\"]+\")は正規表現で、 [^,]+でカンマを除くという意味。(\"[^\"]+\")でダブルクォートで囲まれていて内部にダブルクォートを含まないという意味です。 | でOR条件になります
  • FPATを渡す場合は、-F の区切り文字指定は不要です

これで、項目中のカンマに対応することができました。ちなみに、RFC4180の仕様では、クォートで囲まれた場合で、項目の中身にクォートがある場合、二重クォートでエスケープ("")するというルールがあります。この場合はうまく動きません。

FPATでもうまく動かないケース
$ cat quote.csv
title,category,tag,date,char_count
"テキスト処理 ""sed"", ""awk"" の入門",Test,Test,2021-03-28 09:02:14,691

$ awk -v FPAT='([^,]+)|(\"[^\"]+\")' '{print $1}' quote.csv
title
"テキスト処理 ""sed""

正規表現でガンバることも可能かもしれませんが、この場合は次で説明する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
001 title
002 ごあいさつ
003 LT大会前編

GNU AWKのフォーマット記述はこちらを参考ください。

csvqで良いのでは?

CSVファイルに対しての高度な抽出条件や、集計処理に関してはCSVファイルに対してSQLを実行できる csvq を使うのも有効だと思います。

https://github.com/mithrandie/csvq

例えば、文字数50,000を超える記事は以下のようにSQLに慣れた人なら一瞬で理解できる構文で取得可能です。

$ csvq 'select title from posts where char_count > 50000'
+---------------------------------------------------+
| title |
+---------------------------------------------------+
| Amazon Redshiftの仕様を調べてみた |
| Goを学ぶときにつまずきやすいポイントFAQ |
| 春の入門祭り 🌸 #01 Goのテストに入門してみよう! |
+---------------------------------------------------+

一方で、空行を含んだCSVファイルには脆弱な部分があります。

$ csvq 'select title from emptyline where char_count > 50000'
Empty RecordSet

そのため、データ分析の前処理などにawkを活用すると良い使い分けになると思います。

まとめ

いざという時に覚えておくと便利なawkのTipsでした。awscliのawsコマンドと間違えちゃう人も安心です。

  • awkは簡単な記述によって強力な結果を生み出すことができる
  • sedやgrepなどと合わせてawkを使えるようになると、あまり行儀が良くないCSVに対するデータクレンジングに便利
  • 集計などの分析は必要に応じてcsvqなどと使い、awkは前処理に用いるなど使い分けが大事

明日は中本光さんの今さらながらfindパイセンについてまとめてみたについてです。

補足

同じようなテキスト処理であるgrepや、git grep について懇切丁寧に説明した記事です、すごいです。