优化CSV处理脚本 - Python、Perl和Java
我正在尝试写一个简单的脚本,快速处理一个嘈杂的CSV文件。
我只想从一个很大的CSV文件(经过gzip压缩)中提取几个列,然后写入一个新的CSV文件,里面是经过筛选的数据。还添加了一个简单的过滤方法,检查第一列的长度是否等于15。
我比较了perl、java和python的脚本,发现Java的速度比其他语言快很多。我在想,是否还有其他方法可以优化每种语言的这个简单过程呢?
对于一个800MB的gzip文件,各种语言的基准测试时间如下:
1. Java: 74秒
2. Python: 197秒
3. Perl: 7分钟
Python代码:
import gzip
import csv
import time
def getArray(row):
columns = [0,4,5,26,33,34,35,36,39,41,42,47,54,65,66,72,73,91]
row_filt = []
for i in columns:
row_filt.append(row[i])
return row_filt
filename = 'Very_large_csv.gz'
outfile = filename + '.csv'
csv.register_dialect('wifi', delimiter='|', quoting=csv.QUOTE_NONE, quotechar = '')
start_time = time.time()
try:
f = gzip.open(filename, 'rb')
f2 = open(outfile, 'wb')
reader = csv.reader(f, dialect = 'wifi')
writer = csv.writer(f2, dialect = 'wifi')
header = reader.next()
writer.writerow(getArray(header))
for row in reader:
if (len(row[0]) != 15):
continue
writer.writerow(getArray(row))
print(time.time() - start_time)
finally:
f.close()
Perl代码:
use strict;
use warnings;
use Cwd;
use IO::Uncompress::Gunzip qw($GunzipError);
use Text::CSV_XS;
use Time::Piece;
use Time::Seconds;
my @COLUMNS = (0,4,5,26,33,34,35,36,39,41,42,47,54,65,66,72,73,91);
my $csv = Text::CSV_XS->new ({ binary => 1,
sep_char => '|',
escape_char => undef,
eol => "\n",
quote_char => undef
});
my $infile='Very_large_csv.gz';
my $fh = IO::Uncompress::Gunzip->new($infile) or die "IO::Uncompress::Gunzip failed: $GunzipError\n";
my $outfile = $infile . ".csv";
open my $out, ">", $outfile or die "$outfile: $!\n";
my @header_row = split(/\|/,<$fh>);
my @header = ();
foreach my $column (@COLUMNS)
{
push @header, $header_row[$column];
}
my $header_filter = \@header;
$csv->print ($out, $header_filter);
print "Start.\n";
while (my $row = $csv->getline($fh))
{
length($row->[0]) == 15 or next;
my @data = ();
foreach my $column (@COLUMNS)
{
push @data, $row->[$column];
}
my $row_filter = \@data;
$csv->print($out, $row_filter);
}
$csv->eof or $csv->error_diag ();
close $fh;
close $out or die "$outfile: $!";
Java代码:
public class NoiseFilter {
static final int[] columns = {0,4,5,26,33,34,35,36,39,41,42,47,54,65,66,72,73,91};
public static void main(String[] args) throws IOException {
fname='Very_large_csv.gz';
GZIPInputStream gzip = new GZIPInputStream(new FileInputStream(fname));
BufferedReader reader = new BufferedReader(new InputStreamReader(gzip));
String line = reader.readLine(); // Header
String[] header = line.split("\\|");
PrintWriter ww = new PrintWriter(fname + ".csv");
printRow(header, ww);
while ((line = reader.readLine()) != null) {
String[] data = line.split("\\|",-1);
if (data[0].length() != 15 ) { continue; }
printRow(data, ww);
}
ww.close();
reader.close();
}
private static void printRow(String[] row, PrintWriter writer) {
for (int i = 0; i < columns.length; i++) {
if (i == 0) {
writer.print(row[columns[i]]);
} else {
writer.print("|" + row[columns[i]]);
}
}
writer.print("\n");
}
}
我对Python代码进行了修改,运行时间变成了95秒,和Java差不多。
def getArray(line):
string=''
row=line.split(',')
for i in columns:
string+=(row[i]+',')
return string+'\n'
try:
f = gzip.open(filename, 'rb')
f2 = open(outfile, 'wb')
header = f.readline()
f2.write(getArray(header))
for line in f:
f2.write(getArray(line))
finally:
f.close()
2 个回答
你的内循环里没有太多多余的东西。在Python版本中,每次调用getarray()时,都会创建一个新的列对象。由于getarray()这个函数本身很简单,你可以把它的内容直接写在调用的地方。
这样做可能不会带来明显的速度提升。
你也可以试试PyPy,这可能会带来比较大的差别——不过可能还是没有Java版本快。
你的Perl脚本有一些地方可以优化。比如,这段代码:
while (my $row = $csv->getline($fh))
{
length($row->[0]) == 15 or next;
my @data = ();
foreach my $column (@COLUMNS)
{
push @data, $row->[$column];
}
my $row_filter = \@data;
$csv->print($out, $row_filter);
}
可以换成:
my $row;
length($row->[0])==15 and $csv->print($out, [ @{$row}[@COLUMNS] ])
while $row = $csv->getline($fh);
...这样做可能会稍微提高性能。我没有进行过性能测试,但应该不会有太大的差别。
更重要的是,你的Java代码之所以更快,是因为它“做的事情少得多”。Text::CSV_XS(我想你用的Python模块也是)是一个完整的解析器,它能处理带引号的字段、转义字符等等。考虑一下下面这个用管道符分隔的文件,它应该有两行两列:
1|"Foo+Bar"
2|"Foo|Bar"
你的Java代码简单地用管道符来分割行,这样“Foo|Bar”本来应该是一个完整的字符串,却被分成了两个字段。如果Java代码也像Perl和Python版本那样进行检查,速度就会慢下来。
相反,你可以通过放弃正确的CSV格式解析,直接使用split
来加快Perl或Python版本的速度。例如,在Perl中:
while (<$fh>) {
chomp;
my @F = split /\|/;
length $F[0] == 15 or next;
print {$out} join("|", @F[@COLUMNS]), "\n";
}
你的整个脚本甚至可以用下面这一行代码来完成:
gzip -d -c Very_large_csv.gz | perl -F'\|' -lane 'print join("|", @F[0,4,5,26,33,34,35,36,39,41,42,47,54,65,66,72,73,91]) if $. == 1 || length($F[0]) == 15' > output.csv
解释:
选项说明:
-F
:split()
的模式,用于-a
选项(//是可选的)-l
: 启用行结束符处理-a
: 按空格分割行,并将结果加载到数组@F
中-n
: 为输入文件中的每一“行”创建一个while(<>){...}
循环。-e
: 告诉perl
在命令行上执行代码。
代码说明:
gzip -d -c Very_large_csv.gz
: 解压文件,并将其输出到标准输出print join("|", @F[0,4,5,26,33,34,35,36,39,41,42,47,54,65,66,72,73,91])
: 只保留CSV文件中的某些索引if $. == 1 || length($F[0]) == 15
: 根据表头或第一列进行过滤