我有一个包含许多行的文本文件,需要重新格式化,但我遇到了困难。每行将包含一个 HeaderSegment,后跟 1 个或多个详细信息段,其中还包含子段。
我需要读取文件,匹配模式,然后“做一些事情”(下面解释的内容)。
以下是我的文本文件中的 2 行示例:(从 HeaderSegment 开始)
HeaderSegment:1234:989898:51:2101211748:29:DetailSegment:123467:654321:2:20210122112325:C:0:0:Purchased:SubSegment:064:null:Cash:Whaler:DetailSegment:123468:814211:1:20210121233042:N:0:147:Refund:SubSegment:000:null:Check:Everglades:DetailSegment:234569:825455:1:20210121233113:N:0:685:Purchased:SubSegment:000:null:Cash:Key West:DetailSegment:201754:663854:2:20210122012327:P:128:128:Purchased:SubSegment:000:null:null:null
HeaderSegment:1234:989898:22:2101211750:28:DetailSegment:55555:6637948:0:20210122013332:N:0:401:Refund:SubSegment:000:null:Credit:Whaler
HeaderSegment:1234:989898:22:2101211750:28:DetailSegment:55555:6637948:0:20210122013332:N:0:401:Sale:SubSegment:000:null:Credit:Whaler:SubSegment:30757:null:Cash:Whaler:SubSegment:25500:null:Credit:Seavee
HeaderSegment 始终以 Header 开头,包含 5 个数据字段。
DetailSegment 始终以 DetailSegment 开头,包含 8 个数据字段,并且附加有 1 个或多个 SubSegment。
SubSegments 将附加到 DetailSegments 并包含 4 个数据字段。
每行只有 1 个标题,但可能有多个详细信息段和子段。
我需要解析文本文件的每一行并创建一个新的输出,其中每个详细信息段的一行包含多行。该行将包含:
- 标头段
- 详细信息段
- DetailSegment 与下一个子段之间的每个子段的第一个字段的总和(如果同一行中有 2 个子段)。
- 并且字段分隔符需要更改为,
示例输出将是:
1234,989898,51,2101211748,29,123467,654321,2,20210122112325,C,0,0,Purchased,064
1234,989898,51,2101211748,29,123468,814211,1,20210121233042,N,0,147,Refund,000
1234,989898,51,2101211748,29,234569,825455,1,20210121233113,N,0,685,Purchased,000
1234,989898,51,2101211748,29,201754,663854,2,20210122012327,P,128,128,Purchased,000
1234,989898,22,2101211750,28,55555,6637948,0,20210122013332,N,0,401,Refund,000
1234,989898,22,2101211750,28,55555,6637948,0,20210122013332,N,0,401,Sale,56257
我尝试过使用 awk,但由于缺乏 awk 知识,我很难分解这些段。
希望有人可以提供一些指导(非常欢迎对推荐解决方案的解释,因为它将帮助我更轻松地“学习”这一点)。
答案1
awk -F':?(Header|Detail)Segment:' '
{ sumPos=10;
for(i=3; i<=NF; i++) {
split($i, tmp, ":")
for(x in tmp) {
sum+=tmp[sumPos]; sumPos+=5
};
gsub(/:|:SubSegment.*/, ",", $i)
gsub(/:/, ",", $2)
printf("%s,%s%.3d\n", $2, $i, sum)
sum=0
};
}' infile
答案2
您提到了“学习”,所以这里有一个相当简单的 awk 脚本,我认为它解决了您的问题。它在速度和代码长度方面都没有优化,但希望它是可读的。我会尝试解释各个部分,以便您可以从中学习。
这是代码:
BEGIN {
FS=":"
OFS=","
}
{
pos=readheader()
sanitycheck(pos)
printresult()
}
func readheader() {
r["a"]=$2
r["b"]=$3
r["c"]=$4
r["d"]=$5
r["e"]=$6
pos=7
dcount=0
while($(pos) == "DetailSegment") {
pos=readdetail(pos+1, dcount)
dcount++
}
return pos
}
func readdetail(pos, dcount) {
r["detail"][dcount]["a"]=$(pos+0)
r["detail"][dcount]["b"]=$(pos+1)
r["detail"][dcount]["c"]=$(pos+2)
r["detail"][dcount]["d"]=$(pos+3)
r["detail"][dcount]["e"]=$(pos+4)
r["detail"][dcount]["f"]=$(pos+5)
r["detail"][dcount]["g"]=$(pos+6)
r["detail"][dcount]["h"]=$(pos+7)
pos=pos+8
scount=0
while($(pos) == "SubSegment") {
pos=readsub(pos+1, dcount, scount)
scount++
}
return pos
}
func readsub(pos, dcount, scount) {
r["detail"][dcount]["sub"][scount]["a"]=$(pos+0)
r["detail"][dcount]["sub"][scount]["b"]=$(pos+1)
r["detail"][dcount]["sub"][scount]["c"]=$(pos+2)
r["detail"][dcount]["sub"][scount]["d"]=$(pos+3)
return pos+4
}
func sanitycheck(pos) {
if (pos <= NF) {
print "error line "NR" only parsed "pos" of "NF" fields"
}
}
func printresult() {
for(d in r["detail"]) {
subsum=0
for(s in r["detail"][d]["sub"]) {
subsum+=r["detail"][d]["sub"][s]["a"]
}
print r["a"],r["e"],r["detail"][d]["a"],r["detail"][d]["h"],subsum
}
}
将其保存在名为filter.awk
.并将输入放入名为的文件input
和类型命令中
$ awk -f filter.awk input
或通过管道从源输入
$ fromwherecomesinput | awk -f filter.awk
这是处理您提供的三个样本行的输出
1234,29,123467,Purchased,64
1234,29,123468,Refund,0
1234,29,234569,Purchased,0
1234,29,201754,Purchased,0
1234,28,55555,Refund,0
1234,28,123468,Refund,0
1234,28,234569,Purchased,0
1234,28,201754,Purchased,0
1234,28,55555,Sale,56257
1234,28,123468,Refund,0
1234,28,234569,Purchased,0
1234,28,201754,Purchased,0
我没有输出所有字段。我懒得把它们全部打出来。
我不确定我是否正确理解您的输出要求。也许我听错了。但我尝试解释代码,因此如果您理解其余代码,您可以根据需要更改输出函数。
如果一行的格式不正确,它将输出如下内容:
error line 3 only parsed 7 of 20 fields
除了这个调试输出行之外,我没有编写其他代码来进行错误处理和报告。
代码解释:
首先概述一下代码的作用:awk 逐行读取输入。每行都在冒号处分开。然后我们循环遍历字段并查找段。然后我们将数据收集在树状结构中。最后我们迭代树并计算想要的输出。
值得庆幸的是,所有段都有固定数量的字段。这使得寻找细分变得非常容易。
树结构大致如下:根部有五个标头变量和一个详细信息列表。每个细节都有八个细节变量和一个子列表。每个子中有四个子变量。
最后我链接了 awk 手册中的一些相关页面。因此,如果您想了解有关某个主题的更多信息,请参阅最后。
让我们开始详细的解释
BEGIN {
FS=":"
OFS=","
}
该BEGIN
块由 awk 在从输入读取第一行之前执行。它主要用于初始化变量。
还有一个END
块将在读取最后一行后执行。它通常用于打印最终结果。在我们的例子中,我们每行都有结果,但没有累积的最终结果,因此我们没有END
块。
FS
是字段分隔符。这告诉 awk 如何将每个输入行拆分为所谓的字段。这是 awk 的核心优势之一。通常,合适的字段分隔符值是解决方案的一半。在本例中,我们将字段分隔符设置为冒号 ( :
)。
OFS
是输出字段分隔符。这将是打印语句中字段之间的字符。在本例中,我们设置为逗号 ( ,
)。
这些变量称为控制变量,因为它们改变了 awk 的工作方式。
接下来是“每行”代码块
{
pos=readheader()
sanitycheck(pos)
printresult()
}
该代码块将对每一行执行(对于 awk 术语中的每条记录)。我将代码提取到函数中,因此这个块简短而有趣。
(请注意,您还可以将记录分隔符更改为除换行符之外的其他内容,然后记录可能多于或少于输入行。)
请注意,在 awk 中,所有变量都是全局的(甚至在代码块上),因此这些函数主要用于人类的结构。这就是为什么printresult()
可以打印结果而不传递任何数据。它只是打印全局变量的结果。pos
从回来readheader()
也不是绝对必要的,因为它是全球性的,但我喜欢它,所以我把它留在了里面。
另请注意,在 awk 中只有两种类型的变量:字符串和数字(和数组)。转换是隐式的。未初始化的变量始终为零或空字符串。这过于简单化了。阅读末尾链接的手册。
代码块通常带有一些前缀。例如
/foo/ { ... }
或者
NR > 1 { ... }
这些都是条件。这意味着只有当前记录满足条件时才会执行该块。
我们的代码块没有这样的条件,所以它会对每一行执行。
在 awk 术语中,条件称为模式,代码块称为操作。
接下来对函数的解释:
func readheader() {
r["a"]=$2
r["b"]=$3
r["c"]=$4
r["d"]=$5
r["e"]=$6
pos=7
dcount=0
while($(pos) == "DetailSegment") {
pos=readdetail(pos+1, dcount)
dcount++
}
return pos
}
这里的代码开始看起来像普通的编程语言。记住这个函数是从“for every line”块调用的。所以这个函数每行调用一次。
$2
其他美元数字是对字段的引用。这些字段是 awk 进行分割后的行的一部分。在我们的例子中,字段是冒号之间的值。
($
awk 中的 只用于字段变量。在正则表达式中,它们具有完全不同的含义,但这完全是另一个故事。我们在这段代码中没有正则表达式,所以这里都没有)
$0
始终是整条线。
$1
在我们的例子中始终是HeaderSegment
这样的,所以我们只是跳过它(没有错误检查)。$2
是$6
的五个值HeaderSegment
。
我们将这些变量存储在一个名为 的数组中r
。短名称很危险,因为一切都是全局的,但我们非常需要这个变量,而且我很懒,所以我取了一个短名称。
awk 中的数组不是像 c 或 java 中的数组,而是映射或字典。键值映射。键可以是任何字符串或数字。如果迭代的话顺序基本上是随机的。这些值可以是任何值,包括其他数组。这些数组的数组就是我们用来构建树的东西。
我使用这些键"a"
是"e"
因为我没有更好的值名称。您知道这些值的语义,并且可以为它们指定更有意义的名称,例如"customerID"
或"froobazzaloopaCount"
。
$(pos)
是计算的或间接的字段变量。首先评估括号中的部分。然后引用该字段。所以如果pos
是7
那么$(pos)
就是$7
并且$(pos+1)
是$8
。如果像我们在这里循环寻找下一个那样在字段上循环,这尤其时髦DetailSegment
。
用 awk 术语来说,这称为非常量字段号。
其他函数 (readdetail
和readsub
) 的工作原理类似。
功能sanitycheck
:
func sanitycheck(pos) {
if (pos <= NF) {
print "error line "NR" only parsed "pos" of "NF" fields"
}
}
NF
是该记录中的字段数。pos
是我们用来跟踪接下来要查看的字段的变量。所以如果pos
小于就NF
出问题了。然后我们报告错误。
NR
是当前记录的编号。因为在我们的例子中,记录基本上是一行,这就是行号。
这些变量称为信息变量,因为它们提供有关 awk 当前所处状态的信息。
打印结果函数:
func printresult() {
for(d in r["detail"]) {
subsum=0
for(s in r["detail"][d]["sub"]) {
subsum+=r["detail"][d]["sub"][s]["a"]
}
print r["a"],r["e"],r["detail"][d]["a"],r["detail"][d]["h"],subsum
}
}
没有什么大惊喜。这就像现在大多数现代语言一样。for(d in r["detail"])
迭代数组中的键r["detail"]
。第一个循环迭代细节。第二个循环迭代细节中的子项。
对于每个细节,打印 subs 中第一个值的数字和总和。
关于该声明的一点注释print
:
我们这里有print 1,2,3
(用逗号分隔),输出是1,2,3
(用逗号分隔)。这是因为我们将OFS
(输出字段分隔符)设置为逗号。OFS
例如,如果是,#
则将print 1,2,3
输出1#2#3
。
注意print "1,2,3"
(引用)将始终是1,2,3
不管,OFS
因为这次逗号是字面逗号。
我希望这可以帮助您了解如何使用 awk 解决您的问题。希望我也能够足够好地解释事情,以便您可以根据您的进一步需求调整代码。
Fine awk 手册中相关主题的链接
有关BEGIN
和END
块的更多信息:https://www.gnu.org/software/gawk/manual/html_node/Using-BEGIN_002fEND.html
有关字段分隔符 ( FS
) 的更多信息:https://www.gnu.org/software/gawk/manual/html_node/Field-Separators.html
有关“控制”变量的更多信息(FS
等OFS
):https://www.gnu.org/software/gawk/manual/html_node/User_002dmodified.html
有关“信息”变量(NR
和NF
)的更多信息:https://www.gnu.org/software/gawk/manual/html_node/Auto_002dset.html
有关记录的更多信息(最常见的行):https://www.gnu.org/software/gawk/manual/html_node/Records.html
有关变量可见性的更多信息(一切都是全局的):https://www.gnu.org/software/gawk/manual/html_node/Global-Namespace.html但有些可能是本地的https://www.gnu.org/software/gawk/manual/html_node/Variable-Scope.html
有关变量类型(字符串和数字)的更多信息:https://www.gnu.org/software/gawk/manual/html_node/Variable-Typing.html
有关模式和操作(条件和代码块)的更多信息:https://www.gnu.org/software/gawk/manual/html_node/Patterns-and-Actions.html
有关数组(键值映射或字典)的更多信息:https://www.gnu.org/software/gawk/manual/html_node/Arrays.html
有关字段数字的更多信息(美元数字或字段变量):https://www.gnu.org/software/gawk/manual/html_node/Fields.html
有关非常量字段数(计算或间接字段变量)的更多信息:https://www.gnu.org/software/gawk/manual/html_node/Nonconstant-Fields.html