Подтвердить что ты не робот

Как эффективно анализировать CSV файл в Perl?

Я работаю над проектом, который включает в себя синтаксический анализ большого файла csv в Perl и я хочу сделать что-то более эффективным.

Мой подход состоял в split() файле по строкам первым, а затем split() каждую строку снова запятыми, чтобы получить поля. Но это субоптимально, так как требуется не менее двух проходов по данным. (один раз разделить по линиям, затем еще раз для каждой строки). Это очень большой файл, поэтому обработка обработки пополам станет значительным улучшением для всего приложения.

Мой вопрос в том, что самое эффективное время для синтаксического анализа большого CSV файла с использованием только встроенных инструментов?

Примечание. Каждая строка имеет различное количество токенов, поэтому мы не можем просто игнорировать строки и делиться только запятыми. Также мы можем предположить, что поля будут содержать только буквенно-цифровые данные ascii (никаких специальных символов или других трюков). Кроме того, я не хочу проходить параллельную обработку, хотя она может работать эффективно.

изменить

Он может включать только встроенные инструменты, поставляемые с Perl 5.8. По бюрократическим причинам я не могу использовать сторонние модули (даже если они размещены на cpan)

другое редактирование

Предположим, что нашему решению разрешено иметь дело только с файловыми данными, когда оно полностью загружено в память.

еще одно редактирование

Я просто понял, насколько глупый этот вопрос. Извините за то, что потратил ваше время. Голосование закрывается.

4b9b3361

Ответ 1

Правильный способ сделать это - на порядок - использовать 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);

Ответ 2

Вот версия, которая также учитывает кавычки (например, 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

Как говорили другие люди, правильный способ сделать это - Text::CSV, и либо конец Text::CSV_XS (для FASTEST) или Text::CSV_PP назад (если вы не можете скомпилировать модуль 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.
}

С помощью этого метода данные копируются только немного за раз из скаляра.

Ответ 4

Вы можете сделать это за один проход, если вы читаете файл по строкам. Нет необходимости сразу читать все это в памяти.

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

     # now parse the csv however you want.

}

Не уверен, что это значительно эффективнее, но Perl довольно быстро работает при обработке строк.

ВАМ НУЖНО СЪЕЗДИТЬ ВАШ ИМПОРТ, чтобы узнать, что вызывает замедление. Если, например, вы делаете вставку db, которая занимает 85% времени, эта оптимизация не будет работать.

Изменить

Хотя это похоже на кодовый гольф, общий алгоритм состоит в том, чтобы прочитать весь файл или часть fie в буфер.

Повторяйте байты по байту через буфер, пока не найдете разделитель csv, или новую строку.

  • Когда вы найдете разделитель, увеличьте количество столбцов.
  • Когда вы находите новую строку, увеличивайте количество строк.
  • Если вы попали в конец своего буфера, прочитайте больше данных из файла и повторите.

Что это. Но чтение большого файла в память на самом деле не самый лучший способ, см. Мой первоначальный ответ для обычного способа, которым это делается.

Ответ 5

Предположим, что ваш файл 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

Отвечая на ограничения, налагаемые этим вопросом, вы все равно можете вырезать первый раскол, разбивая ваш входной файл на массив, а не на скаляр:

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);
}

И даже если вы не можете установить (версия pure-Perl) Text::CSV, вы сможете уйти с подтягиванием исходного кода на CPAN и скопировать/вставить код в свой проект...