【问题标题】:Filling empty fields填充空白字段
【发布时间】:2017-04-20 08:06:16
【问题描述】:

我有一个非常大的制表符分隔文件(大约 1200 万行),如下所示:

F1    1
      2
      700
F2    89
      900
      10000
      19
F3    100
      60001

有什么办法可以让我这样:

F1    1
F1    2
F1    700
F2    89
F2    900
F2    10000
F2    19
F3    100
F3    60001

我尝试过使用 sed 脚本,但需要很长时间。

例如

sed 's/^/F1/' FILE | cut -c3- > FILE1 ; mv FILE1 FILE

我可以在excel中使用

=IF(a2=="",c1,a2)

并向下拖动。但是 Excel 只允许我加载一定数量的行。

(假设我已将“F1”复制到 C1)

用 awk 或 sed 肯定有更简单的方法吗?

【问题讨论】:

  • 第二行的数字 2 是否带有一个或两个制表符前缀?
  • 如果 sed 需要很长时间,那么 awk 也可能需要很长时间......我可以提供一个 Python 解决方案,但它也会很慢。在这种情况下,问题是磁盘 I/O。
  • 第 1 列中没有值的行有两个制表符
  • 1200 万行并不是一个“非常大的文件”。
  • @EdMorton 及时指出。而这一次它避免我发布awk 解决方案(你必须在之后更正但仍然...... :))。谢谢。

标签: excel bash perl awk sed


【解决方案1】:

Perl 命令看起来像这样:

perl -F'\t' -ple '$c1 = $F[0] if $F[0]; $F[0] ||= $c1; $_=join"\t",@F' 40982582.tsv > your_output.tsv

更具可读性:

#!/usr/bin/perl -pl -F\t

$c1 = $F[0] if $F[0]; # save off the first column if we have one.
$F[0] ||= $c1;        # override empty first-columns.
$_ = join "\t", @F;   # set the topic back to the full line for -p to print

然后运行:

perl yourscript.pl input_file.tsv > output_file.tsv

(您也可以使用“-i”标志“就地”覆盖文件,但这实际上不会在运行时为您节省任何时间或磁盘空间。)

不过,无论您的文件有多长,这将需要多长时间。

【讨论】:

  • 问题是 awk,但有 perl 标签,所以我认为 perl 是一个可以接受的选项。同样的事情可以在 awk 中完成。
  • "-F 隐式设置 -a 和 -n。"在 perlrun 中(无论如何使用 5.22.2,多年来这可能已经改变)。
  • 是的,这很新。 5.20+
【解决方案2】:

awk 来救援!

$ awk 'BEGIN {FS=OFS="\t"} 
             {if($1!="") p=$1; else $1=p}1' file

F1      1
F1      2
F1      700
F2      89
F2      900
F2      10000
F2      19
F3      100
F3      60001

这是我使用的输入文件

$ cat -A file

F1^I1$
^I2$
^I700$
F2^I89$
^I900$
^I10000$
^I19$
F3^I100$
^I60001$

【讨论】:

  • 非常感谢您的帮助!
【解决方案3】:
$ cat pre.awk
BEGIN   { FS = OFS = "\t" }
NF == 1 { print  pre,       $1 }
NF == 2 { print (pre = $1), $2 }

用法:

$ awk -f pre.awk file.dat

