如何使用Perl解析二进制数据

解析二进制数据是那些似乎很少出现但很有用的任务之一。许多常见的文件类型,如图像、音乐、时间戳、网络数据包和认证日志,都是以二进制形式存在的。不幸的是,它远不如电影《黑客》中的虚构描述那样令人兴奋。不过,好消息是使用Perl的unpack函数解析二进制数据非常简单。我将为您讲解处理二进制数据所需的三个步骤。

1. 打开一个二进制文件句柄

首先,正确地打开一个指向二进制文件的文件句柄

use autodie;
open my $fh, '<:raw', '/usr/share/zoneinfo/America/New_York';

这是一个足够现代的Perl开始。我开始导入autodie,这将确保代码在调用任何函数失败时会抛出异常。这避免了重复的... or die "IO failed"类型代码结构。

接下来,我使用:raw IO层打开一个指向二进制文件的文件句柄。这将避免换行符转换问题。这里不需要binmode。我打开的文件是纽约时区变更的历史记录,来自tz数据库

2. 读取一些字节

所有二进制文件都遵循特定的格式。在zoneinfo文件的情况下,前44个字节/八位字节是头部,所以我会抓取这些

use autodie;
open my $fh, '<:raw', '/usr/share/zoneinfo/America/New_York';

my $bytes_read = read $fh, my $bytes, 44;
die 'Got $bytes_read but expected 44' unless $bytes_read == 44;

在这里,我使用read读取44个字节的数据到变量$bytes中。read函数返回读取的字节数;检查这是一个好习惯,因为如果到达文件末尾,read可能不会返回预期数量的字节。在这种情况下,如果文件在头部之前结束,我们知道数据有问题,就退出。

3. 将字节解包到变量中

现在到了有趣的部分。我必须将$bytes中的数据拆分到单独的Perl变量中。tzfile 手册页定义了头部格式

时区信息文件以“TZif”魔术字符开始,以识别它们为时区信息文件,然后是一个字符,表示文件格式的版本(截至2005年,为ASCII NUL(’\0’)或‘2’),然后是包含保留用于未来用途的15个字节的字符,最后是6个四字节的长整型值

Tzfile手册

unpack函数接受要读取的二进制数据的模板(这定义在pack 文档中),并返回Perl变量。我将匹配头部描述与模板代码,以设计模板。

描述 示例 类型 长度 模板代码
魔术字符 TZif 字符串 4 a4
版本 2 字符串 1 a
保留 0 忽略 15 x15
数字 244 长整型 1 N N N N N N

头部以“TZif”魔术字符开始,这是4个字节。模板代码a4与这个匹配。接下来是版本,这是一个单个ASCII字符,由a匹配(字符串不是空格或空终止的,我本可以使用A)。接下来的15个字节是保留的,可以忽略,所以我使用x15跳过它们。最后有6个长整型数字。每个都是一个单独的变量,所以我们必须写N 6次而不是N6

use autodie;
open my $fh, '<:raw', '/usr/share/zoneinfo/America/New_York';

my $bytes_read = read $fh, my $bytes, 44;
die 'Got $bytes_read but expected 44' unless $bytes_read == 44;

my ($magic, $version, @numbers) = unpack 'a4 a x15 N N N N N N', $bytes;

这段代码将模板传递给unpack,然后返回我们需要的变量。现在它们变成了Perl变量,困难的部分已经完成。对于tzfile,头部定义了文件主体的长度,因此我可以使用这些变量来计算还需要从文件中读取多少数据。

如果您想了解如何解析tzfile的其余部分,请查看我的模块源代码Time::Tzfile

故障排除

有时您会解包一些二进制数据并得到垃圾。这发生在传递给unpack的模板与二进制数据不匹配时。您首先可以做的事情是将二进制数据通过hexdump打印到终端。

以下是纽约tzfile的前44字节

$ hexdump -c -n 44 /usr/share/zoneinfo/America/New_York
0000000   T   Z   i   f   2  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000010  \0  \0  \0  \0  \0  \0  \0 005  \0  \0  \0 005  \0  \0  \0  \0
0000020  \0  \0  \0 354  \0  \0  \0 005  \0  \0  \0 024

这给了您逐字节检查数据并查看它是否与模板匹配的机会。为了创建匹配二进制数据的模板,一次取一个值。考虑您要尝试匹配的值的类型。获取正确的位长度,并且对于数字,确保您知道它是有符号的还是无符号的。

其他需要注意的事情是数据的字节序。通常手册页面会说明变量在“标准”或“网络”顺序。这意味着大端序。tzfile中有几个32位有符号整数以大端序排列。没有匹配该类型的unpack模板代码。为了匹配它,我需要使用l>。其中l匹配有符号32位整数,而>是一个修饰符,它告诉Perl该值是大端序。

通过Perl的内置模板类型和修饰符,您可以匹配任何二进制数据。

更多二进制解析示例

  • 使用Perl进行数据转换的第7.2节中,Dave Cross展示了如何解析png和mp3文件。
  • 在Perl Monk的Perl黑客忏悔帖子中也有一些有用的回复。
  • Perl Monk的Pack/Unpack教程包含有关模板类型的一些有用信息。
  • 来自有效的Perl编程的第117篇帖子“使用pack和unpack进行数据转换”展示了如何使用unpack进行固定宽度数据。
  • 官方Perl文档也有一份pack/unpack教程


本文最初发布在PerlTricks.com

标签

David Farrell

David是一位职业程序员,他经常推文并在博客上关于代码和编程艺术发表文章。

浏览他们的文章

反馈

这篇文章有什么问题吗?请通过在GitHub上打开问题或拉取请求来帮助我们。