【Awk & Jq活用講座】検索対象が無いときの対処方法〜エラー時の値を#N/Aに置き換える


2021/04/07

検索結果が無い行を発見したときに、通常は何も表示されないで無視されることが多いですが、エラーハンドリングを定義し、エラーを発見したときの処置も実装したい場合があります。

たとえば、検索結果で一致しない行には、新しい内容を新規作成してそこに挿入する...などです。

今回はCSVデータ使う上でのAwkとJqを使ったシェルスクリプトのエラーの捌き方の基礎を行っていきます。


はじめに

当サイトではオフィス業務のComputer-Aidedなハイブリッドな方法を模索し、より効率的なExcel業務を実現したい多忙なオフィスワーカー向けの主にAwkとSedを使うシェル講座です。

シェルスクリプトはどこでもどんなOSでも基本的に使えて、しかも一度使い方を覚えると、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の正規表現で数値かそうではないかの判断をする場合に、text("パターン")Jqの条件分岐構文を組み合わせて利用します。

少しJqスクリプトの働きが分かりにくいかもしれないので、段階的に説明していきます。

まずJqでは読み込んだCsvデータは全てstring型として扱われるので、
tonumber関数を介してnumber型に変換しないといけません。このとき注意が必要なのが、デフォルトで、tonumber関数は数値以外の文字列を入力してしまうと、パースが失敗のエラーでプロセス自体が終了します。

ということで、
if ~ then ~ else ~ end構文でtonumber手前でnumber型にパースできるかどうかを判定しないといけません。

            
            $ 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になっていますが、処理の説明の都合上、同じ処理を2回施してしまっているので、もう少し丸括弧で処理のスコープ範囲を見直して最適化してみると、

            
            $ 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++で実装されいるということもあり、裏ではオブジェクト型に対して使う側がデリケートに取り扱う必要があります。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。