我正在开发一个涉及在Perl中解析大型csv格式文件的项目,并希望提高效率。
我的方法是首先按行split()
逐行,然后再用逗号再次split()
来获取字段。但这是次优的,因为至少需要两次传递数据。 (一次用线分开,然后再用每一行分开)。这是一个非常大的文件,因此切割加工一半将是整个应用程序的重大改进。
我的问题是,使用内置工具解析大型CSV文件的最有效时间是什么?
注意:每一行都有不同数量的标记,因此我们不能只忽略行并仅用逗号分割。此外,我们可以假设字段将只包含字母数字ascii数据(没有特殊字符或其他技巧)。此外,我不想进行并行处理,尽管它可能有效。
修改
它只能涉及Perl 5.8附带的内置工具。出于官僚主义的原因,我不能使用任何第三方模块(即使托管在cpan上)
其他编辑
假设我们的解决方案只有在文件数据完全加载到内存后才能处理。
又是另一个编辑
我刚刚抓住这个问题是多么愚蠢。抱歉浪费你的时间。投票结束。
答案 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::CSV,Text::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上提取源代码并将代码复制/粘贴到项目中。 ..