2009-09-24 118 views
3

使用在Windows中通过批处理文件调用的Perl删除重复数据删除 Windows中的DOS窗口通过批处理文件调用。 批处理文件调用执行操作的Perl脚本。我有批处理文件。 我的工作重复数据的代码脚本是删除,只要数据文件不是太大。 需要解决的问题是数据文件较大(2 GB或更多),如果尝试将完整文件加载到数组以删除重复数据,则会发生此文件大小的内存错误。 在子程序时发生存储器错误: -如何从Perl的大文件中删除非唯一的行?

@contents_of_the_file = <INFILE>; 

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

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

您不必要的存储原始文件的完整副本@contents_of_the_file和 - 如果重复量相对较低的文件大小 - %unique_contents_of_the_file@unique_contents_of_the_file近两年其他完整副本。正如ire_and_curses指出的那样,您可以通过对数据进行两次传递来降低存储要求:(1)分析文件,存储有关非重复行的行数的信息;和(2)再次处理该文件以向输出文件写入非牟取文件。

下面是一个例子。我不知道我是否选择了散列函数的最佳模块(Digest::MD5);也许其他人会对此发表评论。还请注意您应该使用的3参数形式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; 
+1

+1用于实际构建代码。 – 2009-09-24 17:43:07

+2

只要被哈希的行是16个字符或更大,这将是一个胜利。如果行长度小于16,则使用该行本身而不是'%seen'键。 my $ hashed_line = length($ line)> 15? md5($ line):$ line; 将做的伎俩。另请参阅'Bit :: Vector'作为'%keep_line_num'的替代,以减少内存占用。 – dland 2009-09-26 17:15:55

2

Perl并英勇事大文件,但2GB的可能是DOS/Windows的的限制。

你有多少RAM?

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

我想使用钻石运算符< >但我不愿意推荐任何代码,因为在我发布代码的时候,我冒犯了SO上的Perl专家。

我宁愿不冒险。我希望佩尔骑兵马上就会到来。

与此同时,here's的一个链接。

+2

无论操作系统是否抱怨或其他原因,嗅探2GB文件总是一个糟糕的主意。 – 2009-09-24 11:22:00

+0

你可以在我的代码中提出修改吗? – 2009-09-24 11:24:53

+1

pavium,不用担心冒犯Perl大师。这是学习的好方法,如果人们评论,那不是你,而是你的代码。不是一回事。 Perl的格言之一是“玩得开心”。 – dland 2009-09-26 17:18:57

4

你应该能够使用哈希高效地做到这一点。您不需要存储来自行的数据,只需确定哪些是相同的。所以...

  • 不要sl - - 一次只读一行。
  • 哈希线。
  • 将散列行表示形式存储为列表的Perl哈希中的键。将行号存储为列表的第一个值。
  • 如果该键已经存在,请将重复的行号附加到与该值对应的列表中。

在这个过程结束时,您将拥有一个标识所有重复行的数据结构。然后您可以再次通过文件来删除这些重复项。

+0

+1为一般的想法。但是,除非我忽略了某些事情,否则将dup信息存储为列表的散列似乎并不方便,只要第二次传递数据 - 没有快速的方法来知道是否打印该行。似乎更加容易,用想要的行号建立一个Perl散列作为散列键。 – FMc 2009-09-24 16:35:49

+0

@FM:是的,我明白你的意思。我试图避免使用行号的第二个散列来减少内存使用量,但我的权衡是,与您的解决方案相比,从我的表示中重建文件相当复杂。我更喜欢你的方法。 ;) – 2009-09-24 17:42:31

0

在 “完全不同的方法” 一类,如果你有Unix命令(如Cygwin的):

cat infile | sort | uniq > outfile 

这应该工作 - 没有必要的Perl在所有 - 这可能,或者可能不解决你的记忆问题。但是,您将失去infile的排序(因为outfile现在将被排序)。

编辑:另一种解决方案,它能够更好地处理大文件可能是通过使用以下算法:

  1. 读INFILE行由行
  2. 哈希每行一个小散(例如散列#MOD 10)
  3. 追加每一行唯一的散列数目的文件(例如TMP-1至TMP-10)
  4. 关闭INFILE
  5. 打开和排序每个tmp-#到一个新文件sortedtmp-#
  6. Mergesort sortedtmp- [1-10](即打开所有10个文件并同时读取它们),跳过重复项并将每次迭代写入最终输出文件

对于非常大的文件,这会比sl sa更安全。

零件2 & 3可以更改为随机#,而不是一个哈希数模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 
} 
+0

如果没有可用的整个文件进行处理,排序无法正常工作,因此会遭遇与OP原始示例相同的内存问题。尽管如此,我还没有减去,因为它在很多相关情况下都是有用的解决方案。 – 2009-09-24 19:13:16

0

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

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

尽管如此,您仍然希望将文件的全部内容存储在RAM中,这是最初的问题。 – 2009-09-25 16:04:11

+0

非常好的一点。 – Scimon 2009-10-02 12:44:20

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;