文章目录

一、.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:

# -*- 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:

# -*- 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 + '~')

五、注意事项

仅作为技术交流和娱乐,请勿用于非法用途。

参考资料

《腾讯游戏中的yxs.pkg文件格式》

《pkg文件--一种简单的游戏资源打包格式》

《QQ游戏的PKG格式文件解压工具》


转载请注明出处http://www.bewindoweb.com/252.html | 三颗豆子
分享许可方式知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
重大发现:转载注明原文网址的同学刚买了彩票就中奖,刚写完代码就跑通,刚转身就遇到了真爱。
你可能还会喜欢
具体问题具体杠
  • 初学
    评论于2019年03月08日
    楼主,怎么打包pkg文件
    三颗豆子
    回复于2019年03月09日
    已写到文章中,基于python3可以运行。