【讨论】:

    【解决方案4】:

    我建议:

    awk -F '\t' '{OFS=FS; $1==""?$1=b:b=$1}1' file
    

    【讨论】:

    • 我现在已经用 Solaris 11 的 awk 对其进行了测试。它不起作用,但它可以在 Solaris 11 的 gawk (GNU) 上正常工作。
    • \t 没有被引用,而是变为t - 由外壳程序 - 再次变为:$1 == t。 t 是一个空变量。但是你在\t 上中继将在给定的shell 中产生t。
    • 该脚本存在 2 个性能问题 - 1) 为每一行输入设置 OFS=FS 显然比在启动时设置一次要慢,并且 2) 分配给 $1 会强制 awk 每次重新编译记录发生的时间。此外,带括号的三元表达式比带括号的表达式更难阅读,并且在某些情况下会在某些 awk 中导致语法错误,因此请始终将其括起来,因为这样做没有负面影响。
    【解决方案5】:
    perl -F'\t' -lane'$h = $F[0] ||= $h; print join "\t", @F'
    

    赋值是右关联的,所以

    $h = $F[0] ||= $h;
    

    等价于

    $h = ( $F[0] ||= $h );
    

    因此

    $F[0] ||= $h;
    $h = $F[0];
    

    和

    $F[0] = $h if !$F[0];
    $h = $F[0];
    

    【讨论】:

    • 这是一个聪明的任务。如果设置$,="\t",则不必join
    • 我会使用BEGIN {$,="\t"} $h = ...; print @F
    • 我为什么要使用BEGIN { $, = "\t"; } print @F; 而不是print join "\t", @F;'?
    • perl -aF'/(\t)/' -lne '$d = $F[0] ||= $d; print @F'
    【解决方案6】:
    $ awk '{sub(/^\t/,p"&");p=$1}1' file
    F1      1
    F1      2
    F1      700
    F2      89
    F2      900
    F2      10000
    F2      19
    F3      100
    F3      60001
    

    【讨论】:

      【解决方案7】:

      这是sed 解决方案:

      sed -r -n '/\w+\s+\w+/{p; s/^(\w+\s+).*/\1/; h};/^\w/!{G;s/^\s+(\w+)\s+(\w+\s+)/\2\1/;p}' file.dat
      F1    1
      F1    2
      F1    700
      F2    89
      F2    900
      F2    10000
      F2    19
      F3    100
      F3    60001
      

      时间消耗和与其他 awk 解决方案的比较

      这是测试代码(bash 脚本)

      #!/bin/sh
      
      ## Input file with data to process
      inputfile="bigdata3.txt"
      
      ## solutions dir, that contains
      ## - solution files, and
      ## - every solution file contains code to evaluate
      solutions="solutions/"
      
      file_size_kb=$(du -k "$inputfile" | cut -f1)
      echo "Size of input file: $file_size_kb kB"
      file_lines_count=$(wc -l $inputfile | sed -r 's/\s*([0-9]+)\s+.*/\1/')
      echo "Lines of input file: $file_lines_count"
      
      test_code="time \$code > out.txt"
      echo "Test code: '$test_code'"
      
      for solution in $solutions* ; do
          ## output file deletion
          if [ -f out.txt ]; then 
              rm out.txt 
          fi;
      
          code_content=$(cat $solution)
          code="time $code_content $inputfile > out.txt"
          echo "--------------------------------------------------"
          echo "Solution: $solution"
          echo "Code    : $code"
          res=$(sh -c "cd $PWD; $code")
          echo $res
      
          ## check correctness of output
          incorrect_lines_count=$(sed -r -n "/^[^[a-zA-Z0-9_]+/p" out.txt |  wc -l | sed -r 's/\s*([0-9]+)\s*.*/\1/')
          total_lines=$(wc -l out.txt | sed -r 's/\s*([0-9]+)\s+.*/\1/') 
          if [ $incorrect_lines_count -eq 0 ] && [ $total_lines -eq $file_lines_count ]; then
              echo "TEST PASSED"
          else
              echo "INVALID SOLUTION:"
              echo " - not processed lines: $incorrect_lines_count (spaces at line beginning)"
              echo " - total processed lines: $total_lines (expecting: $file_lines_count)"
          fi
      done;
      

      和结果(对于 46kB 的输入文件):

      Size of input file: 46034 kB
      Lines of input file: 8658000
      Test code: 'time $code > out.txt'
      --------------------------------------------------
      Solution: solutions/Cyrus_awk
      Code    : time awk -F '\t' '{OFS=FS; $1==""?$1=b:b=$1}1' bigdata3.txt > out.txt
      
      real    0m8.072s
      user    0m7.644s
      sys     0m0.420s
      
      TEST PASSED
      --------------------------------------------------
      Solution: solutions/Ed_Morton_awk
      Code    : time awk '{sub(/^\t/,p"&");p=$1}1' bigdata3.txt > out.txt
      
      real    0m11.887s
      user    0m11.434s
      sys     0m0.389s
      
      TEST PASSED
      --------------------------------------------------
      Solution: solutions/Marek_Nowaczyk_sed
      Code    : time sed -r -n '/\w+\s+\w+/{p; s/^(\w+\s+).*/\1/; h};/^\w/!{G;s/^\s+(\w+)\s+(\w+\s+)/\2\1/;p}' bigdata3.txt >
      out.txt
      
      real    0m30.239s
      user    0m29.577s
      sys     0m0.545s
      
      TEST PASSED
      --------------------------------------------------
      Solution: solutions/Tanktalus_perl
      Code    : time perl -F'\t' -ple '$c1 = $F[0] if $F[0]; $F[0] ||= $c1; $_=join"\t",@F'  bigdata3.txt > out.txt
      
      real    0m6.992s
      user    0m6.692s
      sys     0m0.281s
      
      TEST PASSED
      --------------------------------------------------
      Solution: solutions/ikeagami_perl
      Code    : time perl -F'\t' -lane'$h = $F[0] ||= $h; print join "\t", @F' bigdata3.txt > out.txt
      
      real    0m12.977s
      user    0m12.463s
      sys     0m0.483s
      
      TEST PASSED
      --------------------------------------------------
      Solution: solutions/karakfa_awk
      Code    : time awk 'BEGIN {FS=OFS="\t"} {if($1!="") p=$1; else $1=p}1'  bigdata3.txt > out.txt
      
      real    0m7.545s
      user    0m6.832s
      sys     0m0.498s
      
      TEST PASSED
      --------------------------------------------------
      Solution: solutions/slitvinov_awk
      Code    : time awk 'BEGIN   { FS = OFS = "\t" } NF == 1 { print  pre,       $1 } NF == 2 { print (pre = $1), $2 }' bigda
      ta3.txt > out.txt
      
      real    0m8.333s
      user    0m7.908s
      sys     0m0.404s
      
      INVALID SOLUTION:
       - not processed lines: 5772000 (spaces at line beginning)
       - total processed lines: 8658000 (expecting: 8658000)
      

      结论

      @Tanktalus perl 解决方案具有最佳性能,但 awk @karakfa 和 awk @Cyrus 解决方案也表现良好。

      题外话

      这个sed 解决方案在较小的文件(来自此示例和 8k 文件)上具有最佳性能,但在较大的数据上确实很慢。

      【讨论】:

      • 干得好,但您可以展示您使用的测试代码,并运行测试至少 3 秒,以消除启动时间对时序的影响。参见例如Perl module Benchmark。
      • 太棒了!感谢更新。现在,接下来可能是检查时间是否稳定。例如,如果您在 for solution in $solutions* 周围添加另一个 for 循环,在其中取 10 次运行的平均值,并检查时间是否在迭代中剧烈波动或似乎相当恒定。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-28
      • 1970-01-01
      相关资源
      最近更新 更多