Я работаю над проектом, который включает парсинг большого csv, отформатировал файл в Perl и надеюсь делать вещи более эффективными.
Мой подход был к split()
файл строками сначала, и затем split()
каждая строка снова запятыми для получения полей. Но это субоптимальное, так как по крайней мере два передают данные, требуется. (однажды для разделения строками, затем еще раз для каждой строки). Это - очень большой файл, так вырезание обрабатывающий в половине было бы существенным улучшением к целому приложению.
Мой вопрос, каковы действенные средства большей части времени парсинга большого файла CSV с помощью только созданный в инструментах?
примечание: Каждая строка имеет переменное количество маркеров, таким образом, мы не можем только проигнорировать строки и разделение запятыми только. Также мы можем предположить, что поля будут содержать только алфавитно-цифровые данные ASCII (никакие специальные символы или другие приемы). Кроме того, я не хочу входить в параллельную обработку, хотя она могла бы работать эффективно.
править
Это может только связать встроенные инструменты та поставка с Perl 5.8. По бюрократическим причинам я не могу использовать сторонние модули (даже если размещенный на cpan)
другое редактирование
Давайте предположим, что нашему решению только позволяют иметь дело с данными файла, после того как это полностью загружается в память.
еще одно редактирование
Я просто схватил, насколько глупый этот вопрос. Извините за трату Вашего времени. Голосование для закрытия.
Правильный способ сделать это - на порядок больше - использовать 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);
Предполагается, что ваш 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]}, $_; }
}
Вы можете сделать это за один проход, если будете читать файл построчно. Нет необходимости сразу считывать весь файл в память.
#(no error handling here!)
open FILE, $filename
while (<FILE>) {
@csv = split /,/
# now parse the csv however you want.
}
Не уверен, что это значительно эффективнее, хотя Perl довольно быстр в обработке строк.
ВАМ НУЖНО ПРОВЕРИТЬ ВАШ ИМПОРТ, чтобы понять, что вызывает замедление. Если, например, вы делаете вставку в базу данных, которая занимает 85% времени, эта оптимизация не сработает.
Edit
Хотя это похоже на кодовый гольф, общий алгоритм состоит в том, чтобы прочитать весь файл или часть файла в буфер.
Итерация байт за байтом по буферу до тех пор, пока не будет найдена делимма csv или новая строка.
Вот и все. Но чтение большого файла в память на самом деле не лучший способ, см. мой первоначальный ответ о том, как это делается обычно.
Как отмечали другие люди, правильный способ сделать это - Text:: CSV, и либо Text::CSV_XS
back end (для быстрейшего чтения), либо Text::CSV_PP
back end (если вы не можете скомпилировать модуль XS
).
Если вам разрешено получать дополнительный код локально (например, ваши личные модули), вы можете взять Text::CSV_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: $!";
А затем читать через интерфейс Text::CSV:
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.
}
При таком способе данные копируются из скаляра только по биту за раз.
Вот версия, которая также уважает кавычки (например, 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)
}
Отвечая в рамках ограничений, налагаемых вопросом, вы все равно можете вырезать первое разделение, превратив входной файл в массив, а не в скаляр:
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);
}
И даже если вы не можете install (версия на чистом Perl) Text :: CSV
, вы можете уйти, загрузив его исходный код на CPAN и скопировав / вставив код в свой проект ...