在Python中解析C结构体

2 投票
2 回答
9915 浏览
提问于 2025-04-17 20:07

我知道我可能做错了很多事情,现在遇到了一些问题。我把一系列的 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 个回答

0

你应该使用标准库中的 struct 模块,因为你是在解析一个二进制文件格式。ctypes 模块是用来把共享库(DLL)和二进制接口整合到 Python 应用里的。我不是说你想做的事情不可能,但使用 ctypes 比直接从二进制文件解析 C 结构要复杂得多。

记住,在 C 语言中并没有所谓的 PWIN32_FIND_DATAW 指针。这只是一个类型定义,最终会变成原始的 C 数据类型,比如 32 位指针、64 位指针等等。文件中的数据表示的是原始的基本 C 数据类型。

针对评论的回答……要避免寻找捷径。你确实需要深入理解写入文件的每一位数据以及它们是如何组织的。为此,你可能需要做一些十六进制转储,检查实际的数据表示。根据微软的资料,这并不是一个复杂的结构。如果 wintypes 中的结构对你不起作用,可能是你发现了一个 bug。也有可能磁盘上的结构和内存中的结构并不完全相同。通常,内存中的数据结构会包含填充,以保持在 16 或 64 字节边界上的对齐。但程序员有时不会直接转储结构,而是会拆分它并输出到文件中,去掉填充部分。由于 ctypes/wintypes 是为了调用 DLL 的二进制 API 设计的,它的数据布局可能会包含填充。但文件中可能并不包含这些填充。

3

正如评论中提到的,这个问题是因为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字节的块,然后像上面那样解码。

撰写回答