【问题标题】:Splitting a large, complex one column file into several columns with awk使用 awk 将大型、复杂的一列文件拆分为多列
【发布时间】:2018-12-10 13:44:10
【问题描述】:

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

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

我需要实现如下输出:

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

我发现了一个复杂的方法是:

  • 执行sed操作得到

    1
    2
    3
    ...
    #
    11
    22
    33
    ...
    #
    111
    222
    333
    ...
    
  • 如下使用awk将我的文件拆分成几个子文件

    awk -v RS="#" '{print > ("splitted-" NR ".txt")}'
    
  • 使用 sed 再次从我的子文件中删除空格

    sed -i '/^[[:space:]]*$/d' splitted*.txt
    
  • 将所有内容结合在一起:

    paste splitted*.txt > out.txt
    
  • 添加字段分隔符(在我的 bash 脚本中定义)

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

当我多次循环数百万行时,我觉得这很糟糕。 即使返回时间相当好(~80 秒),我也想找到一个完整的 awk 解决方案,但无法解决。 比如:

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

我发现了一些相关的问题,尤其是这个row to column conversion with awk,但它假定括号之间的行数是恒定的,我不能这样做。

任何帮助将不胜感激。

【问题讨论】:

    标签: bash awk field rows


    【解决方案1】:

    使用 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
       ...;   ...;   ...
    

    【讨论】:

    • 非常好的使用三元运算符解决OFS ORS问题的方法。
    • 难以置信的清晰和高效!返回时间为 39 秒。
    • 谢谢。对于您的下一个问题 - 接受您得到的第一个答案并不是一个好主意,因为它会阻止人们发布其他答案。如果您非常幸运并且第一个答案是最好的答案,那么您很高兴,但是如果您没有那么幸运....现在可能还有其他人可以提供很好的答案(也许比我的更好或到目前为止发布的任何其他人)查看您的问题,看到您已经接受了答案,然后继续前进。只是说......
    • @EdMorton,感谢您分享这个先生,您能否解释一下这个(\\s*[()]\\s*)+ RS 中的正则表达式,将不胜感激。
    • @RavinderSingh13 因此,)( 单独或成对的每个组合都被视为 RS,因此它在文件开头单独捕获 (,在文件开头单独捕获 )\n文件的末尾以及中间的每个 \n)\n( 对。
    【解决方案2】:

    如果你知道你有 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 我试过你的通用版本,它也很好用。我也有 62 秒的返回时间。
    【解决方案3】:

    考虑到您的实际 Input_file 与显示的示例相同,请您尝试一次。

    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="; "
    

    或(以上脚本考虑 (...) 内的数字将保持不变,现在添加脚本,该脚本将处理 (....) 内不相等的偶数字段数。

    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.
    

    注意:这也应该适用于 (...) 括号内的 3 个以上的值。

    【讨论】:

    • 太棒了,它有效!脚本时间刚刚从 80 秒到 62 秒,我预计会有更大的收益,但最大的好处是我只需要确保 awk 的版本保持不变,而不用担心 sed+awk。
    • @EdouardIFP 我非常相信这会更快。即使在几秒钟之内。你的文件有多大?
    • 输入文件有 1820 万行 10 位十进制数字,总重量为 172Mb
    【解决方案4】:
    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] 替换为[i][j] 用于GNU awk。我不确定哪个更快,并且我自己没有对脚本进行基准测试。

    【讨论】:

      【解决方案5】:

      这里是 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.. 在 qstn 上需要您的帮助 - 54026451 我没有得到问题perl -F, -lane ' print $#F '中提到的列的确切长度@
      • 如果您的意思是他们的数据中的列数不同,我得到相同的结果:24、25、23。(我通过使用所有元素的格式化打印手动拆分它来确认.)
      • @zdim..是的,我也一样,但是输入的列数更多。awk 的答案是正确的。但是为什么 perl 中的数字更少?
      【解决方案6】:

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

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

      输出:

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

      【讨论】:

      • gsub() 的第一个参数是一个正则表达式,而不是一个字符串,所以使用正则表达式 /.../ 而不是字符串 "..." 分隔符,除非你有特定的理由需要 awk 将字符串转换为使用它之前的正则表达式。此外,不要依赖$1=$1 调用打印,因为它不会总是这样做(如果输入行是0,请考虑结果),再次仅在您有特定目的时在条件上下文中使用操作请注意 - 使用 {$1=$1}1 或类似的,而不仅仅是 $1=$1
      • @EdMorton:出于美学原因,我使用了字符串。 OP 是否要打印空记录?我们不知道,所以我选择了可​​能有副作用的较短版本
      • @EdMorton: $1=$1 是错误的,因为它将删除任何以零开头的记录。更新答案,谢谢。
      • 当比较苹果和苹果时,可以根据美学做出决定,但是当比较“使用正则表达式”(/.../)和“将字符串转换为正则表达式然后使用正则表达式”(@ 987654331@) 恕我直言,无论是/ 还是" 作为分隔符看起来更漂亮,性能和功能方面的考虑都更重要。
      猜你喜欢
      • 2019-06-09
      • 2014-12-16
      • 2015-08-22
      • 2017-01-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-16
      • 2011-10-09
      相关资源
      最近更新 更多