删除重复的行和原始行

时间:2015-05-29 10:10:11

标签: perl awk sed grep tcl

我有一个文件,其中包含每行的图案,其中一些是重复的。我只想要那些重复的模式。所以,我想删除所有重复数据和原始模式。我不能使用sort,因为我希望模式的顺序相同。

文件:

foo1  
foo2   
foo3  
foo2  
foo4  
foo1  
foo1  
foo5

期望的输出:

foo3   
foo4  
foo5

由于它是一个大文件(大约1GB),我更喜欢一些东西。提前致谢

6 个答案:

答案 0 :(得分:4)

最简单的方法是遍历文件两次,计算第一次出现的行数,并在遇到第二次过程中打印唯一行。

如果你有足够的RAM(这需要相当多的时间),你可以使用

awk 'NR == FNR { seen[$0]++; next } seen[$0] == 1' file file

这需要多少内存取决于文件中行的平均长度。如果行非常短,则哈希映射的开销将使内存使用量远远超过纯输入数据所需的1GB。我最近有一个类似的用例,其中 awk最终使用超过8GB的RAM用于~300 MB的输入数据其中行平均长度约为8个字符。用C ++重写代码使问题不那么严重,但仍然不切实际。

我们最终解决了sqlite的问题,RAM的交易速度。对于您的用例,这可能最终为

rm lcount.db
awk -v q=\' '
  NR == 1 {
    print "CREATE TABLE lines (line text PRIMARY KEY, counter INTEGER, nr INTEGER);"
  }
  {
    sub(q, q q);  # hacky way to sanitize lines with quotes in them
    print "INSERT OR IGNORE INTO lines VALUES (" q $0 q ", 0, " NR ");";
    print "UPDATE lines SET counter = counter + 1 WHERE line = " q $0 q ";"
  }
  END {
    print "SELECT line FROM lines WHERE counter = 1 ORDER BY nr;"
  }' file  | sqlite3 lcount.db

令人惊讶的是,这仍然相当快。它的速度有多快取决于你的可用RAM - sqlite进程只会使用几兆字节,但速度在很大程度上取决于文件系统缓存数据库文件的可用空间。

请注意,我对SQL卫生设施并不十分满意; 如果输入数据来自不值得信赖的来源,我不相信它是完全安全的。如果这是一个担心,您可以使用以下内容:

perl -MDBI -e'
   my $dbh = DBI->connect("dbi:SQLite:dbname=lcount.db", "", "", { PrintError=>0, RaiseError=>1 });
   $dbh->do("CREATE TABLE lines (line TEXT PRIMARY KEY, counter INTEGER, nr INTEGER)");

   my $ins_sth = $dbh->prepare("INSERT OR IGNORE INTO lines VALUES (?, 0, ?)");
   my $upd_sth = $dbh->prepare("UPDATE lines SET counter = counter + 1 WHERE line = ?");
   while (<>) {
      $ins_sth->execute($_, $.);
      $upd_sth->execute($_);
   }

   my $sth = $dbh->prepare("SELECT line FROM lines WHERE counter = 1 ORDER BY nr");
   print while ($_) = $sth->fetchrow_array;
' file

答案 1 :(得分:2)

一种可能的解决方案是:

$ awk 'NR==FNR{++seen[$0];next}seen[$0]==1' file file
foo3
foo4
foo5

它读取文件两次,第一次保留每行的出现次数,并第二次打印唯一的行。

另一个选项,它使用更多内存但只读取文件一次:

$ awk '{++seen[$0];a[NR]=$0}END{for(i=1;i<=NR;++i)if(seen[a[i]]==1)print a[i]}' file
foo3
foo4
foo5

这也将每一行存储在数组a中,因此不是重新读取文件,而是可以使用循环来打印唯一的行。

我不确定这是如何工作的(我想内存要求可能类似)但你也可以使用一些标准工具:

$ sort file | uniq -u | grep -Fxf - file
foo3
foo4
foo5

sort file | uniq -u获取唯一的行并将它们作为要匹配的模式列表传递给grep。 -F开关与固定字符串匹配,-x表示仅打印与整个模式匹配的行。

答案 2 :(得分:1)

如果有很多重复的行,这可能会很好,

perl -ne'
  $h{$_}++ or push @r,$_;
  END {
    $h{$_} <2 and print for @r
  }
' file

它循环遍历文件并在%h哈希中存储相同行的计数,同时用唯一行填充@r数组。在文件处理结束时,它循环通过@r并仅打印少于两次的行。

答案 3 :(得分:1)

Perl解决方案。此程序需要输入文件的路径作为命令行上的参数

您问题中的数据具有可变数量的尾随空格。我假设你 需要在比较它们之前修剪它们

1GB对于文件来说并不是那么大,处理它的最快方法是将其读入内存。此解决方案保持哈希以建立唯一性,并保持数组以维持顺序

use strict;
use warnings;

my (%count, @lines);
$count{$_}++ or push @lines, $_ while <>;
print grep $count{$_} == 1, @lines;

<强>输出

foo3
foo4
foo5

答案 4 :(得分:0)

问题的核心是这个 - 因为你需要删除原文,直到你知道它是一个骗局,你必须把它保存在内存中,直到整个文件被解析。

有两种方法可以从根本上做到这一点 - 将整个内容存储在内存中或从磁盘中读取文件两次。

所以在perl中 - 读入内存(由于开销,将使用原始文件大小的多个)。

#!/usr/bin/perl

use strict;
use warnings; 

open ( my $input_fh, "<", "data_file_name" ) or die $!;
my @data = <$input_fh>; 
close ( $input_fh ):

my %count_of;
$count_of{$_}++ for @data;

foreach my $line ( @data ) {
   print $line if $count_of{$line} <= 1;
}

两次读取文件 - 需要更长时间,因为磁盘IO,但内存使用率较低(取决于有多少重复项)。

#!/usr/bin/perl

use strict;
use warnings;

open( my $input_fh, "<", "data_file_name" ) or die $!;
my %count_of;
$count_of{$_}++ for <$input_fh>;

seek( $input_fh, 0, 0 );    #rewind - could close/reopen instead.
foreach my $line (<$input_fh>) {
    print $line if $count_of{$line} <= 1;
}
close($input_fh);

注意 - 在上面两个中,我们使用字面意思 - 包括空格和换行符。因此:"foo ""foo"将被视为不同。您可以通过&#34; sed like&#34;轻松地处理这个问题。搜索并替换等s/\s+//g以删除空格。

答案 5 :(得分:0)

在Tcl中解决此问题的最简单方法是使用字典,因为它们保留了键的插入顺序。特别是dict incrdict for非常有用。作为stdin→stdout过滤器......

set seen {}
while {[gets stdin line] >= 0} {
    dict incr seen $line
}
dict for {line count} $seen {
    if {$count == 1} {
        puts $line
    }
}

这将使用与不同行的数量成比例的存储器,并且将仅读取输入一次;要在较少的情况下满足问题要求是非常困难的,因为在找到一条线的副本之前可能有任意数量的行要读取。