【问题标题】:Deleting duplicate lines along with original删除重复行和原始行
【发布时间】:2015-08-12 03:52:17
【问题描述】:

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

文件:

foo1  
foo2   
foo3  
foo2  
foo4  
foo1  
foo1  
foo5

期望的输出:

foo3   
foo4  
foo5

由于它是一个大文件(大约 1gb),我更喜欢速度非常快的文件。提前致谢

【问题讨论】:

  • 你不喜欢为这项任务获得特别快的东西。而且你最好有很多内存。
  • 您可以将所有行加载到哈希中,其中键是行内容。值是出现次数。然后提取值等于 1 的所有键,即唯一键。
  • @Kim 这种方法的问题是订单丢失了。
  • 好点。您可以为每个键添加一个 couter,因此它变为 1:foo1, 2:foo2 。因此,订单被保留,因为您现在可以按键排序。只需要在检索上拆分出数字部分。
  • @valuable_asset:文件中记录的典型长度是多少?您是否需要忽略尾随空格,因为您的示例包含不同数量的空格字符?

标签: perl awk sed grep tcl


【解决方案1】:

最简单的方法是遍历文件两次,计算第一次出现一行的频率,并在第二次遇到唯一的行时打印它们。

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

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

【讨论】:

  • 一个很好的创造性解决方案!
【解决方案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 表示只打印与整个模式匹配的行。

【讨论】:

  • 我无法排序。我要求模式是按顺序排列的。
  • @valuable_asset grep 的输入已排序,但输出未排序,所以这不是问题。
【解决方案3】:

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

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

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

【讨论】:

    【解决方案4】:

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

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

    1GB 对于一个文件来说并不算大,处理它的最快方法是将它读入内存。该解决方案保留一个哈希来建立唯一性和一个数组来维护顺序

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

    输出

    foo3
    foo4
    foo5
    

    【讨论】:

      【解决方案5】:

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

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

      所以在 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" 将被认为是不同的。您可以通过“sed like”搜索和替换等很容易地处理这个问题。s/\s+//g 删除空格。

      【讨论】:

        【解决方案6】:

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

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

        这将使用与不同行数成正比的内存,并且将只读取一次输入;用更少的时间满足问题要求将非常困难,因为在找到重复行之前可能需要读取任意数量的行。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-11-16
          • 2022-11-18
          • 1970-01-01
          • 2019-08-04
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多