키-값 레코드가 포함된 파일을 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 조합까지 포함하여 모든 항목을 :제거( 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)
}

관련 정보