11

我正在创建一个子程序:

(1) 解析一个CSV文件;

(2) 并检查该文件中的所有行是否具有预期的列数。如果列数无效,它会发出嘶哑的声音。

当行数从数千到数百万不等时,您认为最有效的方法是什么?

现在,我正在尝试这些实现。

(1) 基本文件解析器

open my $in_fh, '<', $file or 
    croak "Cannot open '$file': $OS_ERROR";                                                            
                                                                                                      
my $row_no = 0;                                                                                           
while ( my $row = <$in_fh> ) {                                                                            
    my @values = split (q{,}, $row);                                                                      
    ++$row_no;                                                                                            
    if ( scalar @values < $min_cols_no ) {                                                                
        croak "Invalid file format. File '$file' does not have '$min_cols_no' columns in line '$row_no'.";
    }                                                                                                     
}                                                                                                         
                                                                                                      
close $in_fh                                                                                              
    or croak "Cannot close '$file': $OS_ERROR";                                                           

(2) 使用 Text::CSV_XS (bind_columns 和 csv->getline)

my $csv = Text::CSV_XS->new () or                                                                         
   croak "Cannot use CSV: " . Text::CSV_XS->error_diag();                                                 
open my $in_fh, '<', $file or                                                                             
   croak "Cannot open '$file': $OS_ERROR";                                                                
                                                                                                          
 my $row_no = 1;                                                                                          
 my @cols = @{$csv->getline($in_fh)};                                                                     
 my $row = {};                                                                                            
 $csv->bind_columns (\@{$row}{@cols});                                                                    
 while ($csv->getline ($in_fh)) {                                                                         
    ++$row_no;                                                                                            
    if ( scalar keys %$row < $min_cols_no ) {                                                             
        croak "Invalid file format. File '$file' does not have '$min_cols_no' columns in line '$row_no'.";
    }                                                                                                     
}                                                                                                         
                                                                                                          
$csv->eof or $csv->error_diag();                                                                          
close $in_fh or
    croak "Cannot close '$file': $OS_ERROR";                                                           

(3) 使用 Text::CSV_XS (csv->parse)

my $csv = Text::CSV_XS->new() or                                                                         
   croak "Cannot use CSV: " . Text::CSV_XS->error_diag();                                                
 open my $in_fh, '<', $file or                                                                           
   croak "Cannot open '$file': $OS_ERROR";                                                               
                                                                                                         
 my $row_no = 0;                                                                                         
 while ( <$in_fh> ) {                                                                                    
     $csv->parse($_);                                                                                    
     ++$row_no;                                                                                          
     if ( scalar $csv->fields < $min_cols_no ) {                                                         
       croak "Invalid file format. File '$file' does not have '$min_cols_no' columns in line '$row_no'.";
     }                                                                                                   
}                                                                                                        
                                                                                                         
$csv->eof or $csv->error_diag();                                                                         
close $in_fh or 
    croak "Cannot close '$file': $OS_ERROR";                                                          

(4) 使用 Parse::CSV

use Parse::CSV;                                                                                           
my $simple = Parse::CSV->new(                                                                             
    file => $file                                                                                         
);                                                                                                        
                                                                                                          
my $row_no = 0;                                                                                           
while ( my $array_ref = $simple->fetch ) {                                                                
    ++$row_no;                                                                                            
    if ( scalar @$array_ref < $min_cols_no ) {                                                            
        croak "Invalid file format. File '$file' does not have '$min_cols_no' columns in line '$row_no'.";
    }                                                                                                     
}                                                                                                         

我使用 Benchmark 模块对它们进行了基准测试。

use Benchmark qw(timeit timestr timediff :hireswallclock);

这些是我得到的数字(以秒为单位)

1,000 行文件:

实施1:0.0016

实施2:0.0025

实施3:0.0050

实施4:0.0097

10,000 行文件:

实施1:0.0204

实施2:0.0244

实施3:0.0523

实施4:0.1050

1,500,000 行文件:

实现一:1.8697

实施2:3.1913

实施3:7.8475

实施4:15.6274

鉴于这些数字,我会得出结论,简单解析器是最快的,但从我从不同来源阅读的内容来看,Text::CSV_XS 应该是最快的。

有人会启发我吗?我使用模块的方式有问题吗?非常感谢你的帮助!

4

4 回答 4

19

有 CSV 文件

header1,header2,header3
value1,value2,value3

然后是 CSV 文件。

header1,"This, as they say, is header2","And header3
even contains a newline!"
value1,"value2, 2nd in a series of 3 values",value3

Text::CSV及其同类产品经过精心开发和测试以应对第二种。如果您确信您的输入确实并且始终符合简单的 CSV 规范,那么您很可能可以构建一个性能优于Text::CSV.

于 2012-12-17T15:51:53.590 回答
10

请注意,您的Text::CSV_XS版本比简单的解析器版本做得更多。它拆分行,将其放入内存,并使您的 hashref 指向字段。

它也可能有其他逻辑,比如允许转义分隔符(我不知道,因为我没有使用它)。最重要的是,使用模块时总会有少量开销:函数调用、来回传递参数,以及可能并不真正适用于您的情况的通用代码(例如对您不适用的事情进行错误检查)不在乎)。

通常,使用模块的好处远远超过成本。您可以获得更多功能、更可靠的代码等。但是对于一个非常简单的小任务来说,情况可能并非如此。如果您需要做的只是验证列数,那么使用模块可能有点过头了。您可以通过仅计算列数来更快地实现自己的实现,而根本不需要拆分:

/(?:,[^,]*){$min_cols_no-1}/ or croak "Did not find minimum number of columns";

如果您要在此验证步骤之外进行实际处理,则使用该模块可能会有所帮助。

于 2012-12-17T15:51:51.263 回答
1

所有 CSV 解析模块都做同样的事情:打开文件并以某种方式解析 CSV,就像您在基本子中所做的那样。它们只是带来更多的开销,因为在内部,它们做的比你需要的要多得多(检查正确的 CSV 格式,传递对象结构等)。这使得它们在不同程度上比你的基本方法慢。

您自己对这些方法进行了基准测试;结果不是很明显吗?如果我不需要 CSV 模块的扩展功能,我会以自己的基本方式解析 CSV 文件。

(我不知道您是否可以通过改进模块的使用来加快它们的速度)

于 2012-12-17T15:40:46.857 回答
0

只是为了好玩,我为此测试了正则表达式......并且它有效!;) 如果你有足够的内存,你可以一次读取整个文件,然后使用正则表达式:

my $blob = 'a;s;d
q;w;e
r;t;y
u;i;o
p;z;x
c;;b
n;m;f
g;h;j
k;l;';

say $blob =~ /^ ([^;]*;){2}[^;]* (\n (([^;]*;){2}[^;]*)+ \n ([^;]*;){2}[^;]*)? $/x ? 'ok' : 'bu';

但这不包括分隔符转义、引用等 - 只需测试指定数量的分隔符:)

于 2012-12-22T08:26:38.667 回答