【问题标题】:Removing Lines and columns with all zeros删除全为零的行和列
【发布时间】:2013-08-07 09:32:41
【问题描述】:

如何删除包含所有零的文本文件中的行(行)和列。 比如我有一个文件:

1 0 1 0 1
0 0 0 0 0
1 1 1 0 1
0 1 1 0 1
1 1 0 0 0
0 0 0 0 0
0 0 1 0 1  

我想删除第 2 行和第 4 行以及第 2 列。输出应如下所示:

1 0 1 1 
1 1 1 1 
0 1 1 1 
1 1 0 0 
0 0 1 1 

我可以使用 sed 和 egrep 来做到这一点

  sed '/0 0 0 0/d' or egrep -v '^(0 0 0 0 )$'

对于带有零的行,但对于具有数千列的文件来说太不方便了。我不知道如何删除全为零的列,这里是第二列。

【问题讨论】:

标签: perl bash unix awk


【解决方案1】:

Perl 解决方案。它将所有非零行保留在内存中以在最后打印,因为它在处理整个文件之前无法判断哪些列将是非零的。如果你得到Out of memory,你可以只存储你想要输出的行数,并在打印这些行时再次处理文件。

#!/usr/bin/perl
use warnings;
use strict;

my @nonzero;                                       # What columns where not zero.
my @output;                                        # The whole table for output.

while (<>) {
    next unless /1/;
    my @col = split;
    $col[$_] and $nonzero[$_] ||= 1 for 0 .. $#col;
    push @output, \@col;
}

my @columns = grep $nonzero[$_], 0 .. $#nonzero;   # What columns to output.
for my $line (@output) {
    print "@{$line}[@columns]\n";
}

