Проблема эффективности при создании CSV в Perl - PullRequest
0 голосов
/ 14 января 2011

Я написал скрипт на Perl, который принимает XML, анализирует его и создает CSV.Взятие xml, его синтаксический анализ и сортировка, кажется, идут очень гладко, но как только я попадаю в большие наборы данных (т.е. создаю csv с 10000 строками и 260 столбцами), сценарий начинает занимать огромное количество времени (~ 1 час), покапостроение строки CSV.Я понимаю, что Perl, вероятно, не лучший для объединения строк;но я бы подумал, что это было бы более эффективно, чем это.

В принципе, для сортировки у меня есть два хэша массивов.Один хеш содержит массивы, которые я использовал для сортировки.Другой хеш содержит массивы для всех других столбцов (столбцы, которые я хочу записать в CSV; но не имеют отношения к тому, как я хочу их отсортировать).Таким образом, мой код проблемы (и я убедился, что это блок кода, принимающий навсегда) код выглядит так:

  my $csv = "Header1, Header2, Header3, Header4,...,HeaderN-1,HeaderN\n";
  foreach my $index (@orderedIndecies) {
    my @records = @{$primaryFields{"Important Field 1"}};
    $csv .= $records[$index] ? "$records[$index]," : ",";
    $csv .= $primaryIndex[$index] >= 0 ? "$primaryIndex[$index]," : ",";
    @records = @{$primaryFields{"Important Field 2"}};
    $csv .= $records[$index] ? "$records[$index]," : ",";
    foreach my $key (@keys) {
      @records = @{$csvContent{$key}};
      if($key eq $last) {
        $csv .= $records[$index] ? "$records[$index]\n" : "\n";
      } else {
        $csv .= $records[$index] ? "$records[$index]," : ",";
      }
    }
  }

Я также пробовал то же самое, используя только метод соединения вместо ". =".Я также попытался исключить агрегирование строк и записать их непосредственно в файл.И то и другое не очень помогло.Я буду первым, кто признает, что мои знания по управлению памятью в Perl, вероятно, не самые лучшие;поэтому, пожалуйста, не стесняйтесь учить меня (конструктивно).Кроме того, если вы думаете, что это то, что я должен рассмотреть переписать за пределами Perl, пожалуйста, дайте мне знать.

РЕДАКТИРОВАТЬ: Некоторые примеры XML (пожалуйста, имейте в виду, что я не в состоянии редактировать структуруxml):

<fields>
  <field>
    <Name>IndicesToBeSorted</Name>
    <Records>idx12;idx14;idx18;...idxN-1;idxN</Records>
  </field>
  <field>
    <Name>Important Field1</Name>
    <Records>val1;val2;;val4;...;valn-1;valn</Records>
  </field>
  <field>
    <Name>Important Field2</Name>
    <Records>val1;val2;;val4;...;valn-1;valn</Records>
  </field>
  <field>
    <Name>Records...</Name>
    <Records>val1;val2;;val4;...;valn-1;valn</Records>
  </field>
  <field>
    <Name>More Records...</Name>
    <Records>val1;val2;;val4;...;valn-1;valn</Records>
  </field>
</fields>

Позиция записи в одном поле соответствует позиции в другом поле.Например;первый элемент из каждого элемента «Записи» связан и составляет столбец в моем CSV.В общем, мой сценарий анализирует все это и создает массив упорядоченных индексов (что и есть в @orderedIndecies в моем примере).@OrderdIndecies содержит такие данные, как ...

print "$orderedInecies[0]\n"  #prints index of location of idx0 
print "$orderedInecies[1]\n"  #prints index of location of idx1
print "$orderedInecies[2]\n"  #prints index of location of idx2
print "$orderedInecies[3]\n"  #prints index of location of idx3

Я делаю все так, потому что строка orderIndecies вышла из строя;и я не хотел перемещать все данные.

РЕДАКТИРОВАТЬ: ФИНАЛЬНЫЙ ОТВЕТ

  open my $csv_fh, ">", $$fileNameRef or die "$$fileNameRef: $!";
  print $csv_fh "Important Field 1,Index Field,Important Field 2";

  # Defining $comma, $endl, $empty allows me to do something like:
  #
  #                    print $csv_fh $val ? $val : $empty;
  #                    print $csv_fh $comma;
  # 
  # As opposed to....
  #
  #                    print $csv_fh $val ? "$val," : ",";
  #
  # Note, the first method avoids the string aggregation of "$val,"
  my $comma = ",";
  my $endl = "\n";
  my $empty = "";

  my @keys = sort(keys %csvContent);
  my $last = $keys[-1];
  foreach (@keys) {
    print $csv_fh $_;
    print $csv_fh $_ eq $last ? $endl : $comma;
  }

  # Even though the hash lookup is probably very efficient, I still
  # saw no need to redo it constantly, so I defined it here as 
  # opposed to inline within the for loops
  my @ImportantFields1 =  @{$primaryFields{"Important Field 1"}};
  my @ImportantFields2 =  @{$primaryFields{"Important Field 2"}};

  print "\n\n--------- BUILD CSV START ---------------\n\n";
  foreach my $index (@orderedIndecies) {
    print $csv_fh exists $ImportantFields1[$index] ? $ImportantFields1[$index] : $empty;
    print $csv_fh $comma;
    print $csv_fh $originalIndexField[$index] >= 0 ? $originalIndexField[$index] : $empty;
    print $csv_fh $comma;
    print $csv_fh exists $ImportantFields2[$index] ? $ImportantFields2[$index] : $empty;

    #If needed, this is where you would make sure to escape commas
    foreach my $key (@keys) {
      print $csv_fh $comma;
      $record = exists @{$csvContent{$key}}[$index] 
                     ? @{$csvContent{$key}}[$index];
                     : $empty;
    }
    print $csv_fh $endl;
  }  

  print "\n\n------- CSV Contents wrtten to file -----------\n\n"
  close($csv_fh);

