
データ パーサー スクリプトを書きたいです。サンプル データは次のとおりです。
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
、一部のフィールドが欠落している可能性があります。また、最初のレコードが完全であることは保証されません。description
email
lastLogon
status
期待される出力は、区切り文字で区切られた (例: 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
(または、使用したい他の文字列) に置き換えられます。出力後、フィールドはクリアされ、配列は次の一連のデータに対応できるようになります。
最後に最後のレコードも印刷されます。
注記
- ファイルの「値」部分に
:
スペースの組み合わせも含まれる場合は、次のように置き換えることでプログラムを強化できます。
によるcurrent[i]=$2
sub(/^[^:]*: */,"") current[i]=$0
:
これにより、行の最初の -space の組み合わせまでのすべてを削除 ( ) して、行のsub
最初の -space の組み合わせ以降の すべてに値が設定されます。:
- いずれかのフィールドに出力区切り文字 (例の場合
,
) を含めることができる場合は、準拠する標準に応じて、その文字をエスケープするか、出力を引用符で囲むか、適切な手段を講じる必要があります。 - あなたが正しく指摘したように、シェルループはテキスト処理のツールとしてはあまり推奨されません。もっと詳しく知りたい場合は、以下を参照してください。この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)
}