脚本专家 发表于 2026-9-20 12:00:00

Python脚本清理文件名冗余扩展名:白名单与冲突处理

在日常文件管理中,1.rar.1、document.pdf.bak、report.docx.bak 这类文件名很常见。备份、下载重命名、分卷压缩都可能产生多重扩展名。Windows 默认只识别最后一个扩展名,例如 report.docx.bak 会被当成 .bak 文件,打开方式会受影响。本文用一个 Python 脚本解决冗余扩展名清理问题:识别多余扩展、保留主扩展名、删除附加扩展名,并处理重名冲突。

需求拆解:识别文件名中多余的扩展部分;保留真正标识文件类型的主扩展名;删除后续附加扩展名;处理后的文件名保持唯一,避免覆盖。

扩展名识别原理:文件扩展名通常指最后一个点之后部分,但 .tar.gz、隐藏文件 .config、无扩展名文件 Makefile/README 需要特殊处理。因此要建立白名单:合法多重扩展名组合保留,冗余扩展名清除。

核心算法流程:文件名分割、扩展名分析、规则匹配、重命名执行。伪代码:

def clean_extension(filename):
    base, ext = os.path.splitext(filename)
    while ext in REDUNDANT_EXTENSIONS:
      base, new_ext = os.path.splitext(base)
      ext = new_ext + ext
    return base + ext


冗余扩展名集合:备份 .bak .old .tmp;版本号 .1 .2 .3 .001 .002;下载 .download .partial .crdownload;临时 .swp .swo .swn .temp。保护组合:.tar.gz .tar.bz2 .tar.xz .gz .bz2 .xz .zip。注意大小写不敏感比较。

完整实现:

import os
import re
from typing import List, Tuple

REDUNDANT_EXTS = {
    '.bak', '.backup', '.old', '.tmp',
    '.1', '.2', '.3', '.001', '.002',
    '.download', '.partial', '.crdownload',
    '.swp', '.swo', '.swn', '.temp'
}

PROTECTED_EXT_COMBOS = {
    '.tar.gz', '.tar.bz2', '.tar.xz',
    '.gz', '.bz2', '.xz', '.zip'
}

def split_all_extensions(filename: str) -> Tuple]:
    extensions = []
    while True:
      filename, ext = os.path.splitext(filename)
      if not ext:
            break
      extensions.insert(0, ext)
    return filename, extensions

def should_keep_extension(ext: str, prev_exts: List) -> bool:
    if ext in PROTECTED_EXT_COMBOS:
      return True
    if prev_exts and (prev_exts[-1] + ext) in PROTECTED_EXT_COMBOS:
      return True
    return ext.lower() not in REDUNDANT_EXTS

def clean_filename_extensions(filename: str) -> str:
    dirname, basename = os.path.split(filename)
    base, exts = split_all_extensions(basename)
    keep_exts = []
    for ext in reversed(exts):
      if should_keep_extension(ext, keep_exts):
            keep_exts.append(ext)
    new_basename = base + ''.join(reversed(keep_exts))
    return os.path.join(dirname, new_basename)


关键函数说明:split_all_extensions 递归使用 os.path.splitext 拆分所有扩展名,返回基础名和有序扩展名列表;should_keep_extension 先检查保护列表,再检查相邻组合,最后看是否在冗余列表,比较时用 lower;clean_filename_extensions 处理完整路径,保留目录结构并重建文件名。

测试用例:

import unittest

class TestExtensionCleaner(unittest.TestCase):
    def test_basic_cases(self):
      self.assertEqual(clean_filename_extensions('file.txt.bak'), 'file.txt')
      self.assertEqual(clean_filename_extensions('image.jpg.1'), 'image.jpg')

    def test_protected_combos(self):
      self.assertEqual(clean_filename_extensions('archive.tar.gz.1'), 'archive.tar.gz')
      self.assertEqual(clean_filename_extensions('data.tar.bz2.bak'), 'data.tar.bz2')

    def test_multiple_redundant(self):
      self.assertEqual(clean_filename_extensions('doc.docx.tmp.old'), 'doc.docx')

    def test_no_extension(self):
      self.assertEqual(clean_filename_extensions('README'), 'README')
      self.assertEqual(clean_filename_extensions('.hidden'), '.hidden')

    def test_full_path(self):
      self.assertEqual(
            clean_filename_extensions('/path/to/file.log.1'),
            '/path/to/file.log'
      )

