使用模式匹配解析文本文件中的每一行

使用模式匹配解析文本文件中的每一行

我有一个包含许多行的文本文件,需要重新格式化,但我遇到了困难。每行将包含一个 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)是计算的或间接的字段变量。首先评估括号中的部分。然后引用该字段。所以如果pos7那么$(pos)就是$7并且$(pos+1)$8。如果像我们在这里循环寻找下一个那样在字段上循环,这尤其时髦DetailSegment

用 awk 术语来说,这称为非常量字段号。

其他函数 (readdetailreadsub) 的工作原理类似。

功能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 手册中相关主题的链接

有关BEGINEND块的更多信息: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

有关“控制”变量的更多信息(FSOFS):https://www.gnu.org/software/gawk/manual/html_node/User_002dmodified.html

有关“信息”变量(NRNF)的更多信息: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

相关内容