linux上zip文件解压那些事

发布于 2025-02-11  387 次阅读


这几天在写脚本批量解压zip文件,发现使用unzip或python解压zip文件时出现了一堆问题,遂记录一下问题和解决办法。

一、中文乱码

linux上调用unzip目命令或者python函数zipfile解压zip文件,文件名编码,要么使用utf-8要么cp437;但大部分zip文件是由windows系统压缩的,windows平台上压缩包文件名编码大多是gbk,所以容易出现解压后文件名中文乱码。

采用gbk编码重命名

解压后采用gbk编码对目录和文件逐一修改重命名,比如python调用函数zipfile解压后,使用gbk编码对目录和文件逐一修改重命名

☛点我查看脚本内容
import zipfile
import os
import chardet
import logging

# 配置日志
logging.basicConfig(filename='zip_rename.log', level=logging.ERROR,
                    format='%(asctime)s - %(levelname)s - %(message)s')

def decode_name(name):
    """尝试使用 chardet 检测编码并解码名称"""
    try:
        result = chardet.detect(name.encode('cp437'))
        encoding = result['encoding']
        return name.encode('cp437').decode(encoding)
    except Exception as e:
        logging.error(f"Failed to decode name '{name}': {e}")
        return name  # 如果解码失败,返回原始名称

def extract_and_rename_with_gbk(zip_file_path, extract_path):
    """
    解压 ZIP 文件,并尝试使用 GBK 编码重命名文件和目录。
    添加 chardet 自动检测编码。
    从底部向上遍历目录树。
    """
    try:
        with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
            zip_ref.extractall(extract_path)

        print(f"Successfully extracted to {extract_path}")

        # 构建目录树列表
        dir_tree = []
        for root, dirs, files in os.walk(extract_path):
            dir_tree.append((root, dirs, files))

        # 从底部向上遍历目录树
        for root, dirs, files in reversed(dir_tree):
            # 处理文件名
            for file in files:
                old_path = os.path.join(root, file)
                new_name = decode_name(file)
                new_path = os.path.join(root, new_name)

                if old_path != new_path:
                    try:
                        os.rename(old_path, new_path)
                        print(f"Renamed file '{file}' to '{new_name}'")
                    except FileExistsError as e:
                        logging.error(f"File '{new_name}' already exists. Skipping rename of '{file}': {e}")
                        print(f"File '{new_name}' already exists. Skipping rename of '{file}'.")
                    except Exception as e:
                        logging.error(f"An unexpected error occurred: {e}")
                        print(f"An unexpected error occurred: {e}")

            # 处理目录名
            for dir in dirs:
                old_dir_path = os.path.join(root, dir)
                new_dir_name = decode_name(dir)
                new_dir_path = os.path.join(root, new_dir_name)

                if old_dir_path != new_dir_path:
                    try:
                        os.rename(old_dir_path, new_dir_path)
                        print(f"Renamed directory '{dir}' to '{new_dir_name}'")
                    except FileExistsError as e:
                        logging.error(f"Directory '{new_dir_name}' already exists. Skipping rename of '{dir}': {e}")
                        print(f"Directory '{new_dir_name}' already exists. Skipping rename of '{dir}'.")
                    except OSError as e:
                        # 捕获 OSError 异常,通常发生在目录不为空时
                        logging.error(f"OSError occurred while renaming directory '{dir}': {e}")
                        print(f"OSError occurred while renaming directory '{dir}': {e}")
                    except Exception as e:
                        logging.error(f"An unexpected error occurred: {e}")
                        print(f"An unexpected error occurred: {e}")

        print("Finished renaming files and directories.")

    except Exception as e:
        logging.error(f"Error extracting and renaming: {e}")
        print(f"Error extracting and renaming: {e}")

zip_file = 'your_zip_file.zip'
extract_dir = 'extracted_files'

# 创建提取目录(如果不存在)
if not os.path.exists(extract_dir):
    os.makedirs(extract_dir)

extract_and_rename_with_gbk(zip_file, extract_dir)

ai写的一个脚本,基本逻辑就是从底部向上遍历,先修改文件名编码,再修改目录路径名的编码

python增加补丁函数

python解压zip文件中文乱码,对函数zipfile增加补丁函数解决

from zipfile import ZipFile

def support_gbk(zip_file: ZipFile):
    name_to_info = zip_file.NameToInfo
    # copy map first
    for name, info in name_to_info.copy().items():
        real_name = name.encode('cp437').decode('gbk')
        if real_name != name:
            info.filename = real_name
            del name_to_info[name]
            name_to_info[real_name] = info
    return zip_file

