【実践!CSVデータ検索スクリプト作成編】CSVデータの複数列から複数条件で検索して行データを表示させるスクリプト


2021/04/12

CSVデータの検索に特化したエクセルでいうところのVLOOKUP関数のような機能をもつスクリプトツールを作成してみる特集の第3回目です。

今回も
前の回に引き続いて、もっと自由に検索列を決められて、なおかつ複数の検索条件も与えられるように再度拡張していきましょう。


はじめに

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

シェルスクリプトはどこでもどんなOSでも基本的に使えて、しかも一度使い方を覚えると、Excelと組み合わせて最高に効率の良いオフィスワークツールが作れることでしょう。

合同会社タコスキングダム|蛸壺の技術ブログ


スクリプトツールの概要

さて、前回のスクリプトツールのおさらいですが、一列目だけを検索するためのcsvテキストを以下用に与えておいて、

            
            山田
佐藤
モフ雄
        
のように行で仕込んで置いて、この検索用csvをコマンドで入力し、

            
            $ ./simple_finder.sh -i search_list.csv employee.csv
山田の項目...
佐藤の項目...
モフ雄の項目...
        
これを複数条件かつ複数列ターゲットで検索できるようにするため、以下のように検索用csvの書式を変更します。

            
            山田,製造,,
子,,名古屋,
モフ雄,,,16年
川,製造,,14
        
このcsvフォーマットで、検索するキーをコンマ切りで検索ターゲットの列と併せておいて、なおかつ複数の条件で検索できるようにしてみましょう。


ツールスクリプトの修正

今回もいきなり修正済のツールスクリプトから先にお見せします。

            
            #!/bin/bash

usage_exit() {
    echo "USAGE: $(basename $0) [-l key_list] [-h help] [input_file]" 1>&2
    exit 1
}

noarg_err() {
    echo "ERROR: must provide key_list!" 1>&2
    exit 1
}

selecteither_err() {
    echo "ERROR: should select an option from -l/-i!" 1>&2
    exit 1
}

noinputfile_err() {
    echo "ERROR: not allowed input file to be empty!" 1>&2
    exit 1
}

while getopts l:i: OPT; do
    case $OPT in
        l ) KEY_LIST="$OPTARG"
            ;;
        i ) KEY_LIST_FILE="$OPTARG"
            ;;
        \? ) usage_exit
            ;;
    esac
done

shift $((OPTIND - 1))

if [ -z "$KEY_LIST" ] && [ -z "$KEY_LIST_FILE" ]; then
    noarg_err
elif [ -n "$KEY_LIST" ] && [ -n "$KEY_LIST_FILE" ]; then
    selecteither_err
elif [ -z "$1" ]; then
    noinputfile_err
fi

echo "FILE: $1, KEY_LIST: ${KEY_LIST}, KEY_LIST(FILE): ${KEY_LIST_FILE}"

if [ -n "$KEY_LIST" ]; then
    PARR=($(echo "${KEY_LIST}" | tr ',' ' '))
    for p in "${PARR[@]}"; do
        awk -F"," '
            $1 ~ /'"$p"'/ { last_macthed = $0; }
            END { print last_macthed ? last_macthed : "'"$p"'?,#N/A,#N/A,#N/A"; }
        ' $1
    done
