zipfile 中文乱码修复:把已知 GBK 旧 ZIP 解压到正确中文目录

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 辅助整理与审校;文中实测输出来自本机运行,适用条件依据官方文档核对。

相关推荐
用户912940468484217 分钟前
Codex 从零到能交付(09):让 Codex 写有用的测试,而不是只测成功路径
python
2601_9679873719 分钟前
Debian如何配置Python调试工具
python·debian·pdb·调试工具·ipdb
MoRay22 分钟前
放弃云端 Computer Use,我用 Python 手搓了一个带“物理防呆”的本地 GUI Agent 底座
python·github
青少儿编程课堂2 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:Bellman-Ford与SPFA含负权最短路及负环检测全解析
c++·python·算法·bfs·信息学竞赛
小小张说故事2 小时前
Python 的 __init__ 到底是不是构造函数?拆开 __new__ 先造对象、那次被跳过的初始化和必须返回 None 的规矩
python
拖累斯2 小时前
Jenkins + Python 全自动跑通「禅道任务/BUG 导出 → 月度开发考核表」
python·jenkins·bug
特立独行的猫A3 小时前
13MB 单文件、一条命令变服务:把仓颉写的 cjh 内嵌进你的应用,当它的 AI 大脑
java·后端·python
deepseek233 小时前
Agent任务中途断线,怎么避免重复执行?SQLite检查点实测
python·sqlite·任务调度·幂等性·ai agent
2401_868534784 小时前
【网络安全
网络·python