如何使用Perl从大文件中删除非唯一行?

3
使用Perl进行重复数据删除,通过Windows批处理文件调用。在Windows中调用DOS窗口的批处理文件。批处理文件调用Perl脚本执行操作。我有批处理文件。所提供的代码脚本可以删除重复数据,但只适用于数据文件不太大的情况。需要解决的问题是对于更大的数据文件(2 GB或更大),当尝试将整个文件加载到数组中以删除重复数据时,会发生内存错误。内存错误发生在子例程中:-
@contents_of_the_file = <INFILE>;

(只要解决此问题,完全不同的方法也是可接受的,请提出建议。) 子程序是:

sub remove_duplicate_data_and_file
{
 open(INFILE,"<" . $output_working_directory . $output_working_filename) or dienice ("Can't open $output_working_filename : INFILE :$!");
  if ($test ne "YES")
   {
    flock(INFILE,1);
   }
  @contents_of_the_file = <INFILE>;
  if ($test ne "YES")
   {
    flock(INFILE,8);
   }
 close (INFILE);
### TEST print "$#contents_of_the_file\n\n";
 @unique_contents_of_the_file= grep(!$unique_contents_of_the_file{$_}++, @contents_of_the_file);

 open(OUTFILE,">" . $output_restore_split_filename) or dienice ("Can't open $output_restore_split_filename : OUTFILE :$!");
 if ($test ne "YES")
  {
   flock(OUTFILE,1);
  }
for($element_number=0;$element_number<=$#unique_contents_of_the_file;$element_number++)
  {
   print OUTFILE "$unique_contents_of_the_file[$element_number]\n";
  }
 if ($test ne "YES")
  {
   flock(OUTFILE,8);
  }
}
6个回答

6
您不必要地在@contents_of_the_file中存储原始文件的完整副本,并且如果相对于文件大小,重复量较低,则还会在%unique_contents_of_the_file@unique_contents_of_the_file中存储近乎两个完整的副本。正如ire_and_curses所指出的那样,您可以通过对数据进行两次处理来减少存储需求:(1)分析文件,存储非重复行的行号信息; (2)再次处理文件以将非重复项写入输出文件。
这是一个示例。我不知道是否选择了最佳的哈希函数模块(Digest::MD5),也许其他人会对此发表评论。还请注意使用open()的三参数形式。
use strict;
use warnings;

use Digest::MD5 qw(md5);

my (%seen, %keep_line_nums);
my $in_file  = 'data.dat';
my $out_file = 'data_no_dups.dat';

open (my $in_handle, '<', $in_file) or die $!;
open (my $out_handle, '>', $out_file) or die $!;

while ( defined(my $line = <$in_handle>) ){
    my $hashed_line = md5($line);
    $keep_line_nums{$.} = 1 unless $seen{$hashed_line};
    $seen{$hashed_line} = 1;
}

seek $in_handle, 0, 0;
$. = 0;
while ( defined(my $line = <$in_handle>) ){
    print $out_handle $line if $keep_line_nums{$.};
}    

close $in_handle;
close $out_handle;

2
只要被哈希的行长度大于16个字符,这将是一个胜利。如果行长度小于16,则使用行本身作为%seen键。my $hashed_line = length($line) > 15 ? md5($line) : $line;就可以解决问题了。另外,可以考虑使用Bit::Vector替换%keep_line_num以减少内存占用。 - dland

4
你可以使用哈希算法高效地完成这项任务。你不需要存储每行数据,只需要识别哪些行是相同的即可。因此,你需要:
  • 逐行读取,不要一次性读入所有数据。
  • 对每行进行哈希处理。
  • 将哈希后的行表示作为Perl哈希表中列表的键。将行号作为该列表的第一个值存储。
  • 如果键已经存在,则将重复的行号添加到相应值的列表中。
在这个过程结束后,你将得到一个数据结构来标识所有重复的行。然后,你可以再次遍历文件,删除这些重复的行。

我赞同这个想法。但是,除非我漏掉了什么,将重复信息存储为列表哈希似乎对于第二遍数据处理来说不太方便,因为无法快速知道是否要打印该行。似乎更方便的方法是建立一个 Perl 哈希,以所需行号作为哈希键。 - FMc
@FM:是的,我理解你的观点。我试图避免使用第二个行号哈希表以减少内存使用,但是与你的解决方案相比,从我的表示中重建文件相当复杂。我更喜欢你的方法。 ;) - ire_and_curses

2

Perl在处理大文件方面表现英勇,但2GB可能是DOS / Windows的限制。

您有多少RAM?

