如何在Perl中有效地解析CSV文件?

时间:2010-06-17 19:49:02

标签: perl parsing text csv split

我正在开发一个涉及在Perl中解析大型csv格式文件的项目,并希望提高效率。

我的方法是首先按行split()逐行,然后再用逗号再次split()来获取字段。但这是次优的,因为至少需要两次传递数据。 (一次用线分开,然后再用每一行分开)。这是一个非常大的文件,因此切割加工一半将是整个应用程序的重大改进。

我的问题是,使用内置工具解析大型CSV文件的最有效时间是什么?

注意:每一行都有不同数量的标记,因此我们不能只忽略行并仅用逗号分割。此外,我们可以假设字段将只包含字母数字ascii数据(没有特殊字符或其他技巧)。此外,我不想进行并行处理,尽管它可能有效。

修改

它只能涉及Perl 5.8附带的内置工具。出于官僚主义的原因,我不能使用任何第三方模块(即使托管在cpan上)

其他编辑

假设我们的解决方案只有在文件数据完全加载到内存后才能处理。

又是另一个编辑

我刚刚抓住这个问题是多么愚蠢。抱歉浪费你的时间。投票结束。

6 个答案:

答案 0 :(得分:43)

正确的方法 - 按一个数量级 - 使用Text::CSV_XS。它将比您自己可能做的任何事情更快,更强大。如果您决定仅使用核心功能,则根据速度与稳健性的不同,您有几个选项。

关于pure-Perl的最快速度是逐行读取文件,然后天真地分割数据:

my $file = 'somefile.csv';
my @data;
open(my $fh, '<', $file) or die "Can't read file '$file' [$!]\n";
while (my $line = <$fh>) {
    chomp $line;
    my @fields = split(/,/, $line);
    push @data, \@fields;
}

如果任何字段包含嵌入的逗号,则会失败。更健壮(但更慢)的方法是使用Text :: ParseWords。为此,请将split替换为:

    my @fields = Text::ParseWords::parse_line(',', 0, $line);

答案 1 :(得分:19)

这是一个同样尊重引号的版本(例如foo,bar,"baz,quux",123 -> "foo", "bar", "baz,quux", "123")。

sub csvsplit {
        my $line = shift;
        my $sep = (shift or ',');

        return () unless $line;

        my @cells;
        $line =~ s/\r?\n$//;

        my $re = qr/(?:^|$sep)(?:"([^"]*)"|([^$sep]*))/;

        while($line =~ /$re/g) {
                my $value = defined $1 ? $1 : $2;
                push @cells, (defined $value ? $value : '');
        }

        return @cells;
}

像这样使用:

while(my $line = <FILE>) {
    my @cells = csvsplit($line); # or csvsplit($line, $my_custom_seperator)
}

答案 2 :(得分:8)

正如其他人提到的,正确的方法是使用Text::CSVText::CSV_XS后端(最快读取)或Text::CSV_PP后端(如果可以)编译XS模块。)

如果您被允许在本地获取额外代码(例如,您自己的个人模块),您可以将Text::CSV_PP放在本地某处,然后通过{{1解决方法:

use lib

此外,如果没有其他选择将整个文件读入内存并且(我假设)存储在标量中,您仍然可以通过打开标量句柄来读取文件句柄:

use lib '/path/to/my/perllib';
use Text::CSV_PP;

然后通过Text :: CSV界面阅读:

my $data = stupid_required_interface_that_reads_the_entire_giant_file();

open my $text_handle, '<', \$data
   or die "Failed to open the handle: $!";

或逗号上的次优分割:

my $csv = Text::CSV->new ( { binary => 1 } )
             or die "Cannot use CSV: ".Text::CSV->error_diag ();
while (my $row = $csv->getline($text_handle)) {
    ...
}

使用这种方法,数据一次只能从标量中复制一点。

答案 3 :(得分:2)

如果逐行读取文件,则可以一次性完成。没有必要立刻将整个内容读入内存。

#(no error handling here!)    
open FILE, $filename
while (<FILE>) {
     @csv = split /,/ 

     # now parse the csv however you want.

}

不确定这是否显着提高效率,Perl在字符串处理方面非常快。

您需要对您的进口进行基准测试,以了解导致经济放缓的原因。例如,如果您正在进行占用85%时间的数据库插入,则此优化将无效。

修改

虽然这感觉就像代码高尔夫,但一般算法是将整个文件或部分文件读入缓冲区。

通过缓冲区逐字节迭代,直到找到csv分隔符或新行。

  • 找到分隔符时,请增加列数。
  • 当您发现换行符增加行数时。
  • 如果您点击缓冲区的末尾,请从文件中读取更多数据并重复。

就是这样。但是将大文件读入内存实际上并不是最好的方法,请参阅我的原始答案,了解正常情况。

答案 4 :(得分:1)

假设您已将CSV文件加载到$csv变量中,并且在成功解析之后您不需要此变量中的文本:

my $result=[[]];
while($csv=~s/(.*?)([,\n]|$)//s) {
    push @{$result->[-1]}, $1;
    push @$result, [] if $2 eq "\n";
    last unless $2;
}

如果您需要$csv未触及:

local $_;
my $result=[[]];
foreach($csv=~/(?:(?<=[,\n])|^)(.*?)(?:,|(\n)|$)/gs) {
    next unless defined $_;
    if($_ eq "\n") {
        push @$result, []; }
    else {
        push @{$result->[-1]}, $_; }
}

答案 5 :(得分:1)

在问题所施加的限制范围内回答,你仍然可以通过将输入文件插入数组而不是标量来删除第一次拆分:

open(my $fh, '<', $input_file_path) or die;
my @all_lines = <$fh>;
for my $line (@all_lines) {
  chomp $line;
  my @fields = split ',', $line;
  process_fields(@fields);
}

即使您无法安装(纯Perl版本的)Text::CSV,您也可以在CPAN上提取源代码并将代码复制/粘贴到项目中。 ..