
데이터 파서 스크립트를 작성하고 싶습니다. 예시 데이터는 다음과 같습니다.
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
(또는 사용하려는 다른 문자열) 로 대체됩니다 . 인쇄 후에는 배열이 다음 데이터 묶음을 준비할 수 있도록 필드가 지워집니다.
마지막에는 마지막 레코드도 인쇄됩니다.
메모
- 파일의 "값" 부분에
:
-space-combinations도 포함될 수 있는 경우 대체하여 프로그램을 강화할 수 있습니다.
~에 의해current[i]=$2
줄의 첫 번째 -space 조합을 포함하는 모든 항목을sub(/^[^:]*: */,"") current[i]=$0
:
제거( )하여 해당 줄의 "첫 번째 -space 조합 이후의 모든 항목"으로 값을 설정합니다 .sub
:
- 필드 중 하나라도 출력 구분 문자(예
,
: )를 포함할 수 있는 경우 준수하려는 표준에 따라 해당 문자를 이스케이프하거나 출력을 인용하는 적절한 조치를 취해야 합니다. - 올바르게 언급했듯이 쉘 루프는 텍스트 처리 도구로 사용하지 않는 것이 좋습니다. 더 많은 내용을 읽고 싶으시면 다음을 읽어보셔도 좋습니다.이 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)
}