키-값 레코드가 포함된 스쿼시 파일을 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

내 솔루션은 while read루프를 사용하는 것입니다. 내 스크립트의 주요 부분은 다음과 같습니다.

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. 파일의 "값" 부분에 :-space-combinations도 포함될 수 있는 경우 대체하여 프로그램을 강화할 수 있습니다.
    current[i]=$2
    
    ~에 의해
    sub(/^[^:]*: */,"")
    current[i]=$0
    
    줄의 첫 번째 -space 조합을 포함하는 모든 항목을 :제거( )하여 해당 줄의 "첫 번째 -space 조합 이후의 모든 항목"으로 값을 설정합니다 .sub:
  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)
}

관련 정보