Perl анализирует данные с несколькими символами-разделителями

У меня есть смешанный файл, разделенный символами, со строкой заголовка, которую я пытаюсь прочитать с помощью Text:: CSV, который я успешно использовал в файлах, разделенных запятыми, для извлечения массива хэшей в других сценариях. Я читал, что Text::CSV не поддерживает несколько разделителей (пробелы, табуляции, запятые), поэтому я пытался очистить строку с помощью регулярного выражения перед использованием Text::CSV. Не говоря уже о том, что в файле данных также есть строки комментариев в середине файла. К сожалению, у меня нет прав администратора для установки библиотек, которые могут содержать несколько sep_chars, поэтому я надеялся, что смогу использовать Text::CSV или другие стандартные методы для очистки заголовка и строки перед добавлением в AoH. Или мне следует отказаться от Text::CSV?

Я, очевидно, все еще учусь. Заранее спасибо.

Пример файла:

#
#
#
# name scale     address      type
test.data.one   32768       0x1234fde0      float
test.data.two   32768               0x1234fde4      float
test.data.the   32768       0x1234fde8      float
# comment lines in middle of data
test.data.for   32768                 0x1234fdec      float
test.data.fiv   32768       0x1234fdf0      float

Выдержка из кода:

my $fh;
my $input;
my $header;
my $pkey;
my $row;
my %arrayofhashes;   

my $csv=Text::CSV({sep_char = ","})
    or die "Text::CSV error: " Text::CSV=error_diag;

open($fh, '<:encoding(UTF-8)', $input)
    or die "Can't open $input: $!";

