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

Posted

技术标签:

【中文标题】解压缩具有可变长度的未知序列化格式【英文标题】:unpacking an unknown serialised format with variable length 【发布时间】:2019-08-02 13:35:23 【问题描述】:

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

格式如下:

7个字节,不知什么意思(DB DB 00 00 7A 03 00) 空 (0x00) 带有用户ID的7字节字符串 空 (0x00) 要丢弃的 12 字节字符串 空 (0x00) 3 字节数字指定要遵循的项目数 空 (0x00) 第一项的可变长度字符串 换行符 (0x0a) 第二个项目的可变长度字符串 换行符 (0x0a) 等等... 空 (0x00) 带有用户ID的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() 及其模板可能会提供答案,但我无法弄清楚它如何处理具有自己可变长度的字符串的可变长度数组。

【问题讨论】:

你为什么要找18?根据您提供的数据,它似乎应该是 20:0x0 + 15 字节 str + 0x0 + 2 字节 + 0x0 = 20 我认为问题在于 unpack 需要一个字符串来解压,它不能直接从文件中读取。因此,您需要先将文件读入字符串,但这与您所拥有的相比可能效率不高 抱歉,它实际上是一个12字节的字符串,数字可能是3字节。 +3 nulls 是 18。我算错了。但真正的问题在于它后面的数组。我已经调整了帖子。 一旦你进入“可变长度字符串”项目可以切换到readline(又名&lt;&gt;),因为它们都以换行符结尾。我认为没有理由不能混合read&lt;&gt;(只是不是sysread,它是无缓冲的)。 我不明白“可变长度数组”有什么问题......你不知道有多少个“可变长度字符串 i>”有吗?一旦你切换到readline,你就可以测试你读到的每一行是否有一个以null结尾的7字节字符串;根据描述,似乎没有“项目”可以那样。 【参考方案1】:

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

$/ variable 设置为空字节,并读取前4(四)个这样的“行”。您在那里获得您的用户 ID,然后读取的最后一个这样的“行”是后面的项目数。使用普通的readline(又名&lt;&gt;)将$/ 恢复到换行符并读取该列表。重复,如果这种模式确实重复。

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;
;

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

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

整个事情都在 while 中,使用 eof 进行手动检查(和终止),假设模式 four-nuls + number-of-lines 确实重复(a这个问题有点不清楚)。

我用一个文件测试

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

然后将其附加多次,为while 循环提供一些东西。

最后,在需要它的系统上将其设为 &lt;:raw,并在需要时将其设为 unpack。见下文。


如问题中所述,(某些?)数据是二进制的,因此上面读取的内容需要upack-ed。这也意味着读取最多空字节可能会出现问题——这些数据最初是如何写入的?这些固定宽度字段的未填充部分可能会被用 nuls 精确填充。

另一种选择是简单地读取行,unpack 第一个(然后在给定的行数之后,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;

这里要跳过的字节数(x8x13)包括零。

这假设在每个“块”中读取的“项目”(行)的数量可能不同,并将它们相加(加上带有 nuls 的行,总共运行 $block_lines)以便能够检查它何时再次与 nuls ($. % $block_lines == 0) 对齐

它对未指定的事物做出了一些其他(合理的)假设。这只是稍微检查了一下,有一些虚构的数据。

【讨论】:

周一回办公室就可以测试了,但要注意的是,项目编号实际上是二进制的。简单地读取它然后解包就足够了吗? @bluppfisk 啊,对——应该没问题。 (你想在&lt;raw 打开,就像你一样。) @bluppfisk 如果您的真实数据的布局差异很大,请发布一些,以便我测试 @bluppfisk 已更新,但这可能需要调整——让我知道进展如何 通过微小的更改也可以使用它(需要在继续 while 循环之前读取另一个 NULL),但 ikegami 的解决方案稍微快一些。感谢您向我展示这些选项,但总是很高兴了解更多信息。【参考方案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");
      

      ...
   

【讨论】:

有趣的方法。所以如果文件很大,第二种方法不会像第一种方法那样消耗内存,但我猜速度会大致相同? @Håkon Hægland,如果记录的大小通常小于缓冲区大小,那么它的速度应该是相当的(因为循环有效地折叠成“if”语句)。您可以通过一次提取一个字段而不是一次提取一条记录来减少内存的使用,但这可能不是必需的。 /// 请注意,坏文件(例如没有任何 NUL 的大文件)最终可能会完全加载到内存中,但如果缓冲区太大,则可以通过在 &amp;$reader 中抛出错误来轻松避免这种情况。 是的,这似乎是合理的。但还有别的:在第二个 sn-p 中,我应该将 $file 替换为 $buf 吗? 谢谢!我仍然无法看到它将如何在块之间的边界处工作。假设一条记录并不完全在缓冲区边界处结束。然后内部的while 会失败,然后调用下一个reader-&gt;(),这会覆盖整个缓冲区。那记录不会丢失吗? @Håkon Hægland, &amp;$reader 不会覆盖;它附加。 (注意read 的第四个参数。)

以上是关于解压缩具有可变长度的未知序列化格式的主要内容,如果未能解决你的问题,请参考以下文章

如何在 TensorFlow 中处理具有可变长度序列的批次?

具有可变长度序列的 RNN/LSTM 库,无需分桶或填充

为啥压缩然后未压缩不同长度的流

在 Dynamic_RNN 中使用可变序列长度时是不是应该进行丢失屏蔽

Python struct作为网络数据包(未知字节序列)

如何用bitset储存未知长度的序列?