在Python中解析C结构体
我知道我可能做错了很多事情,现在遇到了一些问题。我把一系列的 WIN32_FIND_DATAW
结构写到了磁盘上,想在我的 Python 脚本中读取和解析它们。
我现在用的代码是:
>>> fp = open('findData', 'r').read()
>>> data = ctypes.cast(fp, ctypes.POINTER(wintypes.WIN32_FIND_DATAW))
>>> print str(data[0].cFileName)
第一个问题是,第三行没有像我预期的那样打印出一个漂亮的字符串。它没有打印出 $Recycle.Bin
,而是显示了 UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128)
这样的错误信息。
这是直接打印存储的数据的结果:
>>> data[0].cFileName
u'\U00520024\U00630065\U00630079\U0065006c\U0042002e\U006e0069'
看起来还算合理。$
是 ASCII 码 0x24,R
是 ASCII 码 0x52,依此类推。
那么,为什么我不能像打印字符串那样打印它呢?
我的第二个问题是,执行:
>>> data[1].cFileName
得到了一些荒谬的数据。我很确定我没有正确使用 ctypes.cast
。我应该怎么做才能访问这些数据呢?为了说明这一点,在 C 语言中,我只需将一个 PWIN32_FIND_DATAW
指针指向缓冲区的开头,然后用类似的代码访问数组中的各个结构,而我想在 Python 中做到同样的事情。
更新
执行:
>>> data[0].cFileName.encode('windows-1252')
出现了这个错误:
UnicodeEncodeError: 'charmap' codec can't encode characters in position 0-5: character maps to <undefined>
更新
第一个条目的开头(data[0]
到 cFileName 的第一部分)看起来像这样:
user@ubuntu:~/data$ hexdump -C findData | head -n 6
00000000 16 00 00 00 dc 5a 9f d2 31 04 ca 01 ba 81 89 1a |.....Z..1.......|
00000010 81 e2 cd 01 ba 81 89 1a 81 e2 cd 01 00 00 00 00 |................|
00000020 00 00 00 00 00 00 00 00 00 00 00 00 24 00 52 00 |............$.R.|
00000030 65 00 63 00 79 00 63 00 6c 00 65 00 2e 00 42 00 |e.c.y.c.l.e...B.|
00000040 69 00 6e 00 00 00 00 00 00 00 00 00 00 00 00 00 |i.n.............|
00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
如果需要,我可以提供更多数据。
2 个回答
你应该使用标准库中的 struct
模块,因为你是在解析一个二进制文件格式。ctypes
模块是用来把共享库(DLL)和二进制接口整合到 Python 应用里的。我不是说你想做的事情不可能,但使用 ctypes
比直接从二进制文件解析 C 结构要复杂得多。
记住,在 C 语言中并没有所谓的 PWIN32_FIND_DATAW 指针。这只是一个类型定义,最终会变成原始的 C 数据类型,比如 32 位指针、64 位指针等等。文件中的数据表示的是原始的基本 C 数据类型。
针对评论的回答……要避免寻找捷径。你确实需要深入理解写入文件的每一位数据以及它们是如何组织的。为此,你可能需要做一些十六进制转储,检查实际的数据表示。根据微软的资料,这并不是一个复杂的结构。如果 wintypes 中的结构对你不起作用,可能是你发现了一个 bug。也有可能磁盘上的结构和内存中的结构并不完全相同。通常,内存中的数据结构会包含填充,以保持在 16 或 64 字节边界上的对齐。但程序员有时不会直接转储结构,而是会拆分它并输出到文件中,去掉填充部分。由于 ctypes/wintypes 是为了调用 DLL 的二进制 API 设计的,它的数据布局可能会包含填充。但文件中可能并不包含这些填充。
正如评论中提到的,这个问题是因为Windows和Linux之间的差异。ctypes
模块试图适应本地环境,所以会出现不匹配的情况。最好的解决办法是使用struct
模块,这样可以在不同的平台上都能正常工作。下面的代码展示了如何处理单个记录。
# Setup test data based on incomplete sample
bytes = "\x16\x00\x00\x00\xdc\x5a\x9f\xd2\x31\x04\xca\x01\xba\x81\x89\x1a\x81\xe2\xcd\x01\xba\x81\x89\x1a\x81\xe2\xcd\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x24\x00\x52\x00\x65\x00\x63\x00\x79\x00\x63\x00\x6c\x00\x65\x00\x2e\x00\x42\x00\x69\x00\x6e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
bytes = bytes + "\x00"*(592-len(bytes))
import struct
import codecs
# typedef struct _WIN32_FIND_DATA {
# DWORD dwFileAttributes;
# FILETIME ftCreationTime;
# FILETIME ftLastAccessTime;
# FILETIME ftLastWriteTime;
# DWORD nFileSizeHigh;
# DWORD nFileSizeLow;
# DWORD dwReserved0;
# DWORD dwReserved1;
# TCHAR cFileName[MAX_PATH];
# TCHAR cAlternateFileName[14];
fmt = "<L3Q4L520s28s"
attrs, creation, access, write, sizeHigh, sizeLow, reserved0, reserved1, name, alternateName = struct.unpack(fmt, bytes)
name = codecs.utf_16_le_decode(name)[0].strip('\x00')
alternateName = codecs.utf_16_le_decode(alternateName)[0].strip('\x00')
print name
注意:这里假设MAX_PATH的大小是260(这通常是对的,但谁也不能保证)。
要从文件中读取所有值,你需要一次读取592字节的块,然后像上面那样解码。