with support_gbk(ZipFile(r'./里面有中文.zip')) as zfp:
    zfp.extractall(r'./中文不乱码')

换用其它软件

换用其它软件解压,比如7z,在linux平台上兼容由windows平台生成的gbk编码文件名
ubuntu or debian直接安装7z

sudo apt update
sudo apt install p7zip-full 

7z常用解压命令

7z x /path/to/file.zip -p{password} -o/destination

x,保持目录结构;
e,所有文件移动到解压目录
-p,无空格跟密码,特殊符号加""括起来,无密码时无需该参数
-o,无空格跟路径,指定解压路径,无该参数时默认./路径
更多自行参考东东's Blog

说明一下,python中使用7z解压,可以通过调用subprocess使用7z命令行,但需要安装好软件包7z,windows注意环境变量配置;也可以直接使用py7zr 这个函数。

7z解压文件中文乱码

在linux vps上,用zip创建的压缩包,实测使用7z解压会出现中文乱码,而使用unzip解压则不会。因此,保证压缩和解压使用同一种软件,更能避免中文乱码。

都在linux平台上,跨机器和压缩软件时,7z解压中文乱码解决办法
使用LC_ALL=C, 而不是LANG=C

LC_ALL=C 7z x -pPassword 中文测试.zip && convmv -f gbk -t utf8 --notest -r .

实测直接使用以下命令即可

LC_ALL=C 7z x -pPassword 中文测试.zip

二、大文件无法解压

使用unzip解压,某些情况下(比如解压大文件时)容易出现"error: invalid zip file with overlapped components (possible zip bomb)"错误,使用python调用函数zipfile也可能出错。
解决办法,换用7z软件,实测不再报错。

三、解压慢

在甲骨文的arm机器上,实测使用unzip或者python函数zipfile解压zip文件时,很低效,换用7z后,同一个文件解压快得多。

四、总结一下

跨平台、跨系统、跨软件时,zip文件名压缩与解压编码可能不同,如果知道生成zip压缩包的软件,尽量使用相同软件和相同编码解压;如若不知道,从兼容性和解压效率上来讲,建议换用7z解压。
下面是一个批量zip图包解压脚本

☛点我查看脚本内容
import os
import time
import shutil
import logging
import subprocess

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# 需要监控的目录
WATCH_DIR = "./"
# 解压到的目录
UNZIP_DIR = os.path.join(WATCH_DIR, "解压到的目录")
# 已处理 ZIP 文件的目录
PROCESSED_DIR = os.path.join(WATCH_DIR, "原有zip压缩包")
# 尝试解压的密码列表
PASSWORDS = ["pw1", "pw2", "pw3"]
# 检测间隔(秒),无需修改
SCAN_INTERVAL = 10

# 确保解压目录和已处理目录存在
os.makedirs(UNZIP_DIR, exist_ok=True)
os.makedirs(PROCESSED_DIR, exist_ok=True)

def extract_zip_with_7z(zip_path, passwords):
    """尝试使用多个密码通过 7z 解压 ZIP 文件"""
    for password in passwords:
        try:
            unzip_path = create_unzip_path(zip_path)
            cmd = ['7z', 'x', zip_path, f'-o{unzip_path}', f'-p{password}']
            subprocess.run(cmd, check=True)
            logging.info(f"Successfully extracted {zip_path} using password: {password}")
            return True  # 解压成功,返回 True
        except subprocess.CalledProcessError as e:
            logging.debug(f"Failed to extract {zip_path} with password {password}: {e}")
    logging.warning(f"Failed to extract {zip_path} with all provided passwords.")
    return False  # 所有密码都失败,返回 False

def get_relative_path(zip_path):
    """获取相对于监控目录的路径"""
    return os.path.relpath(zip_path, WATCH_DIR)

def create_unzip_path(zip_path):
    """创建解压后的目标路径,一级目录为zip文件名"""
    file_name = os.path.splitext(os.path.basename(zip_path))[0]
    unzip_path = os.path.join(UNZIP_DIR, file_name)
    os.makedirs(unzip_path, exist_ok=True)
    return unzip_path

def create_processed_path(zip_path):
    """创建移动后的目标路径,一级目录为zip文件名"""
    file_name = os.path.splitext(os.path.basename(zip_path))[0]
    processed_path = os.path.join(PROCESSED_DIR, os.path.basename(zip_path))
    os.makedirs(os.path.dirname(processed_path), exist_ok=True)
    return processed_path

