2010-06-17 2 views
19

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

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

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

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

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

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

других редактировать

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

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

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

+4

Любая причина вам нужно только встроенные инструменты (я не уверен, если предположить нет прав администратора). В противном случае попробуйте использовать модуль pertext. Это упрощает анализ CSV: http://search.cpan.org/~erangel/Text-CSV/CSV.pm –

+5

Зачем читать весь файл и 'split()' по строкам? Если вы просто открываете файл и используете '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' ' – mob

+1

@Mike некоторые модули perl из cpan не требуют компиляции и могут быть использованы без права администратора ... если есть один из них, он все равно будет указан из вашего нужного образца? – Prix

ответ

42

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

Когда вы говорите о более медленном подходе, вы имеете в виду, что этот модуль имеет известные проблемы с производительностью или он немного медленнее? – MikeKulls

+2

@MikeKulls: Я бы не назвал это проблемой производительности как таковой. Это следствие фактического разбора вместо того, чтобы вслепую предположить, что каждая запятая является разделителем полей. Тем не менее, это не «немного медленнее». В простом тесте голый 'split' был на 10-20 раз быстрее, чем' parse_line'. –

+0

Я бы предположил, что он медленнее и потому, что он написан на Perl, а не на C для функции split. Теоретически должно быть возможно получить производительность ближе к функции разделения, например, возможно, на 2-3 раза медленнее. – MikeKulls

2

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

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

    # now parse the csv however you want. 

} 

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

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

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

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

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

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

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

+0

спасибо за ответ. см. правки – Mike

+0

Поскольку perl 5.8, когда файл находится в памяти (например, в переменной с именем '$ scalar'), вы все равно можете использовать итератор filehandle на нем с' open (FILE, "<", \ $ scalar) ' – mob

8

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

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

+8

И второй наиболее правильный способ сделать это - создать модуль 'Mike :: Text :: CSV', скопировать исходный код из« Text :: CSV »в него и добавить отказ от описания того, как он был« вдохновлен », с открытым исходным кодом Text :: CSV. – mob

+0

Мне это нравится! Мне это очень нравится. –

+0

@RobertP, какое имя для $! в конце открытой ... die clause? – olala

1

Предполагая, что вы файл 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]}, $_; } 
} 
+0

Помимо заполнения ваших строк кода, каким образом это лучше, чем 'split'? – mob

+0

@modrule Если вы используете 'split', вам нужно использовать его дважды, поэтому данные будут прочитаны дважды, мое решение будет считывать данные только один раз. // Но это верно, только если данные уже загружены. – ZyX

0

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

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

14

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

Не сворачивайте свою собственную процедуру синтаксического анализа CSV. Легко поступать неправильно и трудно получить право, и это может укусить вас. Пожалуйста, используйте Text :: CSV, как упоминалось другими плакатами. – MichielB

+2

Я бы не сказал, что никогда не катитесь. Что, если вы напишете то, что лучше, чем существующие решения? – MikeKulls

Смежные вопросы