データパーサスクリプトを書きたいです。データ例は次のとおりです。
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の
:
後にゼロ個以上のスペースが続く」に設定します。 - 出力フィールド区切り文字を次のように設定します。
,
- フィールド名の配列を初期化します(静的アプローチを取り、リストをハードコードします)。
フィールドが見つかったら、name
そのフィールドがファイルの最初の行にあることを確認してからそうでない場合、以前に収集したデータを印刷します。次に、先ほど出会ったフィールドから始めて、配列の次のレコードの収集を開始しますcurrent
。name
他のすべての行の場合(単純化のために空白または注釈付きの行がないと仮定しますが、プログラムはこの行を自動的に無視する必要があります)、プログラムは行に記載されているフィールドを確認し、値を配列current
に保存します。現在のレコードの適切な位置にあります。
関数はprintrec
これらの配列を引数として使用し、実際の出力を実行します。欠落した値はn/a
(または使用したい他の文字列)に置き換えられます。印刷後、アレイが次のデータセットを準備できるようにフィールドがクリアされます。
最後に、最後のレコードも印刷されます。
ノート
- ファイルの「値」部分に
:
-space-combinationsも含めることができる場合は、置き換えることでプログラムを強化できます。
渡すcurrent[i]=$2
これは、行の最初の-spaceの組み合わせまでを含むすべての項目をsub(/^[^:]*: */,"") current[i]=$0
:
削除(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)
}