def process_single_directory(unzip_path):
    """处理解压后只有单层目录的情况,将内容上移"""
    while True:
        items = os.listdir(unzip_path)
        if len(items) == 1 and os.path.isdir(os.path.join(unzip_path, items[0])):
            single_dir = os.path.join(unzip_path, items[0])
            # 移动 single_dir 下的所有内容到 unzip_path
            for item in os.listdir(single_dir):
                s = os.path.join(single_dir, item)
                d = os.path.join(unzip_path, item)
                try:
                    shutil.move(s, d)
                    logging.info(f"Moved {s} to {d}")
                except Exception as e:
                    logging.error(f"Failed to move {s} to {d}: {e}")

            # 删除 single_dir
            try:
                shutil.rmtree(single_dir)
                logging.info(f"Removed directory: {single_dir}")
            except Exception as e:
                logging.error(f"Failed to remove directory {single_dir}: {e}")
        else:
            break  # 不再是单层目录,退出循环

def scan_and_process_files():
    """扫描并处理ZIP文件"""
    logging.info("Scanning for ZIP files...")
    for root, _, files in os.walk(WATCH_DIR):
        # 排除已处理目录
        if root.startswith(PROCESSED_DIR):
            continue
        # 排除解压内容目录
        if root.startswith(UNZIP_DIR):
            continue
        for file in files:
            if file.lower().endswith(".zip"):
                file_path = os.path.join(root, file)
                logging.info(f"Found ZIP file: {file_path}")
                success = extract_zip_with_7z(file_path, PASSWORDS)
                if success:
                    # 移动到已处理目录
                    processed_path = create_processed_path(file_path)
                    try:
                        shutil.move(file_path, processed_path)
                        logging.info(f"Successfully moved {file_path} to {processed_path}")
                        # 解压后处理单层目录
                        unzip_path = create_unzip_path(file_path)
                        process_single_directory(unzip_path)
                    except Exception as e:
                        logging.error(f"Failed to move {file_path} to {processed_path}: {e}")
                else:
                    logging.warning(f"Failed to extract {file_path}")

                remove_empty_directories(WATCH_DIR)

    logging.info("Finished scanning.")

def remove_empty_directories(path):
    """递归删除空目录"""
    for root, dirs, files in os.walk(path, topdown=False):  # 从叶子节点开始遍历
        for dir_name in dirs:
            dir_path = os.path.join(root, dir_name)
            try:
                if not os.listdir(dir_path):  # 目录为空
                    os.rmdir(dir_path)
                    logging.info(f"Removed empty directory: {dir_path}")
            except OSError as e:
                logging.warning(f"Failed to remove directory {dir_path}: {e}")

if __name__ == "__main__":

    # 处理已存在的解压文件也进行单层目录处理
    for dir_name in os.listdir(UNZIP_DIR):
        dir_path = os.path.join(UNZIP_DIR, dir_name)
        if os.path.isdir(dir_path):
            process_single_directory(dir_path)

    logging.info("Starting ZIP file processor...")
    try:
        while True:
            scan_and_process_files()
            time.sleep(SCAN_INTERVAL)
    except KeyboardInterrupt:
        logging.info("Stopping ZIP file processor.")

支持以下功能:

  • 监控路径,出现zip文件就解压
  • 支持多密码轮询试错解压
  • 调用7z软件包解压,效率高(解压速度快),兼容性好(无中文乱码问题)
  • 解压后删除空目录
  • 创建以zip压缩包名为名称的目录作为解压路径
  • 简化目录结构,解压后去除解压路径下面的单层目录(该层只有一个目录),比如/path/单目录1/单目录2/单目录3/file,会删除得到/path/file
  • 解压后移动zip压缩包到某个目录,并且后续跳过该目录

注意问题

  • 请保证所有zip压缩包名称不一样,否则可能会出现解压后文件覆盖问题,可以提前在zip压缩包名称上增加文件大小,确保文件名不一样。
  • python调用函数subprocess使用7z命令行,注意安装好7z软件包

五、参考

1.https://blog.csdn.net/qq_21076851/article/details/122752196
2.https://blog.csdn.net/yanjiaxuan_AI/article/details/118016848
3.https://www.cnblogs.com/988MQ/p/16819661.html
4.https://blog.yasking.org/a/terminal-7z-usage
5.http://bytetoy.cn/tips/unzip_error.html
6.http://blog.chinaunix.net/uid-20753399-id-703808.html
7.chatgpt&gemini


“一花一世界,一叶一天堂。君掌盛无边,刹那成永恒。”