カテゴリー
【Awk & Jq活用講座】検索対象が無いときの対処方法〜エラー時の値を#N/Aに置き換える
※ 当ページには【広告/PR】を含む場合があります。
2021/04/07
検索結果が無い行を発見したときに、通常は何も表示されないで無視されることが多いですが、エラーハンドリングを定義し、エラーを発見したときの処置も実装したい場合があります。
たとえば、検索結果で一致しない行には、新しい内容を新規作成してそこに挿入する...などです。
今回はCSVデータ使う上でのAwkとJqを使ったシェルスクリプトのエラーの捌き方の基礎を行っていきます。
はじめに
当サイトではオフィス業務のComputer-Aidedなハイブリッドな方法を模索し、より効率的なExcel業務を実現したい多忙なオフィスワーカー向けの主にAwkとSedを使うシェル講座です。
シェルスクリプトはどこでもどんなOSでも基本的に使えて、しかも一度使い方を覚えると、Excelと組み合わせて最高に効率の良いオフィスワークツールが作れることでしょう。

Excelの値入力エラー時の挙動を定義する
Excelなどで関数をつかった集計計算などで、例えば数字が半角と全角を気付かずに四則演算してしまうことです。
$ awk -F"," '{print $1*$2;}' << EOF
3,6
4,19
8,3
5,22
EOF
#👇出力
0
76
24
0
例えばAwkでは通常演算が正しく行われない場合でも、エラーではなくゼロが返さえる仕組みになっています。
これだとどのセルに欠陥データがあるかは分かりにくいので、Excel風に
#N/A
Awkの場合
単純に正規表現で先程の例を修正してみると、
$ awk -F"," '{
if ($1 ~ /[^0-9.]/ || $2 ~ /[^0-9.]/) {
print "#N/A";
} else {
print $1*$2;
}
}' << EOF
3,6
4,19
8,3
5,22
4.5,9.13
526,0.0
9.1,0.0
EOF
#👇出力
#N/A
76
24
#N/A
41.085
0
#N/A
となりいい感じに数値以外の演算結果を#N/Aで出力することができます。
またgawk4.2以降の新しいバーションのAwkでは型判定の関数
typeof
$ awk -F"," '{
if ( typeof($1) != "strnum" || typeof($2) != "strnum" ) {
print "#N/A";
} else {
print $1*$2;
}
}' << EOF
3,6
4,19
8,3
5,22
4.5,9.13
526,0.0
9.1,0.0
EOF
#👇出力
#N/A
76
24
#N/A
41.085
0
#N/A
なお、Awkでの型は
array
number
regexp
string
strnum
undefined
よもやま講座 〜 条件分岐のショートハンド
Awkには従来の
if~else if~else
条件 ? 真の場合の返値 : 偽の場合の返値
$ awk -F"," '{
print ($1 ~ /[^0-9.]/ || $2 ~ /[^0-9.]/) ? "#N/A" : $1*$2;
}' << EOF
3,6
4,19
8,3
5,22
4.5,9.13
526,0.0
9.1,0.0
EOF
#👇出力
#N/A
76
24
#N/A
41.085
0
#N/A
さらに判定に真偽(1か0)を返すカスタマイズ関数を利用し、以下のようなスクリプトの例に示すように
||
&&
$ echo '1,2,3' | awk -F"," '
function func1(arg1_) {
print arg1_;
return 0;
}
function func2(arg2_) {
print arg2_;
return 0;
}
function func3(arg3_) {
print arg3_;
return 1;
}
function func4(arg4_) {
print arg4_;
return 1;
}
{
func1($1) || func2($2) || func3($3) && func4($0);
}'
#👇出力
1
2
3
1,2,3
このテクニックで先程のスクリプトを置き換えると、
$ awk -F"," '
function isStrnum(str_) {
if (str_ ~ /[^0-9.]/) {
print "#N/A";
return 1;
}
return 0;
}
function calc(arg1_, arg2_) {print arg1_ * arg2_}
{
isStrnum($1) || isStrnum($2) || calc($1, $2);
}' << EOF
3,6
4,19
8,3
5,22
4.5,9.13
526,0.0
9.1,0.0
EOF
#👇出力
#N/A
76
24
#N/A
41.085
0
#N/A
のように使えます。
複雑な判定シークエンスなどをもつプログラムがAwkで書きたい場合にはこちらのテクニックは有効です。
Jqの場合
次はJqでも上の節と同様の操作を行ってみます。
最初に断っておきますが、Jqでのストリーム処理では今回のお題としてAwkよりも難解になります。 個人的にはAwkが使えるならそちらで処理しても良い気はします。
まず
少しJqスクリプトの働きが分かりにくいかもしれないので、段階的に説明していきます。
まずJqでは読み込んだCsvデータは全てstring型として扱われるので、
tonumber
ということで、
if ~ then ~ else ~ end
$ jq -s -R '
[
split("\n")[] | select(length > 0) |
[
split(",") | .[] |
if test("[^0-9.]") then "#N/A" else tonumber end
]
]
' << EOF
4,19
3,6
EOF
#👇出力
[
[
4,
19
],
[
3,
"#N/A"
]
]
上は例のようにCsvデータを二次元配列化しているのですが、非数値のセルには
#N/A
ここまで出来ると後は同様の考え方で、セル同士の要素を演算する場合にもif構文を使って処理を分岐させることが可能になります。
$ jq -s -R '
[split("\n")[] | select(length > 0) | [ split(",") | .[] | if test("[^0-9.]") then "#N/A" else tonumber end ]] |
[
.[] | if .[0] == "#N/A" or .[1] == "#N/A" then "#N/A" else .[0] * .[1] end
]
' << EOF
3,6
4,19
8,3
5,22
4.5,9.13
526,0.0
9.1,0.0
EOF
#👇出力
[
"#N/A",
76,
24,
"#N/A",
41.085,
0,
"#N/A"
]
上のスクリプトでも、計算できなかった行では演算結果が
#N/A
$ jq -s -R '
[split("\n")[] | select(length > 0) | split(",")] |
[
.[] | if (.[0] | test("[^0-9.]")) or (.[1] | test("[^0-9.]")) then "#N/A" else (.[0] | tonumber) * (.[1] | tonumber) end
]
' << EOF
3,6
4,19
8,3
5,22
4.5,9.13
526,0.0
9.1,0.0
EOF
#👇出力
[
"#N/A",
76,
24,
"#N/A",
41.085,
0,
"#N/A"
]
Jqではこのくらいのスクリプトが落とし所かもしれません。
まとめ
以上、今回はCsvデータから集計を行う際に生じる数値演算出来ない場合のエラーをどのように出力として安全に取り扱うかを検討してきました。
プログラマーの気持ちの問題かもしれませんが、個人的にはJqよりもAwkの方がショートハンドテクニックを使えることもあり、一日の長といった感じでAwkに軍配が上がるように思いました。
なお、AwkもJqとも明確な静的型付けの言語ではないですが、かと言って動的な型付けもそこまで面倒を見てくれているような印象も受けません。 実際にはC++で実装されいるということもあり、裏ではオブジェクト型に対して使う側がデリケートに取り扱う必要があります。
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー