使用awk将一个大而复杂的单列文件拆分成多列

4

我有一个由一些商业软件生成的文本文件,如下所示。它由括号分隔的部分组成,每个部分包含数百万个元素,但确切的值因情况而异。

(1
 2
 3
...
)
(11
22
33
...
)
(111
222
333
...
)

我需要实现一个类似如下的输出:
 1;  11;   111
 2;  22;   222
 3;  33;   333
...  ...  ...

我找到了一种复杂的方法,即:
  • perform sed operations to get

    1
    2
    3
    ...
    #
    11
    22
    33
    ...
    #
    111
    222
    333
    ...
    
  • use awk as follows to split my file in several sub-files

    awk -v RS="#" '{print > ("splitted-" NR ".txt")}'
    
  • remove white spaces from my subfiles again with sed

    sed -i '/^[[:space:]]*$/d' splitted*.txt
    
  • join everything together:

    paste splitted*.txt > out.txt
    
  • add a field separator (defined in my bash script)

    awk -v sep=$my_sep 'BEGIN{OFS=sep}{$1=$1; print }' out.txt > formatted.txt
    

我觉得这个循环遍历数百万行数据的方式很糟糕。虽然返回结果的时间还可以接受(约80秒),但我想找到一个完整的awk解决方案,但一直无法实现。类似于:

awk 'BEGIN{RS="(\\n)"; OFS=";"} { print something } '

我找到了一些相关的问题,特别是这个问题使用awk将行转换为列,但它假设括号之间的行数是恒定的,而我无法保证。

非常感谢您的帮助。

6个回答

5

使用GNU awk进行多字符RS和真正的多维数组:

$ cat tst.awk
BEGIN {
    RS  = "(\\s*[()]\\s*)+"
    OFS = ";"
}
NR>1 {
    cell[NR][1]
    split($0,cell[NR])
}
END {
    for (rowNr=1; rowNr<=NF; rowNr++) {
        for (colNr=2; colNr<=NR; colNr++) {
            printf "%6s%s", cell[colNr][rowNr], (colNr<NR ? OFS : ORS)
        }
    }
}

$ awk -f tst.awk file
     1;    11;   111
     2;    22;   222
     3;    33;   333
   ...;   ...;   ...