while (<$fh>) {
    $line = $_;
    # skip to header row
    next if($line !~ /^# name/);
    # strip off leading chars on first column name
    $header =~ s/# //g;
    # replace multiple spaces and tabs with comma
    $header =~ s/ +/,/g;
    $header =~ s/t+/,/g;
    # results in $header = "name,scale,address,type"
    last;
}

my @header = split(",", $header);
$csv->parse($header);
$csv->column_names([$csv->fields]);
# above seems to work!

$pkey = 0;
while (<$fh>) {
    $line = $_;
    # skip comment lines
    next if ($line =~ /^#/);
    # replace spaces and tabs with commas
    $line =~ s/( +|\t+)/,/g;
    # replace multiple commas from previous regex with single comma    
    $line =~ s/,+/,/g;
    # results in $line = "test.data.one,32768,0x1234fdec,float"

    # need help trying to create a what I think needs to be a hash from the header and row.
    $row = ?????;
    # the following line works in my other perl scripts for CSV files when using:
    # while ($row = $csv->getline_hr($fh)) instead of the above.  
    $arrayofhashes{$pkey} = $row;
    $pkey++;
}

person Elvis    schedule 18.08.2013    source источник
comment
Это: $arrayofhashes{$pkey} не является массивом хэшей. Это хэш хэшей, где ключами являются индексы. s/t+/,/g не заменяет вкладки, а заменяет ts. Вы также выполняете замены на $header, который пуст. Вы должны использовать use strict; use warnings;, и вы должны ограничить область ваших переменных до минимума, а не объявлять их все в начале скрипта.   -  person TLP    schedule 18.08.2013
comment
@TLP извините, я вручную переписывал с другого компьютера. Спасибо за помощь   -  person Elvis    schedule 18.08.2013


Ответы (1)


Если ваши столбцы разделены несколькими пробелами, Text::CSV бесполезен. Ваш код содержит много повторяющегося кода, пытающегося обойти ограничения Text::CSV.

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

Итак, вы хотите разобрать заголовок.

Нам нужно определение строки заголовка для нашего кода. Возьмем «первую строку комментария, содержащую не пробельные символы». Ему не могут предшествовать строки без комментариев.

use strict; use warnings; use autodie;

open my $fh, '<:encoding(UTF-8)', "filename.tsv";  # error handling by autodie

my @headers;
while (<$fh>) {
  # no need to copy to a $line variable, the $_ is just fine.
  chomp;                                     # remove line ending
  s/\A#\s*// or die "No header line found";  # remove comment char, or die
  /\S/ or next;                              # skip if there is nothing here
  @headers = split;                          # split the header names.
                                             # The `split` defaults to `split /\s+/, $_`
  last;                                      # break out of the loop: the header was found
}

Класс символов \s соответствует символам пробела (пробелы, табуляции, новые строки и т. д.). \S является инверсией и соответствует всем непробельным символам.

Остальные

Теперь у нас есть имена заголовков, и мы можем перейти к обычному разбору:

my @records;
while (<$fh>) {
  chomp;
  next if /\A#/;              # skip comments
  my @fields = split;
  my %hash;
  @hash{@headers} = @fields;  # use hash slice to assign fields to headers
  push @records, \%hash;      # add this hashref to our records
}

Вуаля.

Результат

Этот код создает следующую структуру данных из данных вашего примера:

@records = (
  {
    address => "0x1234fde0",
    name    => "test.data.one",
    scale   => 32768,
    type    => "float",
  },
  {
    address => "0x1234fde4",
    name    => "test.data.two",
    scale   => 32768,
    type    => "float",
  },
  {
    address => "0x1234fde8",
    name    => "test.data.the",
    scale   => 32768,
    type    => "float",
  },
  {
    address => "0x1234fdec",
    name    => "test.data.for",
    scale   => 32768,
    type    => "float",
  },
  {
    address => "0x1234fdf0",
    name    => "test.data.fiv",
    scale   => 32768,
    type    => "float",
  },
);

Эту структуру данных можно использовать как

for my $record (@records) {
  say $record->{name};
}

or

for my $i (0 .. $#records) {
  say "$i: $records[$i]{name}";
}

Критика вашего кода

  • Вы объявляете все свои переменные в верхней части скрипта, фактически делая их глобальными переменными. Не надо. Создавайте свои переменные в наименьшей возможной области. В моем коде во внешней области видимости используются только три переменные: $fh, @headers и @records.

  • Эта строка my $csv=Text::CSV({sep_char = ","}) не работает должным образом.

    • Text::CSV is not a function; it is the name of a module. You meant Text::CSV->new(...).
    • Параметры должны быть хэш-ссылкой, но sep_char = "," пытается назначить что-то для sep_char, к сожалению, это может быть правильный синтаксис. Но на самом деле вы хотели указать отношение ключ-значение. Вместо этого используйте оператор => (называемый толстая запятая или решетка).
  • И это не работает: or die "Text::CSV error: " Text::CSV=error_diag.

    • To concatenate strings, use the . concatenation operator. What you wrote is a syntax error: A literal string is always followed by an operator.
    • Вы действительно любите задания? Text::CSV=error_diag не работает. Вы намеревались вызвать метод error_diag для класса Text::CSV. Поэтому используйте правильный оператор ->: Text::CSV->error_diag.
  • Замена s/t+/,/g заменяет все последовательности t запятыми. Чтобы заменить вкладки, используйте класс символов \t.

  • %arrayofhashes — это не массив хэшей: это хэш (о чем свидетельствует сигила %), но вы используете целые числа в качестве ключей. Массивы имеют сигил @.

  • Чтобы добавить что-то в конец массива, я бы не стал хранить индекс последнего элемента в дополнительной переменной. Вместо этого используйте функцию push, чтобы добавить элемент в конец. Это уменьшает объем бухгалтерского кода.

  • если вы обнаружите, что пишете цикл, подобный my $i = 0; while (condition) { do stuff; $i++}, то обычно вам нужен цикл for в стиле C:

    for (my $i = 0; condition; $i++) {
      do stuff;
    }
    

    Это также помогает с правильной областью видимости переменных.

person amon    schedule 18.08.2013
comment
ТЫВМ! Спасибо, что вытерпели мою нехватку знаний. Мне нужно многому научиться. - person Elvis; 18.08.2013
comment
Я добавил шаг, чтобы пропустить строки комментариев перед строкой заголовка. - person Elvis; 18.08.2013
comment
и моя установка не в autodie установлена. - person Elvis; 18.08.2013
comment
как распечатать хэш с помощью дампера? - person Elvis; 18.08.2013
comment
@Elvis Если у вас не установлен autodie, это означает, что у вас довольно старый perl (старее v5.10.1, которому четыре года). Нет проблем: просто используйте стандартную обработку ошибок, как вы правильно сделали в исходном коде (с ... or die "Can't open $filename: $!"). После use Data::Dumper вы можете отобразить структуру как print Dumper \@records. Это все еще массив, а не хэш. Приведенный выше вывод был получен с помощью модуля Data::Dump. Я не понимаю вашего первого комментария. - person amon; 18.08.2013
comment
Мне пришлось добавить next if ($_ !~ /# name/ );, чтобы пропустить строки комментариев перед строкой заголовка. Из того, что я знаю из хэшей Ruby, первичные ключи должны быть уникальными и не гарантируются, что они будут в каком-либо конкретном порядке при циклическом выполнении. Вот почему я хотел создать числовой уникальный ключ. Также упрощает итерацию по хэшу и выполняет сортировку, а также гарантирует, что я могу записать записи в том же порядке, что и во входном файле. Я предполагаю, что это делает его хэшем хэшей. Или приведенный вами пример представляет собой массив хэшей и гарантирует порядок? - person Elvis; 18.08.2013
comment
@Elvis Мой код пропускает комментарии перед строкой заголовка через /\S/ or next. Конечно, это работает только тогда, когда в других комментариях нет контента, но это имело место в данных вашего примера. Да, хэши в Ruby ведут себя примерно так же, как и в Perl (неупорядоченные, уникальные ключи). Но если ваш ключ находится в непрерывном диапазоне целых чисел, то использование массива более эффективно. Конечно, это сохраняет порядок, как в Ruby. Мой @records представляет собой массив хэш-ссылок. Массивы индексируются как $records[0] со скобками, а не фигурными скобками (это для хэшей). - person amon; 18.08.2013
comment
Здорово! Как мне пройти через AoHrefs в вашем примере? foreach my $record (@records)? И есть ли у меня доступ к отдельным парам хеш-ключ/значение? - person Elvis; 18.08.2013
comment
@Элвис Да, это возможно. Я добавил небольшой пример использования в свой ответ. - person amon; 19.08.2013