解压可变长度的未知序列化格式

unpacking an unknown serialised format with variable length

我正在使用 Perl(5.8.8,不要问),我正在查看一个序列化的二进制文件,我想从中解析和窃取信息。

格式如下:

我当前的代码有些天真地跳过了前 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() 及其模板可能会提供答案,但我无法弄清楚它如何处理具有自己的可变长度的可变长度字符串数组。

根据数据描述,这里有两种减少硬编码细节数量的方法;一个读取那些空字节(然后改回换行符),另一个 unpacks 行带有空值。

设置$/ variable to the null byte, and read first 4 (four) such "lines." You get your user ID there, and then the last such "line" read is the number of items that follows. Restore $/ to newline and read that list, using normal readline (aka <>)。重复,如果此模式确实重复。

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

my $file = shift or die "Usage: [=10=] 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;
};

由于 $/ 是在 READ_BY_NUL 块中使用 local 设置的,因此它的先前值会在块外恢复。

输出符合预期,但请添加检查。此外,可以想象在哪些错误上恢复是有意义的(例如:项目的实际数量低于给定数量)。

整个事情都在 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。这也意味着读取空字节时可能会出现问题——这些数据最初是如何写入的?那些固定宽度字段的未填充部分可以用 nuls.

完全填充

另一种选择是简单地读取行,并且 unpack 第一个(然后 unpack 每次在给定行数之后的一行,指定为 "items," 有已阅读)。

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;

此处要跳过的字节数(x8x13)包括零。

这假设每个 "block" 中要读取的 "items" (行)的数量可能不同,并且随着它的推移将它们加起来(加上带有 nuls 的行,总计 运行 $block_lines) 所以能够检查它何时再次与 nuls ($. % $block_lines == 0)

它对未指定的事物做出了一些其他(合理的)假设。这只是简单地检查了一些数据。

您不知道要读取多少内容,因此一次读取整个文件可以获得最快的速度结果。

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

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

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

      my $user_id = ;
      my @items = split(/\n/, , -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/^([^[=11=]]*)[=11=][^[=11=]]*[=11=][^[=11=]]*[=11=]([^[=11=]]*)[=11=]//) {
            $user_id = ;
            @items = split(/\n/, , -1);
            last;
         }

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

      ...
   }
}