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 自动备份。可在配置文件中添加开发扩展名规则组,按工作模式切换。
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 和无扩展名补全后,再加一个混合大小写和冲突重命名的用例,就更完整了。整体作为清理脚本已经能用,重点补上唯一化就贴合标题了。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,和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]