Python 解压 ZIP 后,中文目录变成了 ╓╨╬──┐┬╝,文件内容却没变。如果创建方能确认旧文件名采用 GBK,Python 3.11+ 可以用 metadata_encoding='gbk' 重新解压,无须修改 zipfile.py。12
这个参数指定文件名的读取编码,不会自动识别编码,也不转换文件内容。来源不明、名称已经损坏的包,不适用下面的做法。
用新目录重新解压
准备 Python 3.11 或以上版本,以及可信的小 ZIP。保留原包和已有解压结果,输出目录必须尚不存在,父目录须已存在。
将代码保存为 extract_known_gbk.py。它只读原 ZIP,把结果写到新目录;输出路径或文件内容核验失败会停止。
python
"""Python >=3.11: 提取可信小 ZIP 中已知 GBK 来源的旧文件名。
用法:python3 extract_known_gbk.py 输入.zip 全新输出目录
前提:未设 UTF-8 标志的成员名称已确认是 GBK;不是自动检测。
正确设置 UTF-8 标志的成员由 zipfile 按 UTF-8 读取。
只读原 ZIP;输出目录必须不存在,父目录必须存在;不删除、不改名旧输出。
失败时保留 ZIP 和可能存在的本次部分输出,换全新目录重试,不在旧目录重跑。
不适用于恶意/未知来源、大归档、加密归档、损坏名称、多种未标志旧编码混用。
验证内容字节与归档一致,不转换文件内容编码;中文名仍须对照来源清单确认。
"""
import argparse
import hashlib
from pathlib import Path
import sys
import zipfile
def main():
if sys.version_info < (3, 11):
raise SystemExit('需要 Python 3.11 或以上版本;请先核对解释器版本。')
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('archive', type=Path, help='可信小 ZIP;已知旧名称为 GBK')
parser.add_argument('output', type=Path, help='必须不存在的新输出目录')
args = parser.parse_args()
if args.output.exists() or args.output.is_symlink():
raise SystemExit('拒绝写入:输出目录已存在,请换一个全新目录。')
with zipfile.ZipFile(args.archive, 'r', metadata_encoding='gbk') as archive:
infos = archive.infolist()
for info in infos:
print('NAME:', info.filename, 'UTF8_FLAG:', bool(info.flag_bits & 0x800))
args.output.mkdir(exist_ok=False)
archive.extractall(args.output)
expected = {info.filename.rstrip('/') for info in infos}
actual = {p.relative_to(args.output).as_posix() for p in args.output.rglob('*')}
# Parent directories may be implicit in a ZIP, so include them in the check.
for name in tuple(expected):
expected.update(p.as_posix() for p in Path(name).parents if p.as_posix() != '.')
if actual != expected:
raise RuntimeError('输出路径与读取到的成员名称不一致,请停止使用本次输出。')
count = 0
for info in infos:
target = args.output / info.filename
if info.is_dir():
if not target.is_dir():
raise RuntimeError('目录核验失败:' + info.filename)
continue
in_zip = archive.read(info)
on_disk = target.read_bytes()
if on_disk != in_zip:
raise RuntimeError('文件内容核验失败:' + info.filename)
count += 1
print('SHA256:', hashlib.sha256(on_disk).hexdigest(), info.filename)
print('VERIFIED:', count, 'files; decoded paths and all content bytes match the archive')
print('OUTPUT:', args.output.resolve())
print('请再与来源清单核对中文名;无异常不代表所选名称编码一定正确。')
if __name__ == '__main__':
main()
运行格式如下,两个路径换成自己的输入包和全新输出目录。路径含空格时保留引号:
bash
python3 extract_known_gbk.py "旧文件.zip" "新解压目录"
'r' 表示只读方式打开 ZIP,仍然可以把成员提取成文件。metadata_encoding='gbk' 用于读取未设置 UTF-8 标志的名称;已正确设置该标志的成员仍按 UTF-8 读取。1
核对落盘结果,不只看打印名称
本次在 Linux、Python 3.14.9 上运行。测试 ZIP 是为复现制作的受控样本,含 GBK 名称和已知文件字节,未冒充真实客户文件。
默认读取时,同一成员的实际输出为:
text
READ_NAME: '╓╨╬──┐┬╝/▒¿╕µ.txt' FLAGS: 0x0
PATHS_MATCH: False
按 GBK 读取并实际解压后,脚本输出节选为:
text
NAME: 中文目录/ UTF8_FLAG: False
NAME: 中文目录/报告.txt UTF8_FLAG: False
NAME: 中文目录/数据.bin UTF8_FLAG: False
VERIFIED: 2 files; decoded paths and all content bytes match the archive
落盘后的相对路径、两个文件的字节和 SHA-256,均与制作样本时的清单一致。另一个混合样本包含正确标记的 UTF-8 文件名,也通过了相同核验。
看到 VERIFIED 后,仍要对照创建方的清单确认中文名。脚本能验证「按所选编码读到的名称确实落盘、内容一致」,无法判断这个编码是否符合原作者的意思。
不报错也可能选错编码
对同一个 GBK 测试包,边界结果如下:
| 操作 | 本次实测结果 |
|---|---|
| 默认读取,未指定名称编码 | 解压退出 0,中文名称错误 |
指定 gbk |
中文路径和内容均符合源清单 |
指定 ascii |
抛出 UnicodeDecodeError |
指定 latin1 |
解压退出 0,名称仍然错误 |
| Python 3.10.12 直接传该参数 | 抛出 TypeError |
在 'w' 模式指定该参数 |
抛出 ValueError |
因此,不要把「能解压」当成「编码选对」。如果来源无法确认,停下来向创建方核实,不要轮流试编码后覆盖旧结果。
metadata_encoding 从 Python 3.11 开始支持;这里未安装运行 3.11,其版本依据来自官方文档,本机提取测试使用 3.14.9。2
它按整个 ZipFile 实例设置,不能给各个未标记成员分别指定不同旧编码。1 未知来源、多种未标记旧编码混用、错误 UTF-8 标志和已损坏名称,都不在本次修复承诺内。
失败时保留原包和已有目录,停止使用本次部分输出。确认来源与解释器后换新目录重试;本文不提供删除旧结果的代码,也未验证 Windows、macOS 或恶意归档的处理。
Sources
1 docs.python.org/3.14/librar... 2 docs.python.org/3.11/librar...
AI 辅助整理与审校;文中实测输出来自本机运行,适用条件依据官方文档核对。