【问题标题】:how to trim file - remove the columns with the same value如何修剪文件 - 删除具有相同值的列
【发布时间】:2011-09-15 20:31:21
【问题描述】:

我希望通过删除具有相同值的列来帮助您修剪文件。

# the file I have (tab-delimited, millions of columns)
jack 1 5 9
john 3 5 0
lisa 4 5 7

# the file I want (remove the columns with the same value in all lines)
jack 1 9
john 3 0
lisa 4 7

你能给我关于这个问题的任何指示吗?我更喜欢 sed 或 awk 解决方案,或者 perl 解决方案。

提前致谢。 最好的,

【问题讨论】:

  • 因此,当且仅当它在整个文件中永远不会更改时,您想删除一列?而且你不知道这些是预先的哪些列?
  • 您的意思是实际的数百万,还是只是一个比喻?因为检查超过 1,000,000 列,在未指定数量的行中检查相同的值将是一项非常具有挑战性的任务。
  • 亲爱的克里斯托弗,是的,我不知道提前哪些专栏。
  • 你确定吗,百万记录中是否有这样的列?
  • 一个包含数百万列的文件感觉不是一个好的设计。也许您应该开始考虑其他方法?你在建什么?

标签: perl unix sed awk


【解决方案1】:
#!/usr/bin/perl
$/="\t";
open(R,"<","/tmp/filename") || die;
while (<R>)
{
  next if (($. % 4) == 3);
  print;
}

嗯,这是假设它是第三列。如果是按值计算:

#!/usr/bin/perl
$/="\t";
open(R,"<","/tmp/filename") || die;
while (<R>)
{
  next if (($_ == 5);
  print;
}

通过问题编辑,OP 的愿望变得清晰。怎么样:

#!/usr/bin/perl
open(R,"<","/tmp/filename") || die;
my $first = 1;
my (@cols);
while (<R>)
{
  my (@this) = split(/\t/);
  if ($. == 1)
  {
    @cols = @this;
  }
  else
  {
    for(my $x=0;$x<=$#cols;$x++)
    {
      if (defined($cols[$x]) && !($cols[$x] ~~ $this[$x]))
      {
        $cols[$x] = undef;
      }
    }
  }
  next if (($_ == 5));
#  print;
}
close(R);
my(@del);
print "Deleting columns: ";
for(my $x=0;$x<=$#cols;$x++)
{
  if (defined($cols[$x]))
  {
    print "$x ($cols[$x]), ";
    push(@del,$x-int(@del));
  }
}
print "\n";

open(R,"<","/tmp/filename") || die;
while (<R>)
{
  chomp;
  my (@this) = split(/\t/);

  foreach my $col (@del)
  {
    splice(@this,$col,1);
  }

  print join("\t",@this)."\n";
}
close(R);

【讨论】:

  • 如果值不是5?他确实说过“数百万”列。
  • 在他/某人格式化问题之前,问题的解释可能完全不同。在新的布局和对问题的新澄清中,它需要多次通过和很多痛苦。
【解决方案2】:

据我了解,您希望遍历每一行并检查某列中的值是否没有差异,然后在这种情况下您可以删除该列。 如果是这种情况,我有一个建议,但不是现成的脚本,但我认为您将能够弄清楚。你应该看看cut。它提取部分行。您可以使用它来提取即第一列,然后对输出的数据运行uniq,然后如果唯一之后只有一个值,则意味着该列中的所有值都是相同的。这样,您可以收集没有差异的列数。您将需要 shell 脚本来查看您的文件有多少列(我猜使用head -n 1 并计算分隔符的数量)并在每一列上运行这样的过程,将列号存储在数组中,然后最后制作切割线以删除列没有兴趣的。授予它不是 awk 或 perl 但应该可以工作,并且只使用传统的 Unix 工具。如果你愿意,你可以在 perl 脚本中使用它们:)

好吧,如果我误解了这个问题,也许 cut 仍然有用:) 它似乎是鲜为人知的工具之一。

【讨论】:

    【解决方案3】:

    如果您知道要提前删除哪一列,那么cut 会很有帮助:

    cut --complement -d' ' -f 3 filename
    

    【讨论】:

      【解决方案4】:

      据我所知,您需要将此程序设为多通道程序以满足您的需求,而不会耗尽内存。对于初学者,将文件的单行加载到数组中。

      open FH,'datafile.txt' or die "$!";
      my @mask;
      my @first_line= split(/\s+/,<FH>);
      

      然后你会想顺序阅读其他行

      while(my @next_line= split(/\s+/,<FH>)) {
      /* compare each member of @first_line to @next_line
       * any match, make a mark in mask to true
       */
      

      当您到达文件底部时,返回顶部并使用掩码确定要打印的列。

      【讨论】:

        【解决方案5】:

        您可以选择要剪切的列

        # using bash/awk
        # I had used 1000000 here, as you had written millions of columns but you should adjust it
        for cols in `seq 2 1000000` ; do
            cut -d DELIMITER -f $cols FILE | awk -v c=$cols '{s+=$0} END {if (s/NR==$0) {printf("%i,",c)}}'
        done | sed 's/,$//' > tmplist
        cut --complement -d DELIMITER -f `cat tmplist` FILE
        

        但它可能真的很慢,因为它没有经过优化,并且会多次读取文件......所以要小心大文件。

        或者您可以使用 awk 读取整个文件一次并选择可转储列,然后使用 cut。

        cut --complement -d DELIMITER -f `awk '{for (i=1;i<=NF;i++) {sums[i]+=$i}} END {for (i=1;i<=NF; i++) {if (sums[i]/NR==$i) {printf("%i,",c)}}}' FILE | sed 's/,$//'` FILE
        

        HTH

        【讨论】:

          【解决方案6】:

          这是一个快速的 perl 脚本,用于确定可以剪切哪些列。

          open FH, "file" or die $!;
          my @baseline = split /\t/,<FH>;         #snag the first row
          my @linemap = 0..$#baseline;            #list all equivalent columns (all of them)
          
          while(<FH>) {                           #loop over the file
              my @line = split /\t/;
              @linemap = grep {$baseline[$_] eq $line[$_]}  @linemap; #filter out any that aren't equal
          }
          print join " ", @linemap;
          print "\n";
          

          您可以使用上述许多建议来实际删除列。我最喜欢的可能是 cut 实现,部分原因是上面的 perl 脚本可以修改为您提供精确的命令(甚至为您运行它)。

          @linemap = map {$_+1} @linemap;                   #Cut is 1-index based
          print "cut --complement -f ".join(",",@linemap)." file\n";
          

          【讨论】:

          • 我写了一个包含 2M 列的快速示例文件,开始时前几行相同,然后线性递减,直到只有 10% 的文件匹配。在我不是很强大的桌面上,前几行似乎需要大约 1.3 秒/行,随着 @linemap 数组的缩小而迅速减少。
          • 我还做了一些尝试切割列,使用cut 实用程序比在 perl 中进行过滤要快几个数量级(即使您需要多次通过)
          【解决方案7】:

          尚未完全测试,但这似乎适用于提供的测试集,请注意它会破坏原始文件...

          #!/bin/bash
          
          #change 4 below to match number of columns
          for i in {2..4}; do
              cut -f $i input | sort | uniq -c > tmp
              while read a b; do
                  if [ $a -ge 2 ]; then
                      awk -vfield=$i '{$field="_";print}' input > tmp2
                      $(mv tmp2 input)
                  fi
              done < tmp
          done
          
          $ cat input
          jack    1   5   9
          john    3   5   0
          lisa    4   5   7
          
          $ ./cnt.sh 
          
          $ cat input
          jack 1 _ 9
          john 3 _ 0
          lisa 4 _ 7
          

          使用_ 让输出更清晰...

          【讨论】:

            【解决方案8】:

            这里的主要问题是您说“百万列”,并没有指定多少行。为了检查每一行中的每个值与其他列中的对应值..您正在查看大量检查。

            当然,您可以在执行过程中减少列数,但您仍需要逐一检查直到最后一行。如此...大量处理。

            我们可以从前两行开始创建一个“种子”哈希:

            use strict;
            use warnings;
            
            open my $fh, '<', "inputfile.txt" or die;
            my %matches;
            my $line = <$fh>;
            my $nextline = <$fh>;
            my $i=0;
            while ($line =~ s/\t(\d+)//) {
                my $num1 = $1;
                if ($nextline =~ s/\t(\d+)//) {
                   if ($1 == $num1) { $matches{$i} = $num1 }
                } else {
                   die "Mismatched line at line $.";
                }
                $i++;
            }
            

            然后使用这个“种子”哈希,您可以读取其余行,并从哈希中删除不匹配的值,例如:

            while($line = <$fh>) {
                my $i = 0;
                while ($line =~ s/\t(\d+)//) {
                    if (defined $matches{$i}) {
                        $matches{$i} = undef if ($matches{$i} != $1);
                    }
                    $i++;
                }
            }
            

            您可以想象一种解决方案,即剥离所有已被证明是唯一的行,但为了做到这一点,您需要制作该行的数组,或者制作一个正则表达式,我不确定这不会像简单地通过字符串一样长。

            然后,在处理完所有行之后,您将得到一个包含重复数字值的哈希值,因此您可以重新打开文件并进行打印:

            open my $fh, '<', "inputfile.txt" or die;
            open my $outfile, '>', "outfile.txt" or die;
            while ($line = <$fh>) {
                my $i = 0;
                if ($line =~ s/^([^\t]+)(?=\t)//) {
                    print $outfile $1;
                } else { warn "Missing header at line $.\n"; }
                while ($line =~ s/(\t\d+)//) {
                    if (defined $matches{$i}) { print $1 }
                    $i++;
                }
                print "\n";
            }
            

            这是一个相当繁重的操作,并且此代码未经测试。这将为您提供解决方案的提示,处理整个文件可能需要一段时间。我建议运行一些测试以查看它是否适用于您的数据,然后对其进行调整。

            如果您只有几个匹配的列,则简单地从行中提取它们要容易得多,但我不愿在这么长的行上使用 split。比如:

            while ($line = <$fh>) {
                my @line = split /\t/, $line;
                for my $key (sort { $b <=> $a } keys %matches) {
                    splice @line, $key + 1, 1;
                }
                $line = join ("\t", @line);
                $line =~ s/\n*$/\n/; # awkward way to make sure to get a single newline
                print $outfile $line;
            }
            

            请注意,我们必须按数字降序对键进行排序,以便我们从末尾修剪值。否则我们会搞砸后续数组编号的唯一性。

            无论如何,这可能是一种方法。不过,这是一个相当大的操作。我会保留备份。 ;)

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2022-12-16
              • 1970-01-01
              • 2021-12-26
              • 2022-08-11
              • 2022-12-17
              • 2017-11-07
              相关资源
              最近更新 更多