распаковка неизвестного сериализованного формата с переменной длиной

Я использую Perl (5.8.8, не спрашивайте) и просматриваю сериализованный двоичный файл, из которого хочу проанализировать и извлечь информацию.

Формат следующий:

  • 7 байт, значение которых мне неизвестно (DB DB 00 00 7A 03 00)
  • ноль (0x00)
  • 7-байтовая строка с идентификатором пользователя
  • ноль (0x00)
  • 12-байтовая строка, которую нужно отбросить
  • ноль (0x00)
  • 3-байтовое число, указывающее количество элементов, за которыми следует
  • ноль (0x00)
  • строка переменной длины для первого элемента
  • новая строка (0x0a)
  • строка переменной длины для второго элемента
  • новая строка (0x0a)
  • и т.д ...
  • ноль (0x00)
  • 7-байтовая строка с идентификатором пользователя
  • и т.д ...

Мой текущий код несколько наивно пропускает первые 8 байтов, затем считывает байт за байтом, пока не достигнет нуля, а затем выполняет очень специфический синтаксический анализ.

sub readGroupsFile {
    my %index;

    open (my $fh, "<:raw", "groupsfile");
    seek($fh, 8, 0);
    while (read($fh, my $userID, 7)) {
        $index{$userID} = ();
        seek($fh, 18, 1);
        my $groups = "";
        while (read($fh, my $byte, 1)) {
            last if (ord($byte) == 0);
            $groups .= $byte;
        }
        my @grouplist = split("\n", $groups);
        $index{$userID} = \@grouplist;
    }
    close($fh);

    return \%index;
}

Хорошие новости? Оно работает.

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

Я думаю, что unpack() и его шаблоны могут дать ответ, но я не могу понять, как он может работать с массивами строк переменной длины с их собственной переменной длиной.


person bluppfisk    schedule 02.08.2019    source источник
comment
Почему вы ищете 18? вроде должно быть 20 по приведенным вами данным: 0x0 + 15 byte str + 0x0 + 2 byte + 0x0 = 20   -  person Håkon Hægland    schedule 02.08.2019
comment
Я думаю, проблема в том, что unpack нужна строка для распаковки, она не может читать напрямую из файла. Итак, вам нужно сначала прочитать файл в строку, но это может быть не очень эффективно по сравнению с тем, что у вас есть.   -  person Håkon Hægland    schedule 02.08.2019
comment
извините, на самом деле это 12-байтовая строка, а число может быть 3 байта. +3 нуля - это 18. Я просчитался. Но настоящая проблема заключается в следующем за ним массиве. Я подкорректировал пост.   -  person bluppfisk    schedule 02.08.2019
comment
Как только вы доберетесь до строки переменной длины, элементы могут переключиться на readline (он же <>), так как все они заканчиваются символом новой строки. Я думаю, нет причин не смешивать read и <> (только не sysread, который не буферизован).   -  person zdim    schedule 02.08.2019
comment
Я не понимаю, в чем проблема с массивами переменной длины... что вы не знаете, сколько существует строк переменной длины? Как только вы переключитесь на readline, вы сможете проверить каждую прочитанную строку на наличие 7-байтовой строки, заканчивающейся нулем; по описанию кажется, что ни один предмет не может быть таким.   -  person zdim    schedule 02.08.2019
comment
Или: установите local $/ в nul, затем прочитайте четыре таких строки (отбросить -- идентификатор пользователя -- отбросить -- количество элементов, которые нужно следовать); Затем измените $/ обратно на новую строку и, используя последнюю только что прочитанную (количество элементов), прочитайте столько строк (элементов). Повторить?   -  person zdim    schedule 02.08.2019


Ответы (2)


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

{
   my $file = do { local $/; <> };

   $file =~ s/^.{8}//s
      or die("Bad data");

   while (length($file)) {
      $file =~ s/^([^\0]*)\0[^\0]*\0[^\0]*\0([^\0]*)\0//
         or die("Bad data");

      my $user_id = $1;
      my @items = split(/\n/, $2, -1);
      ...
   }
}

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

{
   my $buf = '';
   my $not_eof = 1;

   my $reader = sub {
      $not_eof &&= read(\*ARGV, $buf, 1024*1024, length($buf));
      die($!) if !defined($not_eof);
      return $not_eof;
   };

   while ($buf !~ s/^.{8}//s) {
      $reader->()
         or die("Bad data");
   }      

   while (length($buf) || $reader->()) {
      my $user_id;
      my @items;
      while (1) {
         if ($buf =~ s/^([^\0]*)\0[^\0]*\0[^\0]*\0([^\0]*)\0//) {
            $user_id = $1;
            @items = split(/\n/, $2, -1);
            last;
         }

         $reader->()
            or die("Bad data");
      }

      ...
   }
}
person ikegami    schedule 02.08.2019
comment
Интересный подход. Таким образом, второй подход не будет потреблять столько памяти, как первый подход, если файл большой, но скорость будет примерно такой же, я думаю? - person Håkon Hægland; 03.08.2019
comment
@Håkon Hægland, если размер записей обычно меньше размера буфера, то он должен быть сопоставим по скорости (поскольку циклы эффективно превращаются в операторы if). Вы можете заставить его использовать меньше памяти, извлекая поле за раз, а не запись за раз, но это, вероятно, не нужно. /// Обратите внимание, что плохой файл (например, большой файл без каких-либо NUL) может в конечном итоге быть полностью загруженным в память, но этого легко избежать, выдав ошибку в &$reader, если буфер станет слишком большим. - person ikegami; 03.08.2019
comment
Да, это кажется разумным. Но еще кое-что: во втором фрагменте я должен заменить $file на $buf? - person Håkon Hægland; 03.08.2019
comment
Спасибо! Я до сих пор не могу понять, как это будет работать на границе между блоками. Допустим, запись не заканчивается точно на границе буфера. Тогда внутренний while выйдет из строя, и будет вызван следующий reader->(), который перезапишет весь буфер. Не потеряется ли тогда эта запись? - person Håkon Hægland; 03.08.2019
comment
@Håkon Hægland, &$reader не перезаписывает; это добавляет. (Обратите внимание на четвертый аргумент read.) - person ikegami; 03.08.2019
comment
Конечно, я полностью пропустил, что length($buf) — это смещение в вызове read. - person Håkon Hægland; 03.08.2019
comment
интересное использование Perl. Я ясно показываю свои PHP-начала :) - person bluppfisk; 03.08.2019
comment
Я бы использовал тот же подход в PHP. - person ikegami; 04.08.2019
comment
Это был самый быстрый метод (в среднем примерно на 0,01 с быстрее) с буфером 1024 байта. Как ни странно, мне пришлось изменить регулярное выражение на s/^([^\0]*)\0([^\0]*)\0([^\0]*)\0([^\0]*)\0([^\0]*)\0([^\0]*)\0//) (uid, dn, n_groups, expires, groups — группы захвата не нужны). Даже после исправления бита данных, который я пропустил (срок действия истекает), это все еще на одно поле больше, чем на самом деле в двоичном формате. Может быть, звездочка ведет себя не совсем так, как ожидалось, с двоичным кодом? С плюсом (+) у меня не было спичек. - person bluppfisk; 05.08.2019

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

