与原始行一起删除重复行
Deleting duplicate lines along with original
我有一个文件,其中每一行都包含模式,其中一些是重复的。我只想要那些不重复的模式。所以,我想删除所有重复数据以及原始模式。我不能使用排序,因为我希望模式具有相同的顺序。
文件:
foo1
foo2
foo3
foo2
foo4
foo1
foo1
foo5
期望的输出:
foo3
foo4
foo5
因为它是一个大文件(大约 1gb),我更喜欢速度很快的东西。提前致谢
一个可能的解决方案是:
$ awk 'NR==FNR{++seen[[=10=]];next}seen[[=10=]]==1' file file
foo3
foo4
foo5
它读取文件两次,第一次保留每行出现的总次数,第二次打印唯一的行。
另一个选项,它使用更多内存但只读取文件一次:
$ awk '{++seen[[=11=]];a[NR]=[=11=]}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
表示只打印与整个模式匹配的行。
如果有很多重复行,这可能会表现良好,
perl -ne'
$h{$_}++ or push @r,$_;
END {
$h{$_} <2 and print for @r
}
' file
它遍历文件并在 %h
散列中存储相同行的计数,同时用唯一行填充 @r
数组。在文件处理结束时,它通过 @r
循环并仅打印出现次数少于两次的行。
最简单的方法是遍历文件两次,计算一行第一次出现的频率,并在第二次遍历时打印遇到的唯一行。
如果你有足够的 RAM(这会占用相当多的内存),你可以使用
awk 'NR == FNR { seen[[=10=]]++; next } seen[[=10=]] == 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 [=11=] q ", 0, " NR ");";
print "UPDATE lines SET counter = counter + 1 WHERE line = " q [=11=] q ";"
}
END {
print "SELECT line FROM lines WHERE counter = 1 ORDER BY nr;"
}' file | sqlite3 lcount.db
令人惊讶的是,这仍然相当快。它有多快再次取决于您的可用 RAM -- sqlite 进程将只使用几兆字节,但速度在很大程度上取决于数据库文件的文件系统缓存的可用 space。
请注意,我对 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
你的问题的核心是这个 - 因为你需要删除原始文件,直到你知道它是一个骗局,你必须将它保存在内存中直到整个文件被解析。
有两种方法可以从根本上做到这一点 - 将整个内容存储在内存中或从磁盘读取文件两次。
因此在 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
以删除空格。
Perl 解决方案。该程序期望输入文件的路径作为命令行上的参数
您问题中的数据具有可变数量的尾随空格。我假设您 不需要 在比较它们之前 trim 这些
1GB对于一个文件来说不算大,最快的处理方式是读入内存。该解决方案保留一个散列来建立唯一性,并保留一个数组来维护顺序
use strict;
use warnings;
my (%count, @lines);
$count{$_}++ or push @lines, $_ while <>;
print grep $count{$_} == 1, @lines;
输出
foo3
foo4
foo5
在 Tcl 中解决这个问题的最简单方法是使用字典,因为它们会保留键的插入顺序。特别是 dict incr
和 dict for
非常有用。作为标准输入→标准输出过滤器…
set seen {}
while {[gets stdin line] >= 0} {
dict incr seen $line
}
dict for {line count} $seen {
if {$count == 1} {
puts $line
}
}
这将使用与不同行的数量成比例的内存,并且将只读取一次输入;在 less 中满足问题要求将非常困难,因为在找到一行的重复项之前可能需要读取任意数量的行。
我有一个文件,其中每一行都包含模式,其中一些是重复的。我只想要那些不重复的模式。所以,我想删除所有重复数据以及原始模式。我不能使用排序,因为我希望模式具有相同的顺序。
文件:
foo1
foo2
foo3
foo2
foo4
foo1
foo1
foo5
期望的输出:
foo3
foo4
foo5
因为它是一个大文件(大约 1gb),我更喜欢速度很快的东西。提前致谢
一个可能的解决方案是:
$ awk 'NR==FNR{++seen[[=10=]];next}seen[[=10=]]==1' file file
foo3
foo4
foo5
它读取文件两次,第一次保留每行出现的总次数,第二次打印唯一的行。
另一个选项,它使用更多内存但只读取文件一次:
$ awk '{++seen[[=11=]];a[NR]=[=11=]}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
表示只打印与整个模式匹配的行。
如果有很多重复行,这可能会表现良好,
perl -ne'
$h{$_}++ or push @r,$_;
END {
$h{$_} <2 and print for @r
}
' file
它遍历文件并在 %h
散列中存储相同行的计数,同时用唯一行填充 @r
数组。在文件处理结束时,它通过 @r
循环并仅打印出现次数少于两次的行。
最简单的方法是遍历文件两次,计算一行第一次出现的频率,并在第二次遍历时打印遇到的唯一行。
如果你有足够的 RAM(这会占用相当多的内存),你可以使用
awk 'NR == FNR { seen[[=10=]]++; next } seen[[=10=]] == 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 [=11=] q ", 0, " NR ");";
print "UPDATE lines SET counter = counter + 1 WHERE line = " q [=11=] q ";"
}
END {
print "SELECT line FROM lines WHERE counter = 1 ORDER BY nr;"
}' file | sqlite3 lcount.db
令人惊讶的是,这仍然相当快。它有多快再次取决于您的可用 RAM -- sqlite 进程将只使用几兆字节,但速度在很大程度上取决于数据库文件的文件系统缓存的可用 space。
请注意,我对 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
你的问题的核心是这个 - 因为你需要删除原始文件,直到你知道它是一个骗局,你必须将它保存在内存中直到整个文件被解析。
有两种方法可以从根本上做到这一点 - 将整个内容存储在内存中或从磁盘读取文件两次。
因此在 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
以删除空格。
Perl 解决方案。该程序期望输入文件的路径作为命令行上的参数
您问题中的数据具有可变数量的尾随空格。我假设您 不需要 在比较它们之前 trim 这些
1GB对于一个文件来说不算大,最快的处理方式是读入内存。该解决方案保留一个散列来建立唯一性,并保留一个数组来维护顺序
use strict;
use warnings;
my (%count, @lines);
$count{$_}++ or push @lines, $_ while <>;
print grep $count{$_} == 1, @lines;
输出
foo3
foo4
foo5
在 Tcl 中解决这个问题的最简单方法是使用字典,因为它们会保留键的插入顺序。特别是 dict incr
和 dict for
非常有用。作为标准输入→标准输出过滤器…
set seen {}
while {[gets stdin line] >= 0} {
dict incr seen $line
}
dict for {line count} $seen {
if {$count == 1} {
puts $line
}
}
这将使用与不同行的数量成比例的内存,并且将只读取一次输入;在 less 中满足问题要求将非常困难,因为在找到一行的重复项之前可能需要读取任意数量的行。