將包含鍵值記錄的檔案壓縮為 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, 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(或您可能想要使用的任何其他字串)。列印後,欄位將被清除,以便數組為下一組資料做好準備。

最後,也列印最後一筆記錄。

筆記

  1. 如果檔案的「值」部分還可以包含:-space-combinations,則可以透過替換來強化程式
    current[i]=$2
    
    經過
    sub(/^[^:]*: */,"")
    current[i]=$0
    
    它將將該值設定:為該行中的“第一個 -space 組合之後的所有內容”,方法是刪除 ( sub) 直到包括:該行上的第一個 -space 組合的所有內容。
  2. 如果任何欄位可以包含輸出分隔符號(在您的範例中,),您將必須採取適當的措施來轉義該字元或引用輸出,具體取決於您要遵守的標準。
  3. 正如您正確指出的那樣,非常不鼓勵使用 shell 循環作為文字處理工具。如果您有興趣閱讀更多內容,您可能想看看本次問答

答案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)
}

相關內容