如何修剪文件 - 删除具有相同值的列

时间:2011-06-15 19:59:49

标签: perl unix sed awk

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

# 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解决方案。

提前致谢。 最好,

8 个答案:

答案 0 :(得分:5)

这是一个快速的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";

答案 1 :(得分:3)

#!/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);

答案 2 :(得分:3)

如果您知道要提前剥离哪一列,那么cut会有所帮助:

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

答案 3 :(得分:2)

据我所知,您希望浏览每一行并检查某些列中的值是否没有差异,然后在这种情况下您可以删除该列。 如果是这种情况,我有一个建议,但没有现成的脚本,但我认为你将能够弄明白。你应该看看cut。它提取部分线条。您可以使用它来提取第一列,然后对输出的数据运行uniq,然后如果在唯一后只有一个值,则表示该列中的所有值都相同。这样,您可以收集没有差异的列数。您将需要shell脚本来查看您提交的列数(我猜是使用head -n 1并计算分隔符数)并在每个列上运行此类过程,将列号存储在数组中,然后在最后制作切割线到删除不感兴趣的列。虽然它不是awk或perl但应该工作,并且只使用传统的Unix工具。那么你可以在perl脚本中使用它们,如果你想:)

好吧,如果我误解了这个问题可能会被削减仍然有用:)它似乎是鲜为人知的工具之一。

答案 4 :(得分:1)

据我所知,你需要让它成为一个多程序程序,以满足你的需求而不会耗费内存。对于初学者,将文件的一行加载到数组中。

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 :(得分:1)

您可以选择要剪切的列

# 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 :(得分:1)

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

#!/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

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

答案 7 :(得分:1)

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

当然,您可以随时减少列数,但仍然需要检查每一列到最后一行。所以......很多处理。

我们可以从两个第一行开始制作“种子”哈希:

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;
}

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

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