如果您的操作系统没有抱怨,最好一次读取一行文件,并立即写入输出。

我考虑使用钻石操作符<>来实现某些功能,但我不愿意提供任何代码,因为在我发布代码的场合中,我曾经冒犯了Perl大师们。

我宁愿不冒险。 我希望Perl骑兵很快就会到来。

同时,这里有一个链接。


2
无论操作系统是否抱怨,读取一个2GB的文件总是一个坏主意。 - Matthew Scharley
1
Pavium,不要担心冒犯Perl大师。这是学习的好方法,如果有人评论,那不是你的问题,而是你的代码。这是两码事。Perl的座右铭之一是“玩得开心”。 - dland

1

这里有一个解决方案,无论文件大小如何都可以使用。但它不完全使用 RAM,因此比基于 RAM 的解决方案慢。您还可以指定要使用的 RAM 量。

该解决方案使用一个临时文件,程序将其视为 SQLite 数据库。

#!/usr/bin/perl

use DBI;
use Digest::SHA 'sha1_base64';
use Modern::Perl;

my $input= shift;
my $temp= 'unique.tmp';
my $cache_size_in_mb= 100;
unlink $temp if -f $temp;
my $cx= DBI->connect("dbi:SQLite:dbname=$temp");
$cx->do("PRAGMA cache_size = " . $cache_size_in_mb * 1000);
$cx->do("create table x (id varchar(86) primary key, line int unique)");
my $find= $cx->prepare("select line from x where id = ?");
my $list= $cx->prepare("select line from x order by line");
my $insert= $cx->prepare("insert into x (id, line) values(?, ?)");
open(FILE, $input) or die $!;
my ($line_number, $next_line_number, $line, $sha)= 1;
while($line= <FILE>) {
  $line=~ s/\s+$//s;
  $sha= sha1_base64($line);
  unless($cx->selectrow_array($find, undef, $sha)) {
    $insert->execute($sha, $line_number)}
  $line_number++;
}
seek FILE, 0, 0;
$list->execute;
$line_number= 1;
$next_line_number= $list->fetchrow_array;
while($line= <FILE>) {
  $line=~ s/\s+$//s;
  if($next_line_number == $line_number) {
    say $line;
    $next_line_number= $list->fetchrow_array;
    last unless $next_line_number;
  }
  $line_number++;
}
close FILE;

0
在“完全不同的方法”类别中,如果您有Unix命令(例如Cygwin):
cat infile | sort | uniq > outfile

这应该可以解决你的内存问题,无需使用Perl-但可能会失去infile的顺序(因为outfile现在将被排序)。

编辑:更好地处理大文件的替代解决方案可能是使用以下算法:

  1. 逐行读取INFILE
  2. 将每行哈希到小哈希表中(例如哈希#模10)
  3. 将每行追加到唯一的文件中,特定于哈希编号(例如tmp-1到tmp-10)
  4. 关闭INFILE
  5. 打开并将每个tmp-#排序到新文件sortedtmp-#中
  6. 合并排序sortedtmp-[1-10](即打开所有10个文件并同时读取它们),跳过重复项并将每次迭代写入最终输出文件

对于非常大的文件,这将比读入整个文件更安全。

第2和第3部分可以更改为随机#而不是哈希编号mod 10。

这里有一个脚本BigSort可能会有所帮助(虽然我没有测试过):

# BigSort
#
# sort big file
#
# $1 input file
# $2 output file
#
# equ   sort -t";" -k 1,1 $1 > $2

BigSort()
{
if [ -s $1 ]; then
  rm $1.split.* > /dev/null 2>&1
  split -l 2500 -a 5 $1 $1.split.
  rm $1.sort > /dev/null 2>&1
  touch $1.sort1
  for FILE in `ls $1.split.*`
  do
    echo "sort $FILE"
    sort -t";" -k 1,1 $FILE > $FILE.sort
    sort -m -t";" -k 1,1 $1.sort1 $FILE.sort > $1.sort2
    mv $1.sort2 $1.sort1
  done
  mv $1.sort1 $2
  rm $1.split.* > /dev/null 2>&1
else
  # work for empty file !
  cp $1 $2
fi
} 

排序无法在没有整个文件可用于处理的情况下工作,因此这将遭受与OP原始示例相同的内存问题。尽管如此,在许多相关情况下,这仍是一个有用的解决方案,我不会对此评分。 - ire_and_curses

0

你可以使用命令行 Perl 的内联替换模式。

perl -i~ -ne 'print unless $seen{$_}++' uberbigfilename

1
你仍然想要将整个文件的内容存储在RAM中,这是最初的问题。 - David Precious

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接