elif [ -n "$KEY_LIST_FILE" ] && [ -f "$KEY_LIST_FILE" ]; then
    #👇①...☆今回修正する部分
    cat "$KEY_LIST_FILE" | while read KEY || [ -n "${KEY}" ]; do
        K=($(echo "${KEY}" | awk -F",{1}" '{k1=$1?$1:"NULL";k2=$2?$2:"NULL";k3=$3?$3:"NULL";k4=$4?$4:"NULL";print k1,k2,k3,k4;}'))
        #👇でもキーワードの抽出が可能
        #K=($(echo "${KEY}" | tr , '\n' | sed -r 's,^$,NULL,' | tr '\n' ' '))
        echo "+++ MATCHING-KEYS 1: ${K[0]} 2: ${K[1]} 3: ${K[2]} 4: ${K[3]} +++ "
        awk -F"," '
            function isSkip(s_){return s_ == "NULL";}
            {
                ch1 = !isSkip("'"${K[0]}"'");
                ch2 = !isSkip("'"${K[1]}"'");
                ch3 = !isSkip("'"${K[2]}"'");
                ch4 = !isSkip("'"${K[3]}"'");
                total = ch1 + ch2 + ch3 + ch4;
                if (ch1 && $1 !~ /'"${K[0]}"'/) {ch1=0}
                if (ch2 && $2 !~ /'"${K[1]}"'/) {ch2=0}
                if (ch3 && $3 !~ /'"${K[2]}"'/) {ch3=0}
                if (ch4 && $4 !~ /'"${K[3]}"'/) {ch4=0}
                if (ch1 + ch2 + ch3 + ch4 == total) {print $0}
            }
        ' $1
    done
fi
        
このスクリプトの①の部分の内部のAwkスクリプトの処理を理解することがポイントです。

各列の検索キーワード
K[0] ~ K[3]はawk外部から仕込むためにシングルクォーテーション括弧を分割する合間合間に呼び出すテクニックはいつものこととして、検索キーが仕込まれていなかった列はNULLという名前に一旦変えておきます。

そしてawk内の
isSkipメソッドを定義しておき、列を検索するかしないかを判定します。結局は、このスキップフラグで列を検索する総和(total)を計算しておいて、もしその列で検索がヒットしなかった場合にこのフラグをゼロにすることで、もし検索すべき行がなかったときに再度計算したtotalがその検索の前後で一致しないことを利用した実装になっています。

さて、この修正したツールスクリプトを前回と同じコマンドで実行してみますと、

            
            $ ./simple_finder.sh -i search_list.csv employee.csv
+++ MATCHING-KEYS 1: 山田 2: 製造 3: NULL 4: NULL +++
+++ MATCHING-KEYS 1: 子 2: NULL 3: 名古屋 4: NULL +++
島田フガ子,経理部,名古屋支部,15年
+++ MATCHING-KEYS 1: モフ雄 2: NULL 3: NULL 4: 16年 +++
田頭モフ雄,営業部,名古屋支部,16年
+++ MATCHING-KEYS 1: 川 2: 製造 3: NULL 4: 14年 +++
亀川ヲル士,製造部,山口工場,14年
        
と複数の列に対してマッチングした行だけを抽出してくれております。


おまけ〜キーリストの直接入力検索もターゲット列を変更できるようにする

前々回の話で、-lオプションを使えば検索用のcsvファイルなしでも検索できるようにしていました。

今回のお題は先ほどの節の内容で完了していますが、こちらも検索のターゲット列は1列目だけでしたので、新たに-tオプションを追加して、任意の列で簡易検索できるように修正しておきます。

利用イメージとしては以下のような感じです。

            
            $ ./simple_finder.sh -l 経理,海外 -t 2 employee.csv
経理部の項目...
.....
海外部の項目...
.....
        
それではさらに上のスクリプトに修正を盛り込みます。

            
            #!/bin/bash

usage_exit() {
    echo "USAGE: $(basename $0) [-l key_list] [-t column_number] [-h help] [input_file]" 1>&2
    exit 1
}

noarg_err() {
    echo "ERROR: must provide key_list!" 1>&2
    exit 1
}

selecteither_err() {
    echo "ERROR: should select an option from -l/-i!" 1>&2
    exit 1
}

noinputfile_err() {
    echo "ERROR: not allowed input file to be empty!" 1>&2
    exit 1
}

while getopts l:t:i: OPT; do
    case $OPT in
        l ) KEY_LIST="$OPTARG"
            ;;
        t ) TARGET_COLUMN="$OPTARG"
            ;;
        i ) KEY_LIST_FILE="$OPTARG"
            ;;
        \? ) usage_exit
            ;;
    esac
done

shift $((OPTIND - 1))

if [ -z "$KEY_LIST" ] && [ -z "$KEY_LIST_FILE" ]; then
    noarg_err
