【问题标题】:Fastest CSV Parser in PerlPerl 中最快的 CSV 解析器
【发布时间】:2012-12-04 17:08:34
【问题描述】:

我正在创建一个子例程:

(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);

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

1000 行文件:

实施 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:1.8697

实施 2:3.1913

实施 3:7.8475

实施 4:15.6274

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

有人会告诉我这个吗?我使用模块的方式有问题吗?非常感谢您的帮助!

【问题讨论】:

  • 为什么速度对您如此重要?你确定这是你的代码的慢点吗?你的程序速度有问题吗?你确定你没有过早优化吗?例如,如果您的程序将 90% 的时间用于写入数据库,而将 10% 的时间用于读取 CSV 文件,那么寻找最快的 CSV 解析器并不是花费时间的最佳方式。
  • '"That's the thing about strings", he said, "you can include commas, periods, and all other kinds of stuff in quotes!"'
  • @AndyLester,速度非常重要,因为这是我们现有的几乎所有系统的当前工作流程的附加功能或步骤,如果可能,我们不想增加任何额外的时间。在这种情况下,我真的很想向专家学习,无论我的做法是否正确。我也同意你的看法。优化对数据库的写入确实更有意义,因为大多数时候,瓶颈都发生在那里。仅供参考,我们使用 freebcp 进行插入。再次感谢安迪!
  • @JackManey,我用谷歌搜索了这些陈述的部分内容,但找不到来源。你是在哪里拿到的?也谢谢!
  • @Carlisle:我知道您“如果可能的话,不想增加任何额外的时间”。没有人会。但实际上,这是“任何额外时间”意味着什么的问题。如果您有一些东西在任何更改之前的 37 分钟内运行,并且当您使用 Text::CSV::Whatever 时在 37:02 运行,那么是否有一些东西可以让它在 37:01 运行呢?花费昂贵的程序员时间来解决这个问题值得吗?或者可能使用需要您编写难以理解的代码的模块?当然,如果没有测量,您将无法回答任何问题。

标签: perl parsing csv


【解决方案1】:

有 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 的解析器。

【讨论】:

  • 我现在明白多了。谢谢,@mob!
【解决方案2】:

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

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

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

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

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

【讨论】:

    【解决方案3】:

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

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

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

    【讨论】:

    • 我想确认我的方法是正确的,我在这里没有遗漏任何东西。现在,我真的了解到,就像@dan1111 所说的那样,“通常使用模块的好处远远超过成本。”也谢谢!
    【解决方案4】:

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

    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';
    

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-14
      • 1970-01-01
      • 2010-10-03
      • 1970-01-01
      • 2011-03-04
      • 2013-11-15
      相关资源
      最近更新 更多