1
非常好的使用三元运算符来解决OFS ORS问题的方法。 - kvantour
1
非常清晰高效!返回时间只有39秒。 - EdouardIFP
4
谢谢。对于你的下一个问题——接受你获得的第一个答案并不是一个好主意,因为这会使人们不愿意发布其他答案。如果你非常幸运,那么第一个答案就是最好的答案,那么你就可以直接使用它,但如果你不那么幸运……现在可能有其他人正在看着你的问题,能够提供好的答案(也许比我或其他人已经发表的回答更好),但看到你已经接受了一个答案,他们会放弃回答。只是说一下…… - Ed Morton
1
@EdMorton,感谢您的分享,能否请您解释一下在RS中 (\\s*[()]\\s*)+ 正则表达式的含义,将不胜感激。 - RavinderSingh13
2
@RavinderSingh13 这样做是为了让每个单独的或成对的 )( 组合被视为记录分隔符,以便它可以捕获文件开头处的单独的 (,文件结尾处的单独的 )\n 以及文件中间的每个 \n)\n( 组合。 - Ed Morton

4

如果您知道有3列数据,可以按照以下非常丑陋的方式操作:

pr -3ts <file>

接下来需要做的就是移除你的括号:

$ pr -3ts ~/tmp/f | awk 'BEGIN{OFS="; "}{gsub(/[()]/,"")}(NF){$1=$1; print}'
1; 11; 111
2; 22; 222
3; 33; 333
...; ...; ...

你也可以用一行awk命令来实现,但那只会使事情变得更加复杂。上述方法简单易懂。

以下是完整的通用版本awk程序:

awk 'BEGIN{r=c=0}
     /)/{r=0; c++; next}
     {gsub(/[( ]/,"")}
     (NF){a[r++,c]=$1; rm=rm>r?rm:r}
     END{ for(i=0;i<rm;++i) {
            printf a[i,0];
            for(j=1;j<c;++j) printf "; " a[i,j];
            print ""
          }
     }' <file>

肯定很有趣,但章节的数量可能会有所不同 =/ 修改我的问题。 - EdouardIFP
@EdouardIFP,您能否请检查一下我的解决方案并让我知道吗? - RavinderSingh13
@EdouardIFP 我已经更新了我的答案,使其可以完全通用化,其中行数和列数可以变化。 - kvantour
1
@RavinderSingh13,我尝试了你的通用版本,也很好用。我的返回时间也是62秒。 - EdouardIFP

3

请您尝试以下操作,考虑到您的实际输入文件与示例文件相同。

awk -v RS=""  '
{
  gsub(/\n|, /,",")
}
1' Input_file |
awk '
{
  while(match($0,/\([^\)]*/)){
     value=substr($0,RSTART+1,RLENGTH-2)
     $0=substr($0,RSTART+RLENGTH)
     num=split(value,array,",")
     for(i=1;i<=num;i++){
       val[i]=val[i]?val[i] OFS array[i]:array[i]
     }
  }
  for(j=1;j<=num;j++){
     print val[j]
  }
  delete val
  delete array
  value=""
}'   OFS="; "
< p >< strong >< em > OR(上面的脚本考虑到括号内的数字将是常数,现在添加一个脚本,即使括号内的字段数量不相等,也能正常工作。在< code >(...)中。< / em >< / strong >< / p >
awk -v RS=""  '
{
  gsub(/\n/,",")
  gsub(/, /,",")
}
1'  Input_file |
awk '
{
  while(match($0,/\([^\)]*/)){
     value=substr($0,RSTART+1,RLENGTH-2)
     $0=substr($0,RSTART+RLENGTH)
     num=split(value,array,",")
     for(i=1;i<=num;i++){
       val[i]=val[i]?val[i] OFS array[i]:array[i]
     max=num>max?num:max
     }
  }
  for(j=1;j<=max;j++){
     print val[j]
  }
  delete val
  delete array
}' OFS="; "

输出结果如下。
1; 11; 111
2; 22; 222
3; 33; 333

说明: 在此添加上述代码的解释。


awk -v RS=""  '                                      ##Setting RS(record separator) as NULL here.
{                                                    ##Starting BLOCK here.
  gsub(/\n/,",")                                  ##using gsub to substitute new line OR comma with space with comma here.
  gsub(/, /,",")
}
1' Input_file  |                                        ##Mentioning 1 will be printing edited/non-edited line of Input_file. Using | means sending this output as Input to next awk program.
awk '                                                ##Starting another awk program here.
{
  while(match($0,/\([^\)]*/)){                       ##Using while loop which will run till a match is FOUND for (...) in lines.
     value=substr($0,RSTART+1,RLENGTH-2)             ##storing substring from RSTART+1 to till RLENGTH-1 value to variable value here.
     $0=substr($0,RSTART+RLENGTH)                    ##Re-creating current line with substring valeu from RSTART+RLENGTH till last of line.
     num=split(value,array,",")                      ##Splitting value variable into array named array whose delimiter is comma here.
     for(i=1;i<=num;i++){                            ##Using for loop which runs from i=1 to till value of num(length of array).
       val[i]=val[i]?val[i] OFS array[i]:array[i]    ##Creating array val whose index is value of variable i and concatinating its own values.
     }
  }
  for(j=1;j<=num;j++){                               ##Starting a for loop from j=1 to till value of num here.
     print val[j]                                    ##Printing value of val whose index is j here.
  }
  delete val                                         ##Deleting val here.
  delete array                                       ##Deleting array here.
  value=""                                           ##Nullifying variable value here.
}'  OFS="; "                                         ##Making OFS value as ; with space here.

注意:这个方法同样适用于括号(...)内包含超过三个值的情况。


1
太棒了,它运行成功了!脚本时间从80秒降到了62秒,我原本期望会有更大的提升,但最大的好处是我只需要确保awk的版本不变,而不必担心sed+awk。 - EdouardIFP
1
@EdouardIFP 我非常确信这可以更快。甚至在几秒钟内就能完成。你的文件有多大? - kvantour
输入文件有1820万行的10位数字的十进制数,总重量为172Mb。 - EdouardIFP

2
awk 'BEGIN { RS = "\\s*[()]\\s*"; FS = "\\s*" }
NF > 0 {
  maxCol++
  if (NF > maxRow)
    maxRow = NF
  for (row = 1; row <= NF; row++)
    a[row,maxCol] = $row
}
END {
  for (row = 1; row <= maxRow; row++) {
    for (col = 1; col <= maxCol; col++)
      printf "%s", a[row,col] ";"
    print ""
  }
}' yourFile

输出

1;11;111;
2;22;222;
3;33;333;
...;...;...;

当您希望在字段内允许空格时,请将FS="\\s*"更改为FS="\n*"

此脚本支持不同长度的列。

在基准测试时,还要考虑将[i,j]替换为GNU awk[i][j]。我不确定哪个更快,也没有对脚本进行基准测试。


1
这是一个Perl的一行代码解决方案。
$ cat edouard2.txt
(1
2
3
a
)
(11
22
33
b
)
(111
222
333
c
)

$ perl -lne ' $x=0 if s/[)(]// ; if(/(\S+)/) { @t=@{$val[$x]};push(@t,$1);$val[$x++]=[@t] } END { print join(";",@{$val[$_]}) for(0..$#val) }' edouard2.txt
1;11;111
2;22;222
3;33;333
a;b;c

一种非常好的积累数据的方式,我期望它也非常高效。 - zdim
完全同意! - zdim
@zdim.. 需要您的帮助,关于问题 - 54026451,我无法获得与问题中提到的列的确切长度相同的结果。代码如下:perl -F, -lane ' print $#F ' - stack0114106
@zdim..是的,我也遇到了同样的问题,但输入列数更多.. awk 的答案是正确的.. 但为什么 perl 中的数字较少呢? - stack0114106
这在Perl中是否是预期的..所以不能用一行代码实现吗? - stack0114106
显示剩余7条评论

0

我会将每个部分转换为一行,然后进行转置,例如假设您正在使用GNU awk:

<infile awk '{ gsub("[( )]", ""); $1=$1 } 1' RS='\\)\n\\(' OFS=';' |
datamash -t';' transpose

输出:

1;11;111
2;22;222
3;33;333
...;...;...

gsub() 的第一个参数是 regexp,而不是字符串,所以除非您有特定的原因需要 awk 在使用之前将字符串转换为 regexp,否则请使用 regexp /.../ 而不是字符串 "..." 分界符。此外,不要依赖于 $1=$1 调用打印,因为它并不总是这样做(考虑输入行是 0 的情况),只有在有特定目的时才在条件上下文中使用操作 - 使用 {$1=$1}1 或类似方法而不仅仅是 $1=$1 - Ed Morton
@EdMorton:出于美观原因,我使用了一个字符串。但问题的发起人想要打印空记录吗?我们不确定,所以我选择了可能带有期望副作用的较短版本。 - Thor
1
@EdMorton:$1=$1是错误的,因为它会删除任何以零开头的记录。已更新答案,谢谢。 - Thor
1
当比较相似的事物时,基于美学来做决定是可以的。但是当比较“使用正则表达式”(/.../)和“将字符串转换为正则表达式,然后再使用正则表达式”("...")时,性能和功能方面的考虑在我看来比起/"作为分隔符更重要。 - Ed Morton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接