elif [ -n "$KEY_LIST" ] && [ -n "$KEY_LIST_FILE" ]; then
    selecteither_err
elif [ -z "$1" ]; then
    noinputfile_err
fi

if [ -z "$TARGET_COLUMN" ]; then
    TARGET_COLUMN=1
fi

echo "FILE: $1, KEY_LIST: ${KEY_LIST}, KEY_LIST(FILE): ${KEY_LIST_FILE}"

if [ -n "$KEY_LIST" ]; then
    PARR=($(echo "${KEY_LIST}" | tr ',' ' '))
    for p in "${PARR[@]}"; do
        echo "+++ MATCHING-KEYS: $p @ column #$TARGET_COLUMN +++"
        awk -F"," '
            $'"$TARGET_COLUMN"' ~ /'"$p"'/ {
                last_macthed = last_macthed ? last_macthed "\n" $0 : $0;
            }
            END {
                switch_num='"$TARGET_COLUMN"';
                if (switch_num==1) {
                    err_res="'"$p"'?,#N/A,#N/A,#N/A";
                } else if (switch_num==2) {
                    err_res="#N/A,'"$p"'?,#N/A,#N/A";
                } else if (switch_num==3) {
                    err_res="#N/A,#N/A,'"$p"'?,#N/A";
                } else if (switch_num==4) {
                    err_res="#N/A,#N/A,#N/A,'"$p"'?";
                }
                print last_macthed ? last_macthed : err_res;
            }
        ' $1
    done
elif [ -n "$KEY_LIST_FILE" ] && [ -f "$KEY_LIST_FILE" ]; then
    cat "$KEY_LIST_FILE" | while read KEY || [ -n "${KEY}" ]; do
        K=($(echo "${KEY}" | awk -F",{1}" '{k1=$1?$1:"NULL";k2=$2?$2:"NULL";k3=$3?$3:"NULL";k4=$4?$4:"NULL";print k1,k2,k3,k4;}'))
        #👇でもキーワードの抽出が可能
        #K=($(echo "${KEY}" | tr , '\n' | sed -r 's,^$,NULL,' | tr '\n' ' '))
        echo "+++ MATCHING-KEYS 1: ${K[0]} 2: ${K[1]} 3: ${K[2]} 4: ${K[3]} +++"
        awk -F"," '
            function isSkip(s_){return s_ == "NULL";}
            {
                ch1 = !isSkip("'"${K[0]}"'");
                ch2 = !isSkip("'"${K[1]}"'");
                ch3 = !isSkip("'"${K[2]}"'");
                ch4 = !isSkip("'"${K[3]}"'");
                total = ch1 + ch2 + ch3 + ch4;
                if (ch1 && $1 !~ /'"${K[0]}"'/) {ch1=0}
                if (ch2 && $2 !~ /'"${K[1]}"'/) {ch2=0}
                if (ch3 && $3 !~ /'"${K[2]}"'/) {ch3=0}
                if (ch4 && $4 !~ /'"${K[3]}"'/) {ch4=0}
                if (ch1 + ch2 + ch3 + ch4 == total) {print $0}
            }
        ' $1
    done
fi
        
これで先ほど予告していたコマンドを試すと、

            
            $ ./simple_finder.sh -l 経理,海外 -t 2 employee.csv
FILE: employee.csv, KEY_LIST: 経理,海外, KEY_LIST(FILE):
+++ MATCHING-KEYS: 経理 @ column #2 +++
島田フガ子,経理部,名古屋支部,15年
梅岡ボル伍,経理部,本社,25年
+++ MATCHING-KEYS: 海外 @ column #2 +++
銭形ガメ吉,海外部,メキシコ支部,11年
蒲田ウオ奈,海外部,メキシコ支部,9年
        
といった感じに指定した列で直接引数入力の簡易検索コマンドに仕立てることができました。


まとめ

今回はより利便性を高め、マルチ列検索が入力ファイルから可能にできるように拡張を行いました。

ここまで来るとそれなりにツールスクリプトらしい実用的な感じになってきましたが、実際に実戦投入するには対象のデータベースの構造を考慮した細かい修正が必要になります。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

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