如何使用Python3标准库移除文件末尾嵌入的ZIP归档并更新内容
实现方案
要保留前置宿主文件、移除末尾嵌入的ZIP归档,核心是准确定位ZIP在拼接文件中的起始偏移,再将文件截断到该位置即可,全程仅使用Python3标准库即可实现,不需要额外依赖。
核心原理
完整的ZIP归档末尾必然存在中央目录结束记录(EOCD),固定签名为字节序列b'PK\x05\x06':
- EOCD固定头长度为22字节,尾部最多可携带65535字节的自定义注释,因此只需从文件末尾向前读取最多65557字节,就一定能定位到EOCD签名,不需要读取整个文件
- 从EOCD固定头中可以解析出两个关键值:中央目录总大小、中央目录相对于ZIP起始位置的偏移量
- 由于ZIP结构中中央目录紧接在文件内容之后、EOCD紧接在中央目录之后,因此可以直接通过EOCD的位置反推整个ZIP的起始偏移
- 对于超过4GB、文件数超过65535的ZIP64格式归档,额外解析EOCD前方的ZIP64定位记录即可,逻辑一致
实现代码
import struct def strip_trailing_zip(file_path: str) -> int: """ 移除文件末尾嵌入的ZIP归档,保留前置宿主内容 返回截断后文件的大小(即宿主内容的字节长度),如果文件末尾无有效ZIP则返回原文件大小 """ EOCD_SIGN = b'PK\x05\x06' ZIP64_LOC_SIGN = b'PK\x06\x07' EOCD_MIN_SIZE = 22 MAX_COMMENT_SIZE = 0xFFFF READ_TAIL_SIZE = EOCD_MIN_SIZE + MAX_COMMENT_SIZE with open(file_path, 'rb+') as f: f.seek(0, 2) file_size = f.tell() # 文件过小,不可能包含ZIP if file_size < EOCD_MIN_SIZE: return file_size # 读取文件尾部块 read_size = min(file_size, READ_TAIL_SIZE) f.seek(file_size - read_size) tail_block = f.read(read_size) # 从后往前查找EOCD签名 eocd_pos_in_tail = tail_block.rfind(EOCD_SIGN) if eocd_pos_in_tail == -1: return file_size eocd_abs_pos = file_size - read_size + eocd_pos_in_tail # 解析EOCD固定头 # 结构:4字节签名 + 3个2字节字段 + 2个4字节字段(中央目录大小、中央目录偏移)+ 2字节注释长度 eocd_header = tail_block[eocd_pos_in_tail : eocd_pos_in_tail + EOCD_MIN_SIZE] _, _, _, _, cd_size, cd_offset, comment_len = struct.unpack('<IHHHHIIH', eocd_header) # 处理ZIP64格式:如果普通EOCD里的偏移是0xFFFFFFFF,说明是ZIP64,需要读前面的ZIP64定位器和ZIP64 EOCD if cd_offset == 0xFFFFFFFF or cd_size == 0xFFFFFFFF: # ZIP64定位器在普通EOCD前方20字节,签名b'PK\x06\x07' zip64_loc_pos = eocd_abs_pos - 20 if zip64_loc_pos < 0: return file_size f.seek(zip64_loc_pos) zip64_loc_header = f.read(20) if zip64_loc_header[:4] != ZIP64_LOC_SIGN: return file_size # 解析ZIP64 EOCD的绝对位置 _, zip64_eocd_disk, zip64_eocd_offset, total_disks = struct.unpack('<IIQI', zip64_loc_header) # 读ZIP64 EOCD头(固定56字节) f.seek(zip64_eocd_offset) zip64_eocd_header = f.read(56) if zip64_eocd_header[:4] != b'PK\x06\x06': return file_size # 解析ZIP64 EOCD里的中央目录大小和偏移 _, _, _, _, _, _, _, cd_size, cd_offset, _ = struct.unpack('<IQHHIIQQQQ', zip64_eocd_header) # 计算ZIP归档的起始绝对偏移 zip_start_pos = eocd_abs_pos - cd_size - cd_offset if zip_start_pos < 0: return file_size # 截断文件到ZIP起始位置,丢弃后面的ZIP内容 f.truncate(zip_start_pos) return zip_start_pos
使用流程
更新嵌入ZIP的完整操作步骤如下:
- 先备份原文件,避免操作失误导致数据丢失
- 调用
strip_trailing_zip(目标文件路径),函数会自动移除末尾旧的ZIP归档,保留前置宿主内容 - 以追加模式
'a'打开文件创建ZipFile对象,将需要保留的原归档文件、新增/修改的文件逐一写入,需要删除的文件跳过即可 - 关闭
ZipFile即完成更新,新的ZIP会自动追加到宿主内容末尾,结构和原拼接文件完全一致
注意事项
- 该方案仅适用于完整ZIP归档直接追加在宿主文件尾部的场景,包括ZipFile a模式生成的拼接文件、ZIP格式隐写文件、ZIP拼接EXE的自解压包等,不兼容ZIP数据拆分插入宿主文件其他位置的情况
- 代码兼容普通ZIP和ZIP64格式,支持单文件最大4GB以上的大体积归档
- 所有逻辑仅依赖Python内置的文件IO和
struct模块,不需要安装任何第三方依赖
内容的提问来源于stack exchange,提问作者kiv_apple
相关产品推荐
相关产品推荐