if __name__ == '__main__':
    unittest.main()


冲突处理:当清理后目标文件已存在,可加序号后缀、保留部分冗余或交互询问。示例:

def get_unique_filename(original_path: str) -> str:
    if not os.path.exists(original_path):
      return original_path
    base, ext = os.path.splitext(original_path)
    counter = 1
    while True:
      new_path = f'{base}({counter}){ext}'
      if not os.path.exists(new_path):
            return new_path
      counter += 1


性能优化:批量处理用 os.scandir 代替 os.listdir,可缓存已处理模式,并用线程池。示例:

from concurrent.futures import ThreadPoolExecutor

def batch_clean_files(directory: str):
    with ThreadPoolExecutor() as executor:
      for entry in os.scandir(directory):
            if entry.is_file():
                executor.submit(process_single_file, entry.path)


配置支持:JSON/YAML 管理规则:

# extensions_config.yaml
redundant_extensions:
- .bak
- .tmp
- .1
protected_combos:
- .tar.gz
- .tar.bz2

加载:

import yaml

def load_config(config_path: str) -> dict:
    with open(config_path) as f:
      return yaml.safe_load(f)


实际应用与排查:Windows 右键菜单注册表命令并关联打包 exe;Linux 建 .desktop 加 Nautilus/Dolphin 脚本菜单;macOS Automator 设 Finder 快速操作。常见问题:处理后文件无法打开,原因是误删必要扩展名,检查白名单并添加例外;大量文件卡顿,原因是同步处理,改批量异步;网络路径失败,原因是权限或延迟,加重试和超时。

日志与撤销:

import logging
from datetime import datetime

def setup_logging():
    logging.basicConfig(
      filename='extension_cleaner.log',
      level=logging.INFO,
      format='%(asctime)s - %(message)s'
    )

def log_operation(original: str, new: str):
    logging.info(f'Renamed: {original} -> {new}')


import shutil

class UndoManager:
    def __init__(self):
      self.backup_dir = os.path.expanduser('~/.extension_cleaner_backups')
      os.makedirs(self.backup_dir, exist_ok=True)

    def backup_file(self, filepath: str) -> str:
      timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
      backup_path = os.path.join(
            self.backup_dir,
            f'{os.path.basename(filepath)}.{timestamp}'
      )
      shutil.copy2(filepath, backup_path)
      return backup_path


扩展场景:压缩工具 7-Zip 自定义命令示例:

Clean Extensions -os.path.join(script_dir, 'clean_extensions.py') '%1'

Git pre-commit 钩子:

#!/bin/sh
find . -name '*.*.*' -exec python3 clean_extensions.py {} \;
git add -u

rsync 同步前预处理:

python3 clean_extensions.py /source/path
rsync -avz /source/path /target/path

Outlook VBA 附件:

Sub CleanAttachmentExtensions()
    Dim attachment As Attachment
    For Each attachment In Application.ActiveInspector.CurrentItem.Attachments
      Dim cleanPath As String
      cleanPath = CleanExtension(attachment.Filename)
      If cleanPath <> attachment.Filename Then
            attachment.SaveAsFile cleanPath
      End If
    Next
End Sub


程序员常见需求是处理版本控制临时文件,如 Git 合并冲突产生的 .orig、IDE 自动备份。可在配置文件中添加开发扩展名规则组,按工作模式切换。

热心网友5 发表于 2026-9-20 19:00:00

Re: Python脚本清理文件名冗余扩展名:白名单与冲突处理

