キー値レコードを含むファイルを CSV に圧縮する

キー値レコードを含むファイルを CSV に圧縮する

データ パーサー スクリプトを書きたいです。サンプル データは次のとおりです。

name: John Doe
description: AM
email: [email protected]
lastLogon: 999999999999999
status: active
name: Jane Doe
description: HR
email: [email protected]
lastLogon: 8888888888
status: active
...
name: Foo Bar
description: XX
email: [email protected]
status: inactive

キーと値のペアは常に同じ順序 (、、、、) ですがname、一部のフィールドが欠落している可能性があります。また、最初のレコードが完全であることは保証されません。descriptionemaillastLogonstatus

期待される出力は、区切り文字で区切られた (例: CSV) 値です。

John Doe,AM,[email protected],999999999999999,active
Jane Doe,HR,[email protected],8888888888,active
...
Foo Bar,XX,[email protected],n/a,inactive

私の解決策は whilereadループを使用することです。私のスクリプトの主要部分は次のとおりです。

while read line; do
    grep -q '^name:' <<< "$line" && status=''
    case "${line,,}" in
        name*) # capture value ;;
        desc*) # capture value ;;
        email*) # capture value ;;
        last*) # capture value ;;
        status*) # capture value ;;
    esac

    if test -n "$status"; then
        printf '%s,%s,%s,%s,%s\n' "${name:-n\a}" ... etc ...
        unset name ... etc ...
    fi
done < input.txt

これは動作します。しかし、明らかに非常に遅いです。703 行のデータでの実行時間:

real    0m37.195s
user    0m2.844s
sys     0m22.984s

私はそのアプローチについて考えていますawkが、それを使用する経験が十分ではありません。

答え1

次のawkプログラムは動作するはずです。理想的には、別のファイルに保存します (例: squash_to_csv.awk)。

#!/bin/awk -f

BEGIN {
    FS=": *"
    OFS=","
    recfields=split("name,description,email,lastLogon,status",fields,",")
}

function printrec(record) {
    for (i=1; i<=recfields; i++) {
    if (record[i]=="") record[i]="n/a"
    printf "%s%s",record[i],i==recfields?ORS:OFS;
    record[i]="";
    }
}
    
$1=="name" && (FNR>1) { printrec(current) }

{
    for (i=1; i<=recfields;i++) {
        if (fields[i]==$1) {
            current[i]=$2
            break
        }
    }
}

END {
    printrec(current)
}

これを次のように呼び出すことができます

awk -f squash_to_csv.awk input.dat
John Doe,AM,[email protected],999999999999999,active
Jane Doe,HR,[email protected],8888888888,active
Foo Bar,XX,[email protected],n/a,inactive

これにより、BEGINブロック内でいくつかの初期化が実行されます。

  • 入力フィールドの区切り文字を「a の:後に 0 個以上のスペース」に設定します
  • 出力フィールドセパレータを,
  • フィールド名の配列を初期化します(静的なアプローチを採用し、リストをハードコードします)

フィールドが見つかった場合name、それがファイルの最初の行にあるかどうかを確認し、そうでなければcurrentは、以前に収集されたデータを出力します。次に、遭遇したフィールドから始めて、配列内の次のレコードの収集を開始しますname

他のすべての行については (簡単にするために、空行やコメント行は存在しないものと想定していますが、このプログラムはそれらを黙って無視するはずです)、プログラムはその行に記載されているフィールドを確認し、current現在のレコードに使用される配列内の適切な位置に値を格納します。

この関数はprintrec、このような配列をパラメータとして受け取り、実際の出力を実行します。欠落している値はn/a(または、使用したい他の文字列) に置き換えられます。出力後、フィールドはクリアされ、配列は次の一連のデータに対応できるようになります。

最後に最後のレコードも印刷されます。

注記

  1. ファイルの「値」部分に:スペースの組み合わせも含まれる場合は、次のように置き換えることでプログラムを強化できます。
    current[i]=$2
    
    による
    sub(/^[^:]*: */,"")
    current[i]=$0
    
    :これにより、行の最初の -space の組み合わせまでのすべてを削除 ( ) して、行のsub最初の -space の組み合わせ以降の すべてに値が設定されます。:
  2. いずれかのフィールドに出力区切り文字 (例の場合,) を含めることができる場合は、準拠する標準に応じて、その文字をエスケープするか、出力を引用符で囲むか、適切な手段を講じる必要があります。
  3. あなたが正しく指摘したように、シェルループはテキスト処理のツールとしてはあまり推奨されません。もっと詳しく知りたい場合は、以下を参照してください。このQ&A

答え2

$ cat tst.awk
BEGIN {
    OFS = ","
    numTags = split("name description email lastLogon status",tags)
}
{
    tag = val = $0
    sub(/ *:.*/,"",tag)
    sub(/[^:]+: */,"",val)
}
(tag == "name") && (NR>1) { prt() }
{ tag2val[tag] = val }
END { prt() }

function prt(   tagNr,tag,val) {
    for ( tagNr=1; tagNr<=numTags; tagNr++ ) {
        tag = tags[tagNr]
        val = ( tag in tag2val ? tag2val[tag] : "n/a" )
        printf "%s%s", val, (tagNr<numTags ? OFS : ORS)
    }
    delete tag2val
}

$ awk -f tst.awk file
John Doe,AM,[email protected],999999999999999,active
Jane Doe,HR,[email protected],8888888888,active
Foo Bar,XX,[email protected],n/a,inactive

ヘッダー行も印刷したい場合は、セクションの最後にこれを追加しますBEGIN

for ( tagNr=1; tagNr<=numTags; tagNr++ ) {
    tag = tags[tagNr]
    printf "%s%s", tag, (tagNr<numTags ? OFS : ORS)
}

関連情報