一、.pkg文件格式
.pkg是很多游戏的通用资源打包格式,在QQ的所有游戏都能够见到它的身影,像QQ三国、QQ堂之类。最近还在有小伙伴问我答题器是怎么做的。答题器就只是从数据库查找文本而已,数据就是从pkg文件里提取出来的。解析pkg文件需要知道它的原始格式,pkg文件格式如下:

分为Header、Data、Meta三部分:
- Header:4字节固定标志,4字节文件数,4字节Meta位置偏移,4字节Meta长度
- Data:所有资源文件的压缩数据
- Meta:是一个列表,每个项包含文件名长度、文件名、固定识别标志、文件偏移、文件原始大小、文件压缩大小
我们可以通过Meta偏移找到Meta列表开始的地方,遍历Meta列表,用文件偏移+文件压缩大小从Data区获取压缩数据,然后利用zlib进行解压就得到原始数据了。
二、解压python代码
在他人的轮子上改造,基于python3:
python
# -*- coding: utf-8 -*-
import os, struct, zlib
if __name__ == "__main__":
pkgfilename = r"H:\update.pkg"
outdirname = r"H:\update"
pkgfile = open(pkgfilename, 'rb')
pkgfile.read(4)
filenums, = struct.unpack('I', pkgfile.read(4))
filename_table_offset, = struct.unpack('I', pkgfile.read(4))
filename_table_len, = struct.unpack('I', pkgfile.read(4))
pkgfile.seek(filename_table_offset)
for index in range(filenums):
name_len, = struct.unpack('H', pkgfile.read(2))
name = pkgfile.read(name_len)
pkgfile.read(4)
offset, = struct.unpack('I', pkgfile.read(4))
size, = struct.unpack('I', pkgfile.read(4))
zlib_size, = struct.unpack('I', pkgfile.read(4))
current_pos = pkgfile.tell()
pkgfile.seek(offset)
text = pkgfile.read(zlib_size)
text = zlib.decompress(text)
pkgfile.seek(current_pos)
outfilename = os.path.join(outdirname, os.path.join(os.path.splitext(os.path.basename(pkgfilename))[0], str(name, encoding="utf-8")))
print(u'进度 [%d/%d]: ' % (index + 1, filenums),
os.path.join(os.path.splitext(os.path.basename(pkgfilename))[0], str(name, encoding="utf-8")))
if not os.path.exists(os.path.dirname(outfilename)):
os.makedirs(os.path.dirname(outfilename))
open(outfilename, 'wb').write(text)三、能得到什么数据
可以得到图像、文本等数据,例如答题的数据:

四、压缩python代码
当修改完数据后,需要重新压缩成pkg文件。
这里在他人的轮子上改造,基于python3:
python
# -*- coding: utf-8 -*-
import zlib, os, struct
filelist = []
class FileVisitor:
def __init__(self, startDir=os.curdir):
self.startDir = startDir
def run(self):
for dirname, subdirnames, filenames in os.walk(self.startDir, True):
for filename in filenames:
self.visit_file(os.path.join(dirname, filename))
def visit_file(self, pathname):
filelist.append({'filename':pathname, 'size':0, 'zlib_size':0, 'offset':0, 'relative_filename': pathname.replace(os.path.normpath(self.startDir)+os.sep, '')})
if __name__ == "__main__":
source_dirname = r"H:\update"
out_filename = r"H:\test.pkg"
FileVisitor(source_dirname).run()
total = len(filelist)
fp = open(out_filename + '~', 'wb')
fp.write('\x64\x00\x00\x00'.encode())
fp.write(struct.pack('I', len(filelist)))
fp.write(struct.pack('I', 0))
fp.write(struct.pack('I', 0))
offset = 16
for index in range(total):
item = filelist[index]
item['offset'] = offset
infile = open(item['filename'], 'rb')
text = infile.read()
infile.close()
item['size'] = len(text)
text = zlib.compress(text)
item['zlib_size'] = len(text)
fp.write(text)
offset += item['zlib_size']
print(u'已压缩文件 %d/%d' % (index + 1, total))
filename_table_offset = offset
for index in range(total):
item = filelist[index]
fp.write(struct.pack('H', len(item['relative_filename'])))
fp.write(item['relative_filename'].encode())
fp.write('\x01\x00\x00\x00'.encode())
fp.write(struct.pack('I', item['offset']))
fp.write(struct.pack('I', item['size']))
fp.write(struct.pack('I', item['zlib_size']))
offset += 2 + len(item['relative_filename']) + 16
print(u'已输出路径 %d/%d' % (index+1, total))
filename_table_len = offset - filename_table_offset
fp.close()
fp = open(out_filename + '~', 'rb')
ret = open(out_filename, 'wb')
fp.read(16)
ret.write('\x64\x00\x00\x00'.encode())
ret.write(struct.pack('I', len(filelist)))
ret.write(struct.pack('I', filename_table_offset))
ret.write(struct.pack('I', filename_table_len))
copy_bytes = 16
total_bytes = offset
while True:
text = fp.read(2**20)
ret.write(text)
copy_bytes += len(text)
print(u'最后的拷贝 %d%%' % (copy_bytes * 100.0 / total_bytes))
if not text:
break
fp.close()
ret.close()
os.remove(out_filename + '~')五、注意事项
仅作为技术交流和娱乐,请勿用于非法用途。



粤公网安备44030602007943号