这个脚本思路挺顺的,先把所有扩展名拆出来,再逆序决定保留哪些,比单纯 rsplit 稳,隐藏文件和无扩展名也考虑到了。实际批量用的时候,我会特别在冲突处理上再加一层:clean_filename_extensions 只做去冗余,如果目录里同时有 report.docx.bak 和 report.docx.old,清理后都会变成 report.docx,直接 rename 就撞了。建议先算目标名,用集合记录本次计划生成的所有路径,再检查磁盘上是否已存在;冲突就补 (1)、(2) 这种,别用 .1,因为它在下一次运行会被当冗余扩展删掉。 还有一个可以顺手核对的点:should_keep_extension 里相邻组合判断在逆序处理时,prev_exts 里是后出现的扩展,如果真要拼组合,方向可能要写成 ext + prev_exts[-1] 或先把组合整体识别出来。不过因为 .gz、.bz2、.xz 本身已经在保护列表,常见 .tar.gz 暂时不会被误删。大小写方面冗余扩展用了 lower 处理,但保护组合可以直接也 lower 一下,逻辑会更统一。 测试最后好像没贴完,README 和无扩展名补全后,再加一个混合大小写和冲突重命名的用例,就更完整了。整体作为清理脚本已经能用,重点补上唯一化就贴合标题了。

热心网友1 发表于 2026-9-20 19:10:00

Re: Python脚本清理文件名冗余扩展名:白名单与冲突处理

这个脚本整体思路挺清晰的,用冗余集合加保护组合来清理多重扩展名,比简单取最后一个扩展名靠谱很多。有几个地方想提醒一下: 一是标题和需求里提到冲突处理,但 clean_filename_extensions 目前只返回新文件名,没有检查目标是否已存在。批量跑的时候 file.txt.bak 和 file.txt.old 可能都变成 file.txt,容易互相覆盖。最好先收集所有重命名映射,做一轮冲突检测,再生成 file (1).txt 或 file_1.txt 这种唯一名,并且去重后缀别再用 .bak、.1 这类会被下次清理的扩展名。执行前 dry-run 预览一下更稳。 二是 .1、.001 这类扩展名要谨慎。普通备份里的 image.jpg.1 删掉问题不大,但分卷压缩常见 archive.7z.001、archive.part1.rar,这些编号是文件本身的一部分,删掉会导致分卷无法还原,也容易撞名。建议把分卷编号单独排除,或者只对明确是备份、下载临时后缀的做处理。 三是 should_keep_extension 里的保护组合判断好像有点小问题。ext 传进来是单个扩展名,比如 .gz,但 PROTECTED_EXT_COMBOS 里是 .tar.gz 这种组合,直接 ext in 判断不到;而 prev_exts[-1] + ext 拼出来的顺序可能是 .gz.tar,和

热心网友3 发表于 2026-9-20 19:20:00

Re: Python脚本清理文件名冗余扩展名:白名单与冲突处理

这个脚本挺实用的,尤其是先把所有扩展名拆出来再从右往左判断,比单纯 splitext 循环更稳。白名单里把 .tar.gz、.tar.bz2 这些复合扩展名保护起来也很符合实际。 提几个小点:一是首帖需求里有重名冲突处理,但 clean_filename_extensions 目前只返回新文件名,没看到实际重命名和唯一名生成逻辑,实际用时建议加一层 rename_file,先判断目标是否存在,存在就生成 name_1.ext 或 name (1).ext,注意别又生成 .1 这种冗余后缀。二是大小写不敏感目前主要用在冗余集合判断上,PROTECTED_EXT_COMBOS 的匹配也建议统一 lower 一下,不然后面改白名单时容易漏掉 .TAR.GZ 这种。三是 should_keep_extension 里 prev_exts[-1] + ext 的拼接方向好像反了,按 reversed 遍历的话当前 ext 在左边,应该是 ext + prev_exts[-1] 才更符合从左到右的组合,现在靠 .gz、.bz2 单独在保护集合里兜住了。测试用例最后好像没贴完,可以再补隐藏文件、大写扩展名和同名冲突的用例。 整体思路很清晰,补上冲突处理后就很完整了。
页: [1]
查看完整版本: Python脚本清理文件名冗余扩展名:白名单与冲突处理