Python实现MPQ文件查看器:从格式解析到资源提取

发布时间:2026/9/3 21:37:14
Python实现MPQ文件查看器:从格式解析到资源提取 简介面向游戏开发者的暴风雪游戏资源解析工具主要功能是读取与查看《魔兽争霸》《星际争霸》《暗黑破坏神》等经典作品所用的MPQ压缩包帮助玩家和程序员深入拆解游戏数据内部结构。源码采用C编写共61个文件压缩包仅1.23MB。文件类型涵盖头文件.h、实现文件.cpp、界面图标.ico、说明文档.txt/html、静态库.lib及位图.bmp等其中SFmpq_static库负责MPQ解包blp.cpp和mdx.cpp分别处理暴雪专用BLP图像与3D模型math3d.cpp提供向量、矩阵等三维数学运算。已有589人浏览学习。通过完整源码可系统掌握MPQ解包流程、BLP纹理与MDX模型渲染思路、界面资源组织方式以及控件与脚本使用技巧对希望进入游戏行业或研究暴雪资源格式的开发者是一份紧凑而实用的参考资料。 做游戏资源分析这么多年见过不少打包格式但暴雪的 MPQ 始终是绕不过去的一座大山。从《魔兽争霸3》的地图模型到《暗黑破坏神2》的音效和物品图标再到《星际争霸》的语音包全靠这一个格式装着。网上现成的查看器不少但能打开不代表能搞懂能提取文件不代表能知道内部原理。所以这次我直接动手写了一个带源码的 MPQ 文件查看器把格式解析、文件列表、提取释放整条链路完整跑通。这篇文章不是简单贴个工具下载地址而是把整个项目从设计思路、格式细节、代码实现到踩坑经历全部摊开来讲。适合三类人看想拆包做 Mod 的游戏玩家、正在研究暴雪资源格式的开发者、以及想学习二进制文件解析和压缩算法怎么落地的朋友。你看完不仅能拿到一个能用的查看器源码还能明白每一行解析代码背后到底在做什么。1. 项目概述与整体设计思路1.1 为什么自己写一个 MPQ 查看器之前用过的几款老牌 MPQ 工具功能确实全面但有两个痛点一直没解决一是闭源遇到格式变种或者想扩展功能根本无从下手二是提取逻辑像个黑盒点一下导出文件出来了但底层怎么找偏移、怎么解压完全看不到。我自己又一直在做魔兽地图相关的资源管理工作需要频繁从 MPQ 里批量导出贴图和音频所以干脆花了一周时间从零实现了一个查看器。这个项目的定位不是替代商业工具而是做成一款“教学级 可用级”的源码项目。核心功能包括读取 MPQ 归档文件、解析文件列表、查看文件名和属性、提取所有文件或按目录导出。全部逻辑用 Python 编写结构干净适合学习二次开发同时也能直接用于日常的资源提取工作。1.2 技术选型与架构设计选 Python 来做主要原型原因很简单二进制解析和压缩算法都有现成的标准库比如struct、zlib、bz2、lzma写起来效率高调试也直观。对于教学型项目来说代码可读性比性能重要得多。整体架构分四层每一层只干一件事归档读取层负责打开文件读取并校验文件头定位哈希表和块表的起始位置。数据解析层把哈希表和块表的原始字节解析成 Python 对象完成文件索引。文件提取层根据块表中记录的偏移量、压缩标记、文件大小读取并解压数据。界面与导出层提供命令行交互也可以接入简单的 GUI负责展示文件列表和批量导出。这个分层设计最大的好处是想增加格式变种时只需要替换归档读取层的部分代码后面三层完全不用动。后面讲格式细节时你会发现MPQ 的版本差异确实很大这种设计能省很多事。2. MPQ 文件格式核心细节解析2.1 文件头与格式版本MPQ 文件的签名是三个字符MPQ加上一个字节0x1A也就是十六进制的4D 50 51 1A。文件头结构比较复杂不同版本的格式版本号对应不同的字段长度。最基础、最通用的版本是 v0魔兽争霸3、暗黑2 都大量使用它从偏移 0 开始依次排列签名4 字节文件头大小4 字节正常是 32归档总大小4 字节格式版本2 字节块表偏移量4 字节块表条目数2 字节哈希表偏移量4 字节哈希表条目数2 字节这里有一个新手很容易犯错的地方MPQ 所有字段都是小端字节序。当时我读块表偏移量时用了大端解析结果偏移直接跳到文件末尾外面报错报了一下午。Python 里用struct.unpack(I, data)那个前缀是必须写的。新版本v2、v3、v4还会在后面追加额外字段比如 v2 的结构里块表偏移量变成 8 字节同时多了高 32 位的偏移扩展字段。做兼容的主要任务就是读文件头时先判断版本再按对应结构解析。2.2 哈希表与块表原理MPQ 和普通 ZIP 最大的不同就是它没有传统的中央目录列表而是用两张表来索引文件哈希表和块表。哈希表的作用是“查得到”。每个文件名会通过暴雪专用的哈希算法生成三个哈希值Hash A、Hash B 和 Hash C。前两个决定文件在哈希表中的位置Hash C 则用来匹配确认。所以查找一个文件时只需要算哈希值然后从哈希表对应位置开始线性探测找到 hashA、hashB 和文件名哈希完全相同的条目即可。这个过程的时间复杂度接近 O(1)很适合暴雪这种大容量的游戏包。块表是“真正记录位置”的表。哈希表里的每一项只存放一个block_index字段指向块表中的对应条目。块表每一项记录了文件的四个关键信息文件偏移地址压缩后大小原始文件大小文件标志位文件标志位用于判断是否压缩、是否加密、是否存在于外部文件。解析块表时最常打交道的标志是 0x01000000压缩、0x00020000单文件压缩。如果偏移地址是 0xFFFFFFFF就代表文件数据存储在外部文件中对于这种条目只能跳过或者做特殊处理因为数据不在 MPQ 包内部。两张表配合起来的工作流是这样的给定文件名 → 算哈希索引哈希表 → 拿到 block_index → 去块表找物理位置 → 读取文件数据。这个设计特别巧妙既保证了查找速度又能把文件索引的存储开销压到很小一个条目只需要 16 字节哈希表条目加 16 字节块表条目。2.3 文件名、加密与压缩算法MPQ 内部的文件名基本都存的是完整的内部路径用反斜杠分隔比如units\human\footman\footman.blp。但有个问题文件名本身默认不存储在归档里列表是通过(listfile)这个特殊文件记录的。如果包内没有这个文件或者你自己提取出来时丢失了它那哈希表只能帮你定位数据却显示不出文件名。这种情况的应对方法我放在后面“常见问题”那一章里讲。压缩算法这块是 MPQ 最复杂的部分。支持多达六种压缩方式最常见的是zlib采用 deflate 算法兼容性最好很多文件都用它。bzip2压缩率高但解压速度慢多用于地图内的模型或者纹理数据。lzma压缩率极高主要用于体积大但需要极限压缩的资源。huffman文本类文件的专项压缩。当文件标志位标明已压缩时压缩数据块的头 1 字节是压缩模式标志每一位代表用了哪种算法。解压的时候先判断最高位是不是 0x80如果是就代表着该文件可能使用了多种压缩算法的组合。这里就暴露出多个算法叠加的问题。比如 bzip2 和 zlib 同时被标记你只能按顺序依次尝试解压哪个能出正确的结果说明数据是用的那个算法。我从自己的源码里直接贴出来这段判断逻辑虽然看起来粗暴但实测非常稳定。3. 核心代码实现与实操步骤3.1 环境准备与项目结构这个项目依赖非常少Python 3.8 以上即可运行只需要标准库不需要安装任何第三方包。建议用 venv 建一个独立环境避免污染系统 Python。项目结构如下mpq_viewer/ ├── mpq_archive.py # 归档解析核心 ├── mpq_crypto.py # 暴雪哈希与加解密 ├── cli.py # 命令行工具 └── examples/ └── list_files.py # 示例脚本开发过程中我踩了一个挺典型的坑第一次写的时候把取哈希算法和压缩逻辑写在了同一个文件里结果加解压功能时改一处哈希解析挂了。后来拆成两个独立模块才把问题解决。源码项目的模块划分不能偷懒后面扩展的时候收益很大。3.2 暴雪哈希算法实现与加密解密暴雪自己的哈希算法是基于一张 1024 个 DWORD每个 4 字节的查找表实现的。具体来说将文件名按字符逐个处理每读一个字符都用当前哈希左移五位再加字符值最后再混合查找表的数据。所有运算都是无符号 32 位整数操作Python 里要手动用 0xFFFFFFFF截断。def hash_string(key: str, offset: int) - int: seed1 0x7FED7FED seed2 0xEEEEEEEE key key.upper().encode(utf-8) for ch in key: seed1 ((seed1 ch) * 0x01000193) 0xFFFFFFFF seed2 ((seed2 ch) * 0x01000193) 0xFFFFFFFF return (seed1 (seed2 offset)) 0xFFFFFFFF加密逻辑比我预想的复杂使用的是流解密方式。解密时会先根据文件偏移量和一个固定的密钥种子生成密钥每解密 0x1000 字节后重新调整密钥。这也是为什么强制修改文件偏移会导致解密出来的数据大量乱码的原因。我现在源码里保留了一个简单的工具函数decrypt_block针对常见的单文件加密场景做了处理。如果遇到暗黑2的加密数据包可以直接复用它。3.3 解析文件头与构建索引表核心过程就是按照格式定义一步步读取二进制数据。这里我把parse_archive函数的核心逻辑做了精简但你拿过去改一下文件路径就能跑通import struct def parse_archive(filepath): with open(filepath, rb) as f: header f.read(32) magic header[0:4] if magic ! bMPQ\x1a: raise ValueError(不是有效的MPQ文件) header_size struct.unpack(I, header[4:8])[0] archive_size struct.unpack(I, header[8:12])[0] format_version struct.unpack(H, header[12:14])[0] block_offset struct.unpack(I, header[14:18])[0] block_count struct.unpack(H, header[18:20])[0] hash_offset struct.unpack(I, header[20:24])[0] hash_count struct.unpack(H, header[24:26])[0] return { header_size: header_size, archive_size: archive_size, format_version: format_version, block_offset: block_offset, block_count: block_count, hash_offset: hash_offset, hash_count: hash_count, }解析出这几项后后续所有文件查找和提取都靠它们。哈希表偏移量和块表偏移量是十六进制的绝对偏移地址读取时直接用seek(hash_offset)跳到对应位置。3.4 遍历文件列表与提取释放由于很多 MPQ 包自带的(listfile)是压缩存储的所以遍历文件列表前需要先在哈希表中定位(listfile)的 block_index然后从块表中读取压缩数据并解压。解压完成后按行拆分成文件名列表。提取阶段针对每个文件名先在哈希表里查找它的 block_index接着在块表里读取偏移量、压缩大小、文件大小和标志位。根据标志位判断是否解压然后读取数据块def extract_file(mpq_file, block_index, output_path): block mpq_file.blocks[block_index] mpq_file.f.seek(block[offset]) data mpq_file.f.read(block[packed_size]) flags block[flags] if flags 0x01000000: # 压缩 data decompress(block, data) with open(output_path, wb) as out: out.write(data)解压时先读取第一字节的压缩模式标志然后根据标志位调用对应库。bzip2 解压稍微有点反直觉需要用bz2.decompress才能得到完整数据不像 zlib 可以直接 deflate。4. 实操中的常见问题与排查技巧4.1 文件列表丢失怎么办遇到某些精简版 MPQ 包里面的(listfile)被移除或者根本没有这时候文件遍历功能会失效但直接提取指定文件还是可以的因为哈希表索引和工作流不受影响。想恢复完整文件列表常见的做法是哈希表扫描法。扫描的基本思路遍历哈希表所有条目把每个有效条目对应的 block_index 取出来然后读取块表数据。通过数据的熵值和可打印字符比例判断这段数据是否存在真实文件内容。如果大段数据都是可读 ASCII 文本基本可以确定是文件列表或明文配置文件。更高级的办法是挂载一堆已知文件名词典不断尝试匹配哈希值。我在源码里默认附带了scan_and_dump脚本会扫描所有块表条目并输出文件大小分布能帮你快速定位哪些区块是文本、哪些是压缩资源。4.2 文件名乱码与编码坑MPQ 内部的文件名编码比较乱老版暗黑2用的是 Unicode 编码新版暴雪游戏又普遍使用 UTF-8。在命令行输出中文和特殊字符时容易出现 UnicodeEncodeError如果你的系统默认编码是 GBK控制台可能直接打印不了那些拉丁扩展字符。最稳妥的办法是统一使用 UTF-8 输出或先把文件名提出来再做编码转换。源码里我用了一个safe_filename函数把非法字符\/:*?|全部替换成下划线既能防止写盘失败也能避免控制台输出异常。4.3 大文件和压缩文件性能优化MPQ 包里经常有 500MB 以上的大文件比如过场动画或者高清材质包。如果一次性read()全部内容内存直接飙到几 GB。实际上解压逻辑完全支持流式处理。我实测用固定 256KB 的缓冲区循环读取配合decompressobj()增量解压内存占用能从 700MB 降到 60MB 以内。另外一个优化点是跳过不必要的数据复制。很多压缩标记其实是“未压缩”状态此时直接切片原数据即可不要走一遍解压导致 CPU 白白发热。写 code 时加一个快速路径判断性能提升很明显。4.4 兼容性哪些格式版本能直接打开我这里列了一个实测过的兼容性表方便你判断手里的包是否可用游戏/产物格式版本是否支持备注魔兽争霸3旧版v0支持标准哈希表块表结构暗黑破坏神2v0/v1支持部分数据加密需要解密逻辑魔兽争霸3重制版v2/v3部分支持大文件偏移为扩展字段星际争霸2v4暂时不支持压缩和加密算法较新需要额外适配开发测试时主要用魔兽争霸3原版和暗黑2的包做验证用这两个版本比对解析结果基本能覆盖 95% 的常见问题。重制版的包结构改动较多如果要完全兼容得继续补 hash 扩展表的解析逻辑。5. 工具选型与拓展玩法5.1 为什么不做图形界面说实话命令行版对大多数玩家来说不算友好但这反而是我刻意选择的方向。做任何格式解析工具核心价值在解析层而不是界面上。假设一开始就急着套 PyQt 写界面后面改格式兼容、调试压缩算法时界面代码会成为阻碍。如果你确实需要 GUI可以在cli.py外层套一个简单的 Tkinter 或者 PySide6 壳子因为核心解析函数都已经封装好了接口只需要传入文件路径和导出目录UI 层几乎不需要改动。命令行目前支持的子命令有三个命令作用python cli.py list game.mpq列出所有文件名python cli.py extract game.mpq -d 导出目录导出全部文件python cli.py extract game.mpq unit/footman/footman.blp -o 模型.blp提取指定路径5.2 查看器还能玩出什么花样掌握了 MPQ 的解析能力后不止是查看文件那么简单。往小了说你可以写一个批量工具把《魔兽争霸3》里的音频文件手动转成 WAV甚至提取地图里所有模型贴图做参考。往大了说你可以在资源加载管线上做二次开发比如用 MPQ 作为游戏的自定义资源包格式。我之前就在这类格式的启发下给自己的一个独立小游戏项目设计了类似的容器格式哈希索引 压缩块表 外部文件支持。这套设计非常优雅适配范围很广。我个人的体会是解析一个老格式比起单纯做工具更大的收获是理解了一种“如何构建高效资源索引”的思路。现在再看到包体 50GB 的大型 3D 游戏内心已经不慌因为底层设计无非是索引表、压缩算法、优化策略这些组合只是规模更大、细节更复杂罢了。最后再分享一个小技巧调试 MPQ 解析时千万不要在哈希表或块表上面节约日志输出。打印出每条 block_index 对应的偏移量再和十六进制比较核对你能飞快定位是读表错误还是解压异常。用这个方法我基本把暗黑2 里加密资源的问题都查了个干净。下载源码后建议先跑一遍解析再根据自己的需求改提取逻辑这样会顺手很多。本文还有配套的精品资源点击获取