【讨论】:

    【解决方案2】:

    这个版本不是在内存中存储行,而是扫描文件两次:一次找到“零列”,再次找到“零行”并执行输出:

    awk '
        NR==1   {for (i=1; i<=NF; i++) if ($i == 0) zerocol[i]=1; next} 
        NR==FNR {for (idx in zerocol) if ($idx) delete zerocol[idx]; next}
        {p=0; for (i=1; i<=NF; i++) if ($i) {p++; break}}
        p {for (i=1; i<=NF; i++) if (!(i in zerocol)) printf "%s%s", $i, OFS; print ""}
    ' file file
    
    1 0 1 1 
    1 1 1 1 
    0 1 1 1 
    1 1 0 0 
    0 0 1 1 
    

    一个 ruby​​ 程序:ruby 有一个很好的数组方法transpose

    #!/usr/bin/ruby
    
    def remove_zeros(m)
      m.select {|row| row.detect {|elem| elem != 0}}
    end
    
    matrix = File.readlines(ARGV[0]).map {|line| line.split.map {|elem| elem.to_i}}
    # remove zero rows
    matrix = remove_zeros(matrix)
    # remove zero rows from the transposed matrix, then re-transpose the result
    matrix = remove_zeros(matrix.transpose).transpose
    matrix.each {|row| puts row.join(" ")}
    

    【讨论】:

      【解决方案3】:

      另一个 awk 变体:

      awk '{show=0; for (i=1; i<=NF; i++) {if ($i!=0) show=1; col[i]+=$i;}} show==1{tr++; for (i=1; i<=NF; i++) vals[tr,i]=$i; tc=NF} END{for(i=1; i<=tr; i++) { for (j=1; j<=tc; j++) { if (col[j]>0) printf("%s%s", vals[i,j], OFS)} print ""; } }' file
      

      扩展表格:

      awk '{
         show=0;
         for (i=1; i<=NF; i++) {
            if ($i != 0)
               show=1;
          col[i]+=$i;
         }
      }
      show==1 {
         tr++;
         for (i=1; i<=NF; i++)
            vals[tr,i]=$i;
         tc=NF
      }
      END {
         for(i=1; i<=tr; i++) {
            for (j=1; j<=tc; j++) {
               if (col[j]>0)
                  printf("%s%s", vals[i,j], OFS)
            }
            print ""
         }
      }' file
      

      【讨论】:

        【解决方案4】:

        试试这个:

        perl -n -e '$_ !~ /0 0 0 0/ and print' data.txt
        

        或者简单地说:

        perl -n -e '/1/ and print' data.txt
        

        data.txt 包含您的数据的位置。

        在 Windows 中,使用双引号:

        perl -n -e "/1/ and print" data.txt
        

        【讨论】:

        • 其实我不想给它一个像 /0 0 0 0/ 这样的精确模式,因为它对于包含数千个字符的大文件来说太麻烦了。
        • 在 Perl 中删除包含全零的第二列有什么想法吗?
        【解决方案5】:

        大家一起:

        $ awk '{for (i=1; i<=NF; i++) {if ($i) {print; next}}}' file | awk '{l=NR; c=NF; for (i=1; i<=c; i++) {a[l,i]=$i; if ($i) e[i]++}} END{for (i=1; i<=l; i++) {for (j=1; j<=c; j++) {if (e[j]) printf "%d ",a[i,j] } printf "\n"}}'
        

        这使得行检查:

        $ awk '{for (i=1; i<=NF; i++) {if ($i) {print; next}}}' file
        1 0 1 1
        1 0 1 0
        1 0 0 1
        

        它循环遍历该行的所有字段。如果其中任何一个为“真”(表示非 0),则打印该行 (print) 并换行到下一行 (next)。

        这使得列检查:

        $ awk '{l=NR; c=NF;
          for (i=1; i<=c; i++) {
              a[l,i]=$i;
              if ($i) e[i]++
          }}
          END{
            for (i=1; i<=l; i++){
              for (j=1; j<=c; j++)
            {if (e[j]) printf "%d ",a[i,j] }
            printf "\n"
              }
            }'
        

        它基本上保存a数组中的所有数据,l行数,c列数。 e 是一个数组保存,如果一列的值不同于 0 或不。然后它会在设置e 数组索引时循环并打印所有字段,这意味着该列是否具有任何非零值。

        测试

        $ cat a
        1 0 1 0 1
        0 0 0 0 0
        1 1 1 0 1
        0 1 1 0 1
        1 1 0 0 0
        0 0 0 0 0
        0 0 1 0 1
        $ awk '{for (i=1; i<=NF; i++) {if ($i) {print; next}}}' a | awk '{l=NR; c=NF; for (i=1; i<=c; i++) {a[l,i]=$i; if ($i) e[i]++}} END{for (i=1; i<=l; i++) {for (j=1; j<=c; j++) {if (e[j]) printf "%d ",a[i,j] } printf "\n"}}'
        1 0 1 1 
        1 1 1 1 
        0 1 1 1 
        1 1 0 0 
        0 0 1 1 
        

        之前的输入:

        $ cat file 
        1 0 1 1
        0 0 0 0
        1 0 1 0
        0 0 0 0
        1 0 0 1
        $ awk '{for (i=1; i<=NF; i++) {if ($i) {print; next}}}' file | awk '{l=NR; c=NF; for (i=1; i<=c; i++) {a[l,i]=$i; if ($i) e[i]++}} END{for (i=1; i<=l; i++) {for (j=1; j<=c; j++) {if (e[j]) printf "%d ",a[i,j] } printf "\n"}}'
        1 1 1 
        1 1 0 
        1 0 1 
        

        【讨论】:

        • 但是,如果您在 1 0 1 0 1 0 0 0 0 0 1 1 1 0 1 0 1 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0 1 0 1 上运行此脚本结果是 1 0 1 1 1 1 1 1 1 1 0 0 结果应该是 1 0 1 1 1 1 1 1 0 1 1 1 1 1 0 0 0 0 1 1 你能再检查一下吗?谢谢
        • 你能更新你的问题并粘贴这个输入吗?在 cmets 中很难理解。
        • 当然,请参阅我的更新答案。缺少一个大括号。
        【解决方案6】:

        以下脚本也进行了两次传递。在第一次传递期间,它保存要从输出中省略的行号以及应包含在输出中的列索引。在第二遍中,它输出那些行和列。我认为这应该提供接近最小可能的内存占用,如果您正在处理大文件,这可能很重要。

        #!/usr/bin/env perl
        
        use strict;
        use warnings;
        
        filter_zeros(\*DATA);
        
        sub filter_zeros {
            my $fh = shift;
            my $pos = tell $fh;
        
            my %nonzero_cols;
            my %zero_rows;
        
            while (my $line = <$fh>) {
                last unless $line =~ /\S/;
                my @row = split ' ', $line;
                my @nonzero_idx = grep $row[$_], 0 .. $#row;
                unless (@nonzero_idx) {
                    $zero_rows{$.} = undef;
                    next;
                }
                $nonzero_cols{$_} = undef for @nonzero_idx;
            }
        
            my @matrix;
        
            {
                my @idx = sort {$a <=> $b } keys %nonzero_cols;
                seek $fh, $pos, 0;
                local $. = 0;
        
                while (my $line = <$fh>) {
                    last unless $line =~ /\S/;
                    next if exists $zero_rows{$.};
                    print join(' ', (split ' ', $line)[@idx]), "\n";
                }
            }
        }
        
        __DATA__
        1 0 1 0 1
        0 0 0 0 0
        1 1 1 0 1
        0 1 1 0 1
        1 1 0 0 0
        0 0 0 0 0
        0 0 1 0 1
        

        输出:

        1 0 1 1
        1 1 1 1
        0 1 1 1
        1 1 0 0
        0 0 1 1

        【讨论】:

          【解决方案7】:

          有点非正统的解决方案,但速度极快且内存消耗小:

          perl -nE's/\s+//g;$m|=$v=pack("b*",$_);push@v,$v if$v!~/\000/}{$m=unpack("b*",$m);@m=split//,$m;@m=grep{$m[$_]eq"1"}0..$#m;say"@{[(split//,unpack(q(b*),$_))[@m]]}"for@v'
          

          【讨论】:

            【解决方案8】:

            这是我的 awk 解决方案。它适用于可变数量的行和列。

            #!/usr/bin/gawk -f
            
            BEGIN {
                FS = " "
            }
            
            {
                for (c = 1; c <= NF; ++c) {
                    v = $c
                    map[c, NR] = v
                    ctotal[c] += v
                    rtotal[NR] += v
                }
                fields[NR] = NF
            }
            
            END {
                for (r = 1; r <= NR; ++r) {
                    if (rtotal[r]) {
                        append = 0
                        f = fields[r]
                        for (c = 1; c <= f; ++c) {
                            if (ctotal[c]) {
                                if (append) {
                                    printf " " map[c, r]
                                } else {
                                    printf map[c, r]
                                    append = 1
                                }
                            }
                        }
                        print ""
                    }
                }
            }
            

            【讨论】:

              【解决方案9】:

              在我的头顶...

              问题是列。在读入整个文件之前,如何知道一列是否全为零?

              我认为您需要一个列数组,每个数组都是列。您可以推入金额。数组数组。

              诀窍是在阅读时跳过包含全零的行:

              #! /usr/bin/env perl
              #
              use strict;
              use warnings;
              use autodie;
              use feature qw(say);
              use Data::Dumper;
              
              my @array_of_columns;
              for my $row ( <DATA> ) {
                  chomp $row;
                  next if $row =~ /^(0\s*)+$/;  #Skip zero rows;
                  my @columns = split /\s+/, $row;
                  for my $index ( (0..$#columns) ) {
                      push @{ $array_of_columns[$index] }, $columns[$index];
                  }
              }
              
              # Remove the columns that contain nothing but zeros;
              for my $column ( (0..$#array_of_columns) ) {
                  my $index = $#array_of_columns - $column;
                  my $values = join "", @{ $array_of_columns[$index] };
                  if ( $values =~ /^0+$/ ) {
                      splice ( @array_of_columns, $index, 1 );
                  }
              }
              
              say Dumper \@array_of_columns;
              __DATA__
              1 0 1 0 1
              0 0 0 0 0
              1 1 1 0 1
              0 1 1 0 1
              1 1 0 0 0
              0 0 0 0 0
              0 0 1 0 1
              

              当然,您可以使用Array::Transpose,它会转置您的数组,从而使事情变得更容易。

              【讨论】:

                【解决方案10】:

                这是一个真正棘手且具有挑战性的问题..所以为了解决我们也需要变得棘手:) 在我的版本中,我依赖于脚本学习,每次我们阅读新行时,我们都会检查新领域的可能性省略,如果检测到新的变化,我们重新开始。

                检查和重新开始的过程不应如此频繁地重复,因为我们将有几轮 直到我们得到恒定数量的字段要省略或为零,然后我们在特定位置省略每一行的零值。

                #! /usr/bin/env perl
                use strict;
                use warnings;
                use Data::Dumper;
                
                open my $fh, '<', 'file.txt' or die $!;
                
                ##open temp file for output
                open my $temp, '>', 'temp.txt' or die $!;
                
                ##how many field you have in you data
                ##you can increase this by one if you have more fields
                my @fields_to_remove = (0,1,2,3,4);
                
                my $change = $#fields_to_remove;
                
                while (my $line = <$fh>){
                
                    if ($line =~ /1/){
                
                        my @new = split /\s+/, $line;
                        my $i = 0;
                        for (@new){
                            unless ($_ == 0){
                                @fields_to_remove = grep(!/$i/, @fields_to_remove);
                            }
                            $i++;
                        }
                
                        foreach my $field (@fields_to_remove){
                            $new[$field] = 'x';
                        }
                
                        my $new = join ' ', @new;
                        $new =~ s/(\s+)?x//g;
                        print $temp $new . "\n";
                
                        ##if a new change detected start over
                        ## this should repeat for limited time
                        ## as the script keeps learning and eventually stop
                        if ($#fields_to_remove != $change){
                            $change = $#fields_to_remove;
                            seek $fh, 0, 0;
                            close $temp;
                            unlink 'temp.txt';
                            open $temp, '>', 'temp.txt';
                        }
                
                    } else {
                        ##nothing -- removes 0 lines
                    }
                }
                
                ### this is just for showing you which fields has been removed
                print Dumper \@fields_to_remove;
                

                我已经测试了 9 个字段的 25mb 数据文件,它运行良好,虽然速度不是很快,但也没有消耗太多内存。

                【讨论】:

                  【解决方案11】:

                  我使用 grep 和 cut 的紧凑型和大文件兼容替代方案。唯一的缺点:由于 for 循环,对于大文件来说很长。

                  # Remove constant lines using grep
                      $ grep -v "^[0 ]*$\|^[1 ]*$" $fIn > $fTmp
                  
                  # Remove constant columns using cut and wc
                  
                      $ nc=`cat $fTmp | head -1 | wc -w` 
                      $ listcol=""
                      $ for (( i=1 ; i<=$nc ; i++ ))
                      $ do
                      $   nitem=`cut -d" " -f$i $fTmp | sort | uniq | wc -l`
                      $   if [ $nitem -gt 1 ]; then listcol=$listcol","$i ;fi
                      $ done
                      $ listcol2=`echo $listcol | sed 's/^,//g'`
                      $ cut -d" " -f$listcol2 $fTmp | sed 's/ //g' > $fOut
                  

                  【讨论】:

                    【解决方案12】:

                    可以通过这种方式检查行:awk '/[^0[:blank:]]/' file

                    它只是说明如果一行包含任何不同于0 字符的字符,则打印该行

                    如果你现在想检查列,那么我建议改编Glenn Jackman's answer

                    awk '
                        NR==1   {for (i=1; i<=NF; i++) if ($i == 0) zerocol[i]=1; next} 
                        NR==FNR {for (idx in zerocol) if ($idx) delete zerocol[idx]; next}
                        /[^0[:blank:]]/ {for (i=1; i<=NF; i++) if (i in zerocol) $i=""; print}
                    ' file file
                    

                    【讨论】:

                    • 不..它没有给出正确的结果..问题的第二部分是删除列 itef
                    • 类似于格伦的回答
                    • @stack0114106 确实有效,只是测试了一下。它确实是相似的,只是稍作修改。正如我在答案本身中提到的那样。
                    猜你喜欢
                    • 2022-01-23
                    • 1970-01-01
                    • 2016-05-26
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2015-11-11
                    • 2014-05-20
                    • 1970-01-01
                    相关资源
                    最近更新 更多