我想写一个数据解析器脚本。示例数据为:
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
这会将值设置为“sub(/^[^:]*: */,"") current[i]=$0
:
行中第一个 -space 组合之后的所有内容”,方法是删除 (sub
) 直到包括:
行上第一个 -space 组合的所有内容。 - 如果任何字段可以包含输出分隔符(在您的示例中
,
),您将必须采取适当的措施来转义该字符或引用输出,具体取决于您要遵守的标准。 - 正如您正确指出的那样,非常不鼓励使用 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)
}