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

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

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

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

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

править

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

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

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

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

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

26
задан Andy Lester 17 June 2010 в 22:20
поделиться

6 ответов

Правильный способ сделать это - на порядок больше - использовать 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);
46
ответ дан 28 November 2019 в 06:18
поделиться

Предполагается, что ваш 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]}, $_; }
}
1
ответ дан 28 November 2019 в 06:18
поделиться

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

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

     # now parse the csv however you want.

}

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

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

Edit

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

Итерация байт за байтом по буферу до тех пор, пока не будет найдена делимма csv или новая строка.

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

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

2
ответ дан 28 November 2019 в 06:18
поделиться

Как отмечали другие люди, правильный способ сделать это - 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.
}

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

9
ответ дан 28 November 2019 в 06:18
поделиться

Вот версия, которая также уважает кавычки (например, 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)
}
19
ответ дан 28 November 2019 в 06:18
поделиться

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

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

1
ответ дан 28 November 2019 в 06:18
поделиться
Другие вопросы по тегам:

Похожие вопросы: