代码之家  ›  专栏  ›  技术社区  ›  Mike

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

  •  23
  • Mike  · 技术社区  · 14 年前

    我正在做一个项目,它涉及到用Perl解析一个大型csv格式的文件,我希望能让事情变得更高效。

    split() 文件先按行排序,然后 每行用逗号表示字段。但这并不理想,因为至少需要两次传递数据(一次按行拆分,然后每行一次)。这是一个非常大的文件,所以将处理分为两半将是整个应用程序的一个重大改进。

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

    编辑

    另一个编辑

    又是一次编辑

    6 回复  |  直到 14 年前
        1
  •  46
  •   szabgab Brandon Fosdick    10 年前

    正确的方法是使用 Text::CSV_XS

    对于纯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);
    
        2
  •  19
  •   jkramer    14 年前

    这是一个同样尊重引用的版本(例如。 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)
    }
    
        3
  •  9
  •   serenesat    9 年前

    Text::CSV Text::CSV_XS Text::CSV_PP 后端(如果您不能编译 XS 模块)。

    如果你可以得到额外的代码 (例如,你自己的个人模块)你可以 文本::CSV\u PP use lib 解决方法:

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

    此外,如果除了将整个文件读入内存并(我假设)存储在标量中之外没有其他选择,您仍然可以像文件句柄一样读取它,方法是打开标量的句柄:

    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)) {
        ...
    }
    

    或逗号上的次优分割:

    while (my $line = <$text_handle>) {
        my @csv = split /,/, $line;
        ... # regular work as before.
    }
    

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

        4
  •  2
  •   Byron Whitlock    14 年前

    #(no error handling here!)    
    open FILE, $filename
    while (<FILE>) {
         @csv = split /,/ 
    
         # now parse the csv however you want.
    
    }
    

    你需要基准你的进口 看看是什么导致了经济放缓。例如,如果您正在执行占用85%时间的db插入,则此优化将不起作用。

    编辑

    逐字节遍历缓冲区,直到找到一个csv delimeter或新行。

    • 找到分隔符后,增加列计数。
    • 当你找到一个换行符时,增加你的行数。

    就这样。但是把一个大文件读入内存并不是最好的方法,看看我原来的答案,这是正常的方法。

        5
  •  1
  •   ZyX    14 年前

    假设您已将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]}, $_; }
    }
    
        6
  •  1
  •   Dave Sherohman    14 年前

    在问题所施加的限制范围内回答,您仍然可以通过将输入文件拖入数组而不是标量来剪切第一个分割:

    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