Perl使用主键逐行合并2个csv文件
编辑:添加了解决方案 您好,我目前有一些工作,虽然缓慢的代码 它使用主键逐行合并2个CSV文件。 例如,如果文件1有以下行:Perl使用主键逐行合并2个csv文件,perl,Perl,编辑:添加了解决方案 您好,我目前有一些工作,虽然缓慢的代码 它使用主键逐行合并2个CSV文件。 例如,如果文件1有以下行: "one,two,,four,42" "one,two,three,four,42"; 文件2有这一行 "one,,three,,42" 其中,在0中,索引$position=4的主键为42 然后是子文件:merge_file($file1,$file2,$outputfile,$position) 将输出带有以下行的文件: "one,two,,four,42"
"one,two,,four,42"
"one,two,three,four,42";
文件2有这一行
"one,,three,,42"
其中,在0中,索引$position=4的主键为42
然后是子文件:merge_file($file1,$file2,$outputfile,$position)
将输出带有以下行的文件:
"one,two,,four,42"
"one,two,three,four,42";
每个主键在每个文件中都是唯一的,一个键可能存在于一个文件中,但不存在于另一个文件中(反之亦然)
每个文件中大约有一百万行
遍历第一个文件中的每一行,我使用散列存储主键,并将行号存储为值。行号对应于一个数组[line num],该数组存储第一个文件中的每一行
然后我检查第二个文件中的每一行,检查主键是否在散列中,如果在散列中,则从file1array中获取该行,然后将我需要的列从第一个数组添加到第二个数组,然后concat。到最后。然后删除散列,最后将整个内容转储到文件中。(我使用的是SSD,因此我希望尽量减少文件写入。)
最好用一个代码来解释:
sub merge_file2{
my ($file1,$file2,$out,$position) = ($_[0],$_[1],$_[2],$_[3]);
print "merging: \n$file1 and \n$file2, to: \n$out\n";
my $OUTSTRING = undef;
my %line_for;
my @file1array;
open FILE1, "<$file1";
print "$file1 opened\n";
while (<FILE1>){
chomp;
$line_for{read_csv_string($_,$position)}=$.; #reads csv line at current position (of key)
$file1array[$.] = $_; #store line in file1array.
}
close FILE1;
print "$file2 opened - merging..\n";
open FILE2, "<", $file2;
my @from1to2 = qw( 2 4 8 17 18 19); #which columns from file 1 to be added into cols. of file 2.
while (<FILE2>){
print "$.\n" if ($.%1000) == 0;
chomp;
my @array1 = ();
my @array2 = ();
my @array2 = split /,/, $_; #split 2nd csv line by commas
my @array1 = split /,/, $file1array[$line_for{$array2[$position]}];
# ^ ^ ^
# prev line lookup line in 1st file,lookup hash, pos of key
#my @output = &merge_string(\@array1,\@array2); #merge 2 csv strings (old fn.)
foreach(@from1to2){
$array2[$_] = $array1[$_];
}
my $outstring = join ",", @array2;
$OUTSTRING.=$outstring."\n";
delete $line_for{$array2[$position]};
}
close FILE2;
print "adding rest of lines\n";
foreach my $key (sort { $a <=> $b } keys %line_for){
$OUTSTRING.= $file1array[$line_for{$key}]."\n";
}
print "writing file $out\n\n\n";
write_line($out,$OUTSTRING);
}
子合并_文件2{
我的($file1,$file2,$out,$position)=($\[0]、$\[1]、$\[2]、$\[3]);
打印“合并:\n$file1和\n$file2到:\n$out\n”;
我的$OUTSTRING=未定义;
我的%line_用于;
我的@File1阵列;
打开文件1,“$out”;
打印$header;
foreach(@sortedf2){
打印输出(连接“,”,@{$})。“\n”;
}
收尾;
}
谢谢大家,解决方案张贴在上面。现在合并整个过程大约需要1分钟的时间!:) 假设每个文件大约有20个字节行,总计约20 MB,这并不太大。 因为您正在使用哈希,所以您的时间复杂性似乎不是问题 在第二个循环中,您正在为每行打印到控制台,这一点很慢。试着去掉它会有很大帮助。 您还可以避免在第二个循环中删除
同时阅读多行文字也会有所帮助。但我不认为太多,在幕后总是会有预读。想到两种技巧
sort
),然后并行地对每个文件进行线性扫描(从每个文件中读取一条记录;如果键相等,则输出一个连接行并使两个文件前进;如果键不相等,则使用较小的键使文件前进,然后重试)。这一步是O(n+m)时间,而不是O(n*m)和O(1)内存影响性能的是这段代码,它被连接了数百万次
$OUTSTRING.=$outstring."\n";
....
foreach my $key (sort { $a <=> $b } keys %line_for){
$OUTSTRING.= $file1array[$line_for{$key}]."\n";
}
我看不出有什么明显的慢,但我会做出以下改变:
- 首先,我要消除
变量。你不需要它;只需将行本身存储在哈希中:@file1array
while (<FILE1>){ chomp; $line_for{read_csv_string($_,$position)}=$_; }
while(){ 咀嚼; $line_表示{read_csv_string($\u,$position)}=$\u; }
- 第二,虽然这对perl没有多大影响,但我不会一直在
中添加。相反,保留一个输出行数组,并每次将$OUTSTRING
推到该数组上。如果出于某种原因,您仍然需要使用大量字符串调用
,您可以始终在末尾使用write\u line
join(“”,@OUTLINES)
- 如果
不使用write\u-line
或类似的低级调用,而是使用syswrite
或其他基于stdio的调用,那么通过在内存中建立输出文件,就不会保存任何磁盘写入。因此,您最好根本不在内存中构建输出,而是在创建时将其写出。当然,如果您使用的是print
,请忘记这一点syswrite
- 因为没有明显的慢,试着抛出你的代码。我发现这是最好的perl分析器,可以产生那些“哦!这是慢行!”的见解
unde
表示未知值
use 5.10.0; # for // ("defined-or")
use Carp;
use Text::CSV;
sub merge_csv {
my($path,$record) = @_;
open my $fh, "<", $path or croak "$0: open $path: $!";
my $csv = Text::CSV->new;
local $_;
while (<$fh>) {
if ($csv->parse($_)) {
my @f = map length($_) ? $_ : undef, $csv->fields;
next unless @f >= 1;
my $primary = pop @f;
if ($record->{$primary}) {
$record->{$primary}[$_] //= $f[$_]
for 0 .. $#{ $record->{$primary} };
}
else {
$record->{$primary} = \@f;
}
}
else {
warn "$0: $path:$.: parse failed; skipping...\n";
next;
}
}
}
Data::Dumper
模块显示,给定问题的简单输入,得到的哈希值是
$VAR1 = {
'42' => [
'one',
'two',
'three',
'four'
]
};
$VAR1={
'42' => [
"一",,
“两个”,
"三",,
“四”
]
}; 如果你想合并,你应该真的合并。首先,您必须按键对数据进行排序,然后再合并!您将在性能上击败MySQL。我在这方面有很多经验 你可以这样写:
#!/usr/bin/env perl
use strict;
use warnings;
use Text::CSV_XS;
use autodie;
use constant KEYPOS => 4;
die "Insufficient number of parameters" if @ARGV < 2;
my $csv = Text::CSV_XS->new( { eol => $/ } );
my $sortpos = KEYPOS + 1;
open my $file1, "sort -n -k$sortpos -t, $ARGV[0] |";
open my $file2, "sort -n -k$sortpos -t, $ARGV[1] |";
my $row1 = $csv->getline($file1);
my $row2 = $csv->getline($file2);
while ( $row1 and $row2 ) {
my $row;
if ( $row1->[KEYPOS] == $row2->[KEYPOS] ) { # merge rows
$row = [ map { $row1->[$_] || $row2->[$_] } 0 .. $#$row1 ];
$row1 = $csv->getline($file1);
$row2 = $csv->getline($file2);
}
elsif ( $row1->[KEYPOS] < $row2->[KEYPOS] ) {
$row = $row1;
$row1 = $csv->getline($file1);
}
else {
$row = $row2;
$row2 = $csv->getline($file2);
}
$csv->print( *STDOUT, $row );
}
# flush possible tail
while ( $row1 ) {
$csv->print( *STDOUT, $row1 );
$row1 = $csv->getline($file1);
}
while ( $row2 ) {
$csv->print( *STDOUT, $row2 );
$row2 = $csv->getline($file1);
}
close $file1;
close $file2;
嗯,他每1000行只在控制台上打印一次,“delete”对于他在while语句后面的循环中所做的工作非常重要。每行20字节。哈哈,你对Perl的内存效率知之甚少。如果你解析它并存储在散列中,它需要更多的时间。你说的O(nm)是什么意思?他在这里什么也没做。他在一个文件上循环一次,在第二个文件上循环一次,并且没有做任何愚蠢的事情,比如在第二个循环中顺序扫描数组。@Daniel:如果你认为哈希查找是O(1),那么你就太天真了。这只是书本上的事,但事实并非如此。首先,哈希映射查找与哈希计算成正比,它与密钥长度乘以哈希长度成正比,哈希长度通常是密钥空间的logN。所以实际上查找至少是O(logN)。(是的,Perl使用自适应哈希长度。)其次,还有一些额外的效果,比如CPU缓存命中等等。事实上,它比O(logN)和O(1)都要慢得多。我原来的散列变得越来越慢,int
#!/usr/bin/env perl
use strict;
use warnings;
use Text::CSV_XS;
use autodie;
use constant KEYPOS => 4;
die "Insufficient number of parameters" if @ARGV < 2;
my $csv = Text::CSV_XS->new( { eol => $/ } );
my $sortpos = KEYPOS + 1;
open my $file1, "sort -n -k$sortpos -t, $ARGV[0] |";
open my $file2, "sort -n -k$sortpos -t, $ARGV[1] |";
my $row1 = $csv->getline($file1);
my $row2 = $csv->getline($file2);
while ( $row1 and $row2 ) {
my $row;
if ( $row1->[KEYPOS] == $row2->[KEYPOS] ) { # merge rows
$row = [ map { $row1->[$_] || $row2->[$_] } 0 .. $#$row1 ];
$row1 = $csv->getline($file1);
$row2 = $csv->getline($file2);
}
elsif ( $row1->[KEYPOS] < $row2->[KEYPOS] ) {
$row = $row1;
$row1 = $csv->getline($file1);
}
else {
$row = $row2;
$row2 = $csv->getline($file2);
}
$csv->print( *STDOUT, $row );
}
# flush possible tail
while ( $row1 ) {
$csv->print( *STDOUT, $row1 );
$row1 = $csv->getline($file1);
}
while ( $row2 ) {
$csv->print( *STDOUT, $row2 );
$row2 = $csv->getline($file1);
}
close $file1;
close $file2;
(open my $file1, '-|') || exec('sort', '-n', "-k$sortpos", '-t,', $ARGV[0]);
(open my $file2, '-|') || exec('sort', '-n', "-k$sortpos", '-t,', $ARGV[1]);