Задайте для $/ variable нулевой байт и прочитайте первые 4 (четыре) таких «линии». Там вы получаете свой идентификатор пользователя, а затем последняя прочитанная «строка» — это количество следующих за ним элементов. Восстановите $/ на новую строку и прочитайте этот список, используя обычный readline (он же <>). Повторите, если этот шаблон действительно повторяется.

use warnings;
use strict;
use feature 'say';

my $file = shift or die "Usage: $0 file\n";  # a_file_with_nuls.txt    
open my $fh, '<', $file or die "Can't open $file: $!"; 

my ($user_id, $num_items);
while (not eof $fh) {    
    READ_BY_NUL: { 
        my $num_of_nul_lines = 4;
        local $/ = "\x00"; 
        my $line;
        for my $i (1..$num_of_nul_lines) { 
            $line = readline $fh;
            chop $line;
            if ($i == 2) {
                $user_id = $line;
            }
        }   
        $num_items = $line;  # last nul-terminated "line"
    }        
    say "Got: user-id = |$user_id|, and number-of-items = |$num_items|";    

    my @items;
    for (1..$num_items) {
        my $line = readline $fh;
        chomp $line;
        push @items, $line;
    }    
    say for @items;
};

Поскольку $/ задается с помощью local в блоке READ_BY_NUL, его предыдущее значение восстанавливается из блокировать.

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

Все дело в while с ручной проверкой (и завершением) с использованием eof, исходя из предположения, что шаблон four-nuls + number-of-lines действительно повторяется (немного неясно из вопроса).

Я тестирую файл, созданный

perl -wE'say "toss\x00user-id\x00this-too\x003\x00item-1\nitem2\nitem 3"' 
    > a_file_with_nuls.txt

который затем добавляется несколько раз, чтобы дать нам что-то для этого цикла while.

Наконец, сделайте это чтение <:raw в системах, которым это нужно, и unpack по мере необходимости. Смотри ниже.


Как указано в вопросе, (некоторые?) данные являются двоичными, поэтому то, что читается выше, должно быть upack-ed. Это также означает, что могут возникнуть проблемы с чтением нулевых байтов — как эти данные были записаны в первую очередь? Незаполненные части этих полей с фиксированной шириной могут быть заполнены точно нулевыми значениями.

Другой вариант — просто прочитать строки и распаковать первую (а затем unpack одну строку каждый раз после того, как будет прочитано заданное количество строк, указанное как «элементы»).

open my $fh, '<:raw', $file or die "Can't open $file: $!"; 

my @items;
my $block_lines = 1;

while (my $line = <$fh>) { 
    chomp $line;
    if ( $. % $block_lines == 0 ) {
        my ($uid, $num_items) = unpack "x8 A7x x13 i3x", $line;
        say "User-id: $uid, read $num_items lines for items";
        $block_lines += 1 + $num_items;
    }   
    else {
        push @items, $line;
    }
}
say for @items;

Здесь количество пропущенных байтов (x8 и x13) включает ноль.

Это предполагает, что количество «элементов» (строк) для чтения в каждом «блоке» может быть разным, и складывает их по ходу (плюс строка с нулевыми значениями для общего количества запущенных $block_lines), чтобы можно было проверить, когда это снова на строке с нулями ($. % $block_lines == 0)

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

person zdim    schedule 02.08.2019
comment
смогу проверить, когда вернусь в офис в понедельник, но одно замечание состоит в том, что количество элементов на самом деле в двоичном формате. Достаточно ли просто прочитать его, а затем распаковать? - person bluppfisk; 03.08.2019
comment
@bluppfisk Ах да, все должно быть в порядке. (И вы хотите открыть в <raw, как и вы.) - person zdim; 03.08.2019
comment
@bluppfisk Если макет ваших реальных данных значительно отличается, опубликуйте некоторые, чтобы я мог проверить - person zdim; 03.08.2019
comment
@bluppfisk Обновлено, но это может потребовать доработок — дайте мне знать, как это происходит - person zdim; 05.08.2019
comment
заставил это работать с небольшими изменениями (необходимо прочитать еще один NULL перед продолжением цикла while), но решение ikegami немного быстрее. Спасибо, что показали мне эти варианты, всегда рад узнать больше. - person bluppfisk; 05.08.2019