Как мне эффективно проанализировать файл CSV в Perl? - PullRequest
25 голосов
/ 17 июня 2010

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

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

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

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

1012 * редактировать *

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

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

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

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

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

Ответы [ 6 ]

44 голосов
/ 18 июня 2010

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

Вот версия, которая также учитывает кавычки (например, 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)
}
8 голосов
/ 18 июня 2010

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

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

2 голосов
/ 17 июня 2010

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

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

     # now parse the csv however you want.

}

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

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

Редактировать

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

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

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

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

1 голос
/ 18 июня 2010

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

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 и скопировать / вставить код в ваш проект ...

1 голос
/ 18 июня 2010

Предполагается, что ваш 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]}, $_; }
}
...