Спасибо за помощь, ребята: D

Ответы [ 3 ]

4 голосов
/ 14 января 2011

Я собрал небольшую программу, которая создает массив из 100 000 случайных строк и еще одну из 260 случайных строк, а затем зацикливает их, объединяя их, распечатывая время, необходимое для того, чтобы добраться до этой части цикла. Для первых 70 000 конкатонированных значений это время, которое у меня уходит на мою программу:

On Key 0, has been 00:00:00
On value 0, has been 00:00:00
On value 10000, has been 00:00:05.4373956
On value 20000, has been 00:00:22.3901951
On value 30000, has been 00:00:51.1552678
On value 40000, has been 00:01:31.0138775
On value 50000, has been 00:02:26.4659378
On value 60000, has been 00:03:32.6834164
On value 70000, has been 00:04:48.4788361

Как видите, сама конкатенация, как правило, не является отличным способом для выполнения операций такого рода. Даже если у вас есть «фиксированный» объем данных, время, необходимое для этого, увеличивается из-за потребности в копировании памяти.

Альтернатива - делать то, что рекомендовано в Perl, - записать это на диск! :) Не делайте никакой конкатенации - записывайте каждую часть на диск. Моя программа-пример, которая закончила свою последнюю строку в 23.2183042 секунды. Это будет выглядеть примерно так:

# a couple utilities

use File::Temp qw/tempdir/;
use File::Spec::Functions; # catdir

# ... and later

my $dir = tempdir();  # give me a temporary, empty place to put it
my $file = catdir($dir, 'temp.csv');

open my $fh, '>', $file
    or die "Can't open '$file' for write: $!";

print {$fh} "Header1, Header2, Header3, Header4,...,HeaderN-1,HeaderN\n";
foreach my $index (@orderedIndecies) {
    my @records = @{$primaryFields{"Important Field 1"}};
    print $records[$index] ? "$records[$index]," : ",";
    print $primaryIndex[$index] >= 0 ? "$primaryIndex[$index]," : ",";
    @records = @{$primaryFields{"Important Field 2"}};
    print $records[$index] ? "$records[$index]," : ",";
    foreach my $key (@keys) {
        @records = @{$csvContent{$key}};
        if($key eq $last) {
            print {$fh} $records[$index] ? "$records[$index]\n" : "\n";
        } else {
            print {$fh} $records[$index] ? "$records[$index]," : ",";
        }
    }
}

close $fh
    or warn "Can't close '$file' for some reason: $!";

Теперь нет ни конкатенации, ни копирования памяти. Это должно идти очень, очень быстро.

4 голосов
/ 14 января 2011

Если вы попытаетесь использовать следующий код вместо свернутого вручную кода, вы увидите, по крайней мере, некоторое увеличение скорости, и я ожидаю, что оно будет довольно драматичным. Text :: CSV_XS для записи CSV (и чтения, если вам это нужно) и XML :: LibXML для анализа XML.Каждый из них использует C под капотом.

Конкатенация строк может быть признаком проблем как для надежности, так и для скорости.

2 голосов
/ 14 января 2011

Нужно ли собирать весь вывод в одну переменную $csv? Более разумный подход заключается в использовании массива, содержащего, скажем, один элемент для каждой записи. Затем вы можете просто напечатать массив в выходной поток или, если вы настаиваете, использовать join для объединения элементов массива в один скаляр.

В каждой записи вы также можете собрать отдельные поля в массив и join в конце

  my @csv = ("Header1, Header2, Header3, Header4,...,HeaderN-1,HeaderN\n");
  foreach my $index (@orderedIndecies) {
    my @records = @{$primaryFields{"Important Field 1"}};
    my @newRow = ();
    push @newRow, $records[$index] ? $records[$index] : "";
    # alternatively:  push @newRow, $records[$index] || "";
    push @newRow, $primaryIndex[$index]>=0 ? $primaryIndex[$index] : "";
    @records = @{$primaryFields{"Important Field 2"}};
    push @newRow, $records[$index] ? $records[$index] : "";
    foreach my $key (@keys) {
      @records = @{$csvContent{$key}};
      push @newRow, $records[$index] ? $records[$index] : "";
    }
    push @csv, join(",", @newRow) . "\n";
  }

  ...
  $csv = join '', @csv;      # if necessary
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...