← 返回自动化办公目录
← 返回学习笔记首页
专题: Python 自动化办公系统学习
关键词: Python, 自动化办公, 批量重命名, 文件分类, 文件整理, 正则表达式, 文件管理, Python自动化
一、重命名基础
文件重命名是文件管理中最基础也是最常用的操作之一。在日常办公中,我们经常遇到需要批量修改文件名的情况,例如将大量照片按照拍摄日期命名、将下载的文档统一规范命名、或者将项目文件按照特定规则整理。Python 提供了多种方式来实现文件重命名,掌握这些基础方法是构建自动化文件管理系统的重要一步。
Python 中实现文件重命名主要有三种方式:os.rename()、shutil.move() 和 pathlib.Path.rename()。os.rename() 是最基础的方法,它直接操作系统底层的重命名调用,效率最高但功能简单。shutil.move() 不仅可以重命名,还能跨目录移动文件,实际使用中更加灵活。pathlib 是 Python 3.4 引入的面向对象文件路径库,它提供了更现代化的 API,推荐在新项目中使用。
在设计批量重命名流程时,需要遵循几个关键步骤:首先收集所有目标文件并获取完整路径;然后解析现有文件名,提取需要保留或修改的部分;接着根据规则生成新的文件名;最后逐个执行重命名操作。在批量操作之前,强烈建议先模拟运行一遍,确认生成的名称无误后再实际执行,避免因命名冲突或规则错误导致数据丢失。
import os
import shutil
from pathlib import Path
# 方法一:os.rename() 基础重命名
os.rename('old_name.txt', 'new_name.txt')
# 批量重命名目录下的所有 .txt 文件
folder = './documents'
for i, filename in enumerate(os.listdir(folder)):
if filename.endswith('.txt'):
old_path = os.path.join(folder, filename)
new_path = os.path.join(folder, f'file_{i+1:03d}.txt')
os.rename(old_path, new_path)
# 方法二:shutil.move() 重命名并移动
shutil.move('./temp/report.txt', './archive/final_report.txt')
# 批量移动到分类目录
for filename in os.listdir('./downloads'):
if filename.endswith('.pdf'):
src = f'./downloads/{filename}'
dst = f'./pdf_files/{filename}'
shutil.move(src, dst)
# 方法三:pathlib 方式 (推荐)
folder = Path('./documents')
for file in folder.iterdir():
if file.suffix == '.txt':
new_name = f"document_{file.stem}.txt"
file.rename(folder / new_name)
# 结合 glob 模式匹配更简洁
for file in Path('./docs').glob('*.log'):
new_path = file.with_suffix('.txt') # 仅修改扩展名
file.rename(new_path)
在实际开发中,重命名操作需要注意几个容易踩坑的地方:一是文件名冲突问题,如果新文件名已经存在,重命名会失败,需要在生成新名称时进行唯一性检查;二是跨文件系统时 os.rename() 可能失败,此时应改用 shutil.move();三是文件路径中不可包含特殊字符(如空字符),Windows 下路径长度也有限制(MAX_PATH 260个字符)。因此,健壮的重命名代码应该包含异常处理和冲突检测逻辑。
# 健壮的批量重命名函数
import os
def safe_rename(src, dst):
"""安全重命名,自动处理冲突"""
if not os.path.exists(src):
raise FileNotFoundError(f"源文件不存在: {src}")
if src == dst:
return # 相同路径则跳过
# 如果目标已存在则加序号
base, ext = os.path.splitext(dst)
counter = 1
while os.path.exists(dst):
dst = f"{base}_{counter}{ext}"
counter += 1
os.rename(src, dst)
return dst
# 批量应用
for file in os.listdir('./photos'):
if file.lower().endswith(('.jpg', '.png')):
new_name = file.lower().replace(' ', '_')
safe_rename(f'./photos/{file}', f'./photos/{new_name}')
二、正则表达式重命名
当文件名具有一定的模式或规律时,正则表达式是最强大的提取和替换工具。正则表达式(Regular Expression)通过定义匹配模式,可以精确识别文件名中的特定部分,并对其进行灵活替换和重组。这在处理从不同来源下载的文件、系统导出的日志文件、或者需要统一规范命名的项目文件时尤为有效。
Python 的 re 模块提供了完整的正则表达式支持。在文件重命名场景中,最常用的函数包括 re.sub() 用于替换匹配内容、re.search() 用于提取特定部分、以及 re.findall() 用于获取所有匹配项。通过组合这些函数,可以实现几乎任何复杂的文件名转换需求,比如将中文括号替换为英文括号、去除文件名中的版本号、或者从复杂的文件名中提取日期信息。
命名格式转换是正则表达式重命名的典型应用场景。在不同的编程规范和团队习惯中,文件名可能采用不同风格:蛇形命名(snake_case,各单词用下划线连接)、驼峰命名(CamelCase,每个单词首字母大写)、连字符命名(kebab-case,各单词用连字符连接)等。通过正则表达式可以在这些格式之间灵活转换,实现文件命名的统一规范。
import re
# 基本替换:去除文件名中的空格和特殊字符
def clean_filename(filename):
"""清洗文件名,只保留中文、字母、数字、下划线和连字符"""
name, ext = os.path.splitext(filename)
# 保留中文字符、字母、数字,其余替换为下划线
clean = re.sub(r'[^一-龥\w-]', '_', name)
# 合并连续下划线
clean = re.sub(r'_+', '_', clean)
return clean + ext
# 示例
print(clean_filename("项目报告 (final) v2.0!!.pdf"))
# 输出: 项目报告_final_v2_0_.pdf
# 分组替换:提取并重组文件名中的信息
def extract_and_rename(filename):
"""
从 "2023_Report_财务部_Q4.xlsx" 中提取信息
重命名为 "Q4-财务部-2023_Report.xlsx"
"""
pattern = r'(\d{4})_Report_(\w+)_(Q[1-4])\.(\w+)'
match = re.search(pattern, filename)
if match:
year, dept, quarter, ext = match.groups()
return f"{quarter}-{dept}-{year}_Report.{ext}"
return filename
# 批量处理一批报告文件
files = ['2023_Report_财务部_Q4.xlsx', '2023_Report_技术部_Q3.xlsx']
renamed = [extract_and_rename(f) for f in files]
# 结果: ['Q4-财务部-2023_Report.xlsx', 'Q3-技术部-2023_Report.xlsx']
# 格式转换:蛇形命名与驼峰命名互转
def snake_to_camel(filename):
"""蛇形命名转驼峰命名:my_file_name -> MyFileName"""
name, ext = os.path.splitext(filename)
parts = name.split('_')
camel = ''.join(p.capitalize() for p in parts if p)
return camel + ext
def camel_to_snake(filename):
"""驼峰命名转蛇形命名:MyFileName -> my_file_name"""
name, ext = os.path.splitext(filename)
# 在字母边界插入下划线并转小写
snake = re.sub(r'(?<=[a-z])(?=[A-Z])', '_', name).lower()
return snake + ext
print(snake_to_camel("project_data_report.py"))
# 输出: ProjectDataReport.py
print(camel_to_snake("ProjectDataReport.py"))
# 输出: project_data_report.py
# 批量匹配特定模式的文件
def filter_and_rename(folder, pattern, replacement):
"""按正则模式匹配并重命名"""
for file in os.listdir(folder):
old_name = os.path.join(folder, file)
if os.path.isfile(old_name):
new_name = re.sub(pattern, replacement, file)
if new_name != file:
os.rename(old_name, os.path.join(folder, new_name))
# 将所有 "IMG_2023xxxx.jpg" 改为 "Photo_2023xxxx.jpg"
filter_and_rename('./photos', r'^IMG_', 'Photo_')
使用正则表达式重命名时,建议先通过 re.findall() 或 re.finditer() 预览所有匹配结果,确认模式正确后再执行实际的重命名操作。另外,边界情况需要特别注意:通配符过宽可能导致误匹配,建议使用 ^ 和 $ 锚定边界,同时确保正则表达式不会意外捕获到文件扩展名部分。
三、序号批量命名
序号批量命名是最直观、使用范围最广的文件整理方式。当我们有一批文件需要按顺序排列时,通过添加序号前缀可以让文件在资源管理器中自动按指定顺序显示。常见的场景包括课件资料整理、照片序列归档、项目版本管理等。序号命名看似简单,但在实际应用中涉及序号填充位数、排序依据、序号范围控制等多个技术细节。
序号填充是序号命名的关键环节。如果不做填充处理,当文件超过10个时会出现 1, 10, 11, 2, 21 这样的排序混乱。使用 Python 的格式化语法(如 f"{i:03d}")可以指定固定的序号位数,确保排序正确。一般来说,建议根据文件总数来动态计算需要的位数——如果不足100个文件用2位,超过100个用3位,以此类推。
排序依据决定了序号的分配顺序。最常见的排序方式是按文件名排序和按创建时间排序。按文件名排序适合于已有一定命名规律的文件集合;按创建时间排序则适用于按时间顺序整理照片、日志等文件。还可以按文件大小排序(从大到小或从小到大)或按修改时间排序。不同的排序依据适用于不同的业务场景,需要灵活选择。
import os
from pathlib import Path
# 基础序号命名:前缀 + 序号
def basic_rename(folder, prefix='file_', digits=3):
files = sorted([f for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f))])
total = len(files)
for i, filename in enumerate(files, 1):
_, ext = os.path.splitext(filename)
new_name = f"{prefix}{i:0{digits}d}{ext}"
os.rename(
os.path.join(folder, filename),
os.path.join(folder, new_name)
)
print(f"共重命名 {total} 个文件")
# 使用示例
basic_rename('./course_materials', '第', digits=3)
# 文件变为: 第001.pdf, 第002.pdf, ... 第012.pdf
# 按创建时间排序命名
import time
def rename_by_date(folder, prefix='photo_', digits=4):
items = []
for file in Path(folder).iterdir():
if file.is_file():
# 获取创建时间
ctime = file.stat().st_ctime
items.append((ctime, file))
# 按创建时间排序
items.sort(key=lambda x: x[0])
for i, (_, file) in enumerate(items, 1):
new_name = f"{prefix}{i:0{digits}d}{file.suffix}"
file.rename(Path(folder) / new_name)
print(f"按时间排序完成,共处理 {len(items)} 个文件")
# 按文件大小排序命名(从大到小)
def rename_by_size(folder, prefix='file_'):
items = []
for file in Path(folder).iterdir():
if file.is_file():
items.append((file.stat().st_size, file))
# 从大到小排序
items.sort(key=lambda x: x[0], reverse=True)
for i, (size, file) in enumerate(items, 1):
new_name = f"{prefix}{i:03d}{file.suffix}"
file.rename(Path(folder) / new_name)
print(f"按大小排序完成,最大文件: {items[0][0]/1024:.1f}KB")
# 自定义序号范围与多级序号
def custom_seq_rename(folder, start=1, step=1, prefix='img_', suffixes=None):
"""
自定义序号范围、步长和后缀
suffixes: 可选列表,对应每个文件添加的不同后缀标记
"""
files = sorted(Path(folder).iterdir(), key=lambda f: f.name)
current = start
for i, file in enumerate(files):
if file.is_file():
suffix_tag = f"_{suffixes[i]}" if suffixes and i < len(suffixes) else ""
new_name = f"{prefix}{current:03d}{suffix_tag}{file.suffix}"
file.rename(Path(folder) / new_name)
current += step
# 示例:照片按组命名
custom_seq_rename('./photos', start=1, prefix='vacation_', suffixes=['beach', 'mountain', 'city', 'beach'])
# 生成: vacation_001_beach.jpg, vacation_002_mountain.jpg, ...
在实际应用中,序号命名还要考虑一些进阶需求:多批次命名时如何避免序号冲突(可维护一个全局序号计数器);从特定数字开始而非总是从1开始(便于与其他批次文件衔接);逆序编号(最新的排在前面)等。另外,如果需要保留原始文件名的部分内容,可以将序号与原始名称组合,形成"序号_原始名"的复合命名方式。
四、日期与模板命名
日期命名是最具有时间意义和可追溯性的文件命名方式。通过将文件的时间信息嵌入到文件名中,可以直观地了解文件的创建或修改时间,便于后续按时间维度进行检索和归档。Python 的 datetime 模块提供了完善的日期格式化功能,结合文件的元数据(创建时间、修改时间、拍摄时间等),可以构建出高度可读的日期命名方案。
自定义模板命名提供了最大的灵活性。通过定义模板字符串,可以自由组合日期、序号、原始文件名、自定义标签等元素。模板格式如 "{date}_{seq}_{name}" 可以生成类似 "2026-05-05_001_年度报告.pdf" 的文件名。这种模板化设计使重命名规则易于理解和复用,也方便通过配置文件来动态调整命名规则。
元数据嵌入是高级命名技巧。除了标准的时间信息,还可以从文件中提取更多元数据作为命名依据。例如,照片的 GPS 位置信息(可用于生成"2026-05-05_北京故宫.jpg")、文档的作者信息、音频文件的专辑信息等。结合对应的解析库(如 PIL 读取图片 Exif 信息、mutagen 读取音频元数据等),可以实现更加智能和个性化的文件命名。
import os
from datetime import datetime
from pathlib import Path
# 基础日期命名
def date_rename(folder, date_format='%Y%m%d'):
"""按文件创建时间添加日期前缀"""
for file in Path(folder).iterdir():
if file.is_file():
ctime = datetime.fromtimestamp(file.stat().st_ctime)
date_str = ctime.strftime(date_format)
new_name = f"{date_str}_{file.name}"
file.rename(Path(folder) / new_name)
# 精确到秒的命名
def precise_timestamp(folder):
for file in Path(folder).iterdir():
if file.is_file():
mtime = datetime.fromtimestamp(file.stat().st_mtime)
ts = mtime.strftime('%Y%m%d_%H%M%S')
new_name = f"{ts}{file.suffix}"
file.rename(Path(folder) / new_name)
# 示例
date_rename('./reports', '%Y-%m-%d')
# 文件: 2026-05-05_季度报告.xlsx, 2026-05-05_月度总结.docx
# 模板命名引擎
from string import Template
class TemplateRenamer:
"""基于模板的文件重命名器"""
def __init__(self, template_str):
self.template = template_str
def rename(self, folder, seq_start=1):
files = sorted(Path(folder).iterdir(), key=lambda f: f.stat().st_ctime)
for i, file in enumerate(files, seq_start):
if file.is_file():
stat = file.stat()
# 准备模板变量
vars_dict = {
'date': datetime.fromtimestamp(stat.st_ctime).strftime('%Y%m%d'),
'datetime': datetime.fromtimestamp(stat.st_ctime).strftime('%Y%m%d_%H%M%S'),
'year': datetime.fromtimestamp(stat.st_ctime).strftime('%Y'),
'month': datetime.fromtimestamp(stat.st_ctime).strftime('%m'),
'day': datetime.fromtimestamp(stat.st_ctime).strftime('%d'),
'seq': f"{i:03d}",
'name': file.stem,
'ext': file.suffix,
'size': f"{stat.st_size // 1024}KB",
}
# 替换模板变量
new_name = Template(self.template).safe_substitute(vars_dict)
file.rename(Path(folder) / new_name)
print(f" {file.name} -> {new_name}")
# 使用模板
renamer = TemplateRenamer('${date}_${seq}_${name}${ext}')
renamer.rename('./docs')
# 生成: 20260505_001_项目报告.docx, 20260505_002_会议纪要.docx
# 更多模板示例
renamer2 = TemplateRenamer('${year}年${month}月_${name}_${seq}${ext}')
renamer2.rename('./reports')
# 从图片 Exif 提取拍摄日期命名
try:
from PIL import Image
from PIL.ExifTags import TAGS
except ImportError:
Image = None
def rename_photos_by_exif(folder):
"""根据照片 Exif 中的拍摄日期重命名"""
if Image is None:
print("请安装 Pillow: pip install Pillow")
return
for file in Path(folder).iterdir():
if file.suffix.lower() in ('.jpg', '.jpeg', '.png', '.tiff'):
try:
img = Image.open(file)
exif_data = img._getexif()
if exif_data:
for tag_id, value in exif_data.items():
tag_name = TAGS.get(tag_id, tag_id)
if tag_name == 'DateTimeOriginal':
# Exif 时间格式: '2026:05:05 14:30:00'
dt = datetime.strptime(value, '%Y:%m:%d %H:%M:%S')
new_name = f"photo_{dt.strftime('%Y%m%d_%H%M%S')}{file.suffix}"
file.rename(Path(folder) / new_name)
print(f" {file.name} -> {new_name}")
break
except Exception as e:
print(f"处理 {file.name} 时出错: {e}")
# 通用元数据提取模板
def meta_template_rename(folder, template='${date}_${author}_${title}${ext}'):
"""从文档属性中提取元数据用于命名(需安装 python-docx 等库)"""
# 示例逻辑:根据扩展名选择不同的元数据提取方式
for file in Path(folder).iterdir():
if file.is_file() and file.suffix == '.docx':
try:
from docx import Document
doc = Document(file)
props = doc.core_properties
author = props.author or 'unknown'
title = props.title or file.stem
created = props.created or datetime.now()
date_str = created.strftime('%Y%m%d')
new_name = f"{date_str}_{author}_{title}{file.suffix}"
# 清理不合法的文件名字符
new_name = re.sub(r'[\\/:*?"<>|]', '_', new_name)
file.rename(Path(folder) / new_name)
except ImportError:
print("请安装 python-docx: pip install python-docx")
break
模板命名的最佳实践是保持灵活性和可控性的平衡。建议将模板字符串作为配置参数存储在外部文件中(如 JSON 或 YAML),这样非技术人员也可以方便地调整命名规则。同时,模板中应包含足够的变量用于生成唯一文件名(例如同时包含日期和序号),避免因时间精度不够导致的文件名冲突。
五、自动分类整理
文件自动分类整理是提升工作效率的关键功能。当我们的下载文件夹、桌面或工作目录积累了大量文件后,手动将它们分门别类地整理到不同文件夹中耗时且容易出错。Python 提供了强大的文件操作能力,可以根据文件的扩展名、类型、创建时间、大小等属性,自动将文件归入对应的分类目录中。
按扩展名分类是最基础的分类方式。通过建立扩展名与分类名称的映射表,程序可以自动将 .pdf/.docx 归入"文档"、.jpg/.png 归入"图片"、.mp4/.avi 归入"视频"、.mp3/.wav 归入"音频"。扩展名映射通常是大小写不敏感的,并且需要涵盖常见的文件类型。对于遇到的不识别扩展名,可以统一归入"其他"类别。
按日期分类是另一个实用的维度。文件系统会记录每个文件的创建时间、修改时间和最后访问时间。通过提取这些时间信息,可以按年、按月、按日创建层级目录,如 "2026/05/05/文件名.pdf"。这种按时间归档的方式特别适合照片管理、日志备份等场景,让文件的时空脉络一目了然。
import os
import shutil
from pathlib import Path
# 按扩展名自动分类
def classify_by_extension(folder):
"""按文件扩展名分类到对应子目录"""
# 扩展名 -> 目录名 映射
type_map = {
'文档': ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.md'],
'图片': ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.webp', '.ico'],
'视频': ['.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.m4v'],
'音频': ['.mp3', '.wav', '.flac', '.aac', '.ogg', '.wma', '.m4a'],
'压缩包': ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'],
'程序': ['.exe', '.msi', '.sh', '.bat', '.py', '.js', '.html', '.css'],
}
files_moved = 0
for file in Path(folder).iterdir():
if file.is_file():
suffix = file.suffix.lower()
target_dir = '其他'
for dir_name, extensions in type_map.items():
if suffix in extensions:
target_dir = dir_name
break
target_path = Path(folder) / target_dir
target_path.mkdir(exist_ok=True)
shutil.move(str(file), str(target_path / file.name))
files_moved += 1
print(f"分类完成,共移动 {files_moved} 个文件")
classify_by_extension('./downloads')
# 下载目录下的文件将被分类到 文档/ 图片/ 视频/ 等子目录
# 按日期自动分类
from datetime import datetime
def classify_by_date(folder, date_source='ctime', depth='month'):
"""按时间分类:年 / 年-月 / 年-月-日"""
depth_map = {
'year': lambda dt: f"{dt.year}",
'month': lambda dt: f"{dt.year}-{dt.month:02d}",
'day': lambda dt: f"{dt.year}-{dt.month:02d}-{dt.day:02d}",
}
time_func = depth_map.get(depth, depth_map['month'])
ts_func = {
'ctime': lambda s: s.st_ctime,
'mtime': lambda s: s.st_mtime,
}.get(date_source, lambda s: s.st_ctime)
for file in Path(folder).iterdir():
if file.is_file():
dt = datetime.fromtimestamp(ts_func(file.stat()))
subdir = time_func(dt)
target = Path(folder) / subdir
target.mkdir(parents=True, exist_ok=True)
shutil.move(str(file), str(target / file.name))
print(f"{file.name} -> {subdir}/")
# 按修改时间分类,精确到月深度的目录
classify_by_date('./backup', date_source='mtime', depth='month')
# 文件被归入: 2026-05/, 2026-04/, ... 等目录
# 按大小分类 + 自定义规则
def classify_by_size(folder):
"""按文件大小分类:小(<1MB) 中(1-100MB) 大(>100MB)"""
size_categories = {
'小文件': (0, 1 * 1024 * 1024), # 0 ~ 1MB
'中文件': (1 * 1024 * 1024, 100 * 1024 * 1024), # 1MB ~ 100MB
'大文件': (100 * 1024 * 1024, float('inf')), # >100MB
}
for file in Path(folder).iterdir():
if file.is_file():
size = file.stat().st_size
for dir_name, (low, high) in size_categories.items():
if low <= size < high:
target = Path(folder) / dir_name
target.mkdir(exist_ok=True)
shutil.move(str(file), str(target / file.name))
break
# 组合分类规则
def smart_classify(folder):
"""智能分类:先按类型,再按日期"""
type_map = {
'文档': ['.pdf', '.docx', '.xlsx', '.pptx', '.txt'],
'图片': ['.jpg', '.png', '.gif'],
'视频': ['.mp4', '.mkv'],
}
for file in Path(folder).iterdir():
if file.is_file():
# 确定类型
suffix = file.suffix.lower()
file_type = '其他'
for t, exts in type_map.items():
if suffix in exts:
file_type = t
break
# 确定日期
dt = datetime.fromtimestamp(file.stat().st_ctime)
date_dir = f"{dt.year}-{dt.month:02d}"
# 类型/日期 层级目录
target = Path(folder) / file_type / date_dir
target.mkdir(parents=True, exist_ok=True)
shutil.move(str(file), str(target / file.name))
在设计分类整理系统时,需要考虑几个实际因素:一是避免重复移动——已经处于正确分类目录下的文件不应再被移动;二是不要处理正在被其他程序使用的文件,可以通过尝试以独占模式打开来检测;三是对大量文件进行操作时建议显示进度条(使用 tqdm 库)让用户了解处理进度。此外,分类规则最好设计为可配置的,通过外部配置文件定义分类映射,这样用户无需修改代码即可调整分类策略。
六、文件名清洗
文件名清洗是文件整理工作中的预处理环节。从网络下载的文件、从不同系统导出的文件、或者他人发来的文件,文件名中常常包含各种不规范字符:乱码字符、多余的空格、特殊符号、全半角混用、过长文件名等。这些问题不仅影响美观,还可能导致某些系统或软件无法正确处理文件。一个健壮的文件名清洗工具是自动化整理系统的基础组件。
文件名清洗的主要任务包括:去除或替换操作系统不允许的字符(Windows 下不能包含 \ / : * ? " < > |)、控制字符和不可见字符;统一字母大小写风格;替换多余的空格和标点符号;处理中文乱码和编码转换问题;截断超长文件名使其符合系统限制。每个清洗步骤都应该可配置和可选,以适应不同的使用场景。
编码问题是文件名清洗中比较棘手的问题。不同系统默认的字符编码不同(Windows 使用 GBK,现代 Linux/macOS 使用 UTF-8),文件在跨系统传输时容易出现乱码。Python 的字符串编码转换功能可以帮助修复部分乱码问题,但无法保证100%恢复。预防性的策略是在文件整理的一开始就统一编码规范,避免混用。
import os
import re
import unicodedata
from pathlib import Path
# 通用文件名清洗函数
def sanitize_filename(filename, replacement='_'):
"""
清洗文件名:去除非法字符、控制字符、多余空格
参数:
filename: 原始文件名
replacement: 非法字符替换为的字符串
返回:清洗后的合法文件名
"""
name, ext = os.path.splitext(filename)
# 1. 去除控制字符和不可见字符
name = ''.join(c if unicodedata.category(c)[0] != 'C' else replacement for c in name)
# 2. 替换 Windows 非法字符
illegal = r'[\\/:*?"<>|]'
name = re.sub(illegal, replacement, name)
# 3. 合并连续替换字符
name = re.sub(f'{re.escape(replacement)}+', replacement, name)
# 4. 去除首尾替换字符和空格
name = name.strip(f'{replacement} ')
# 5. 处理空名称
if not name:
name = 'unnamed'
# 6. 限制总长度 (Windows MAX_PATH 减去目录路径)
max_len = 200 - len(ext)
if len(name) > max_len:
name = name[:max_len]
return name + ext
# 测试
test_names = [
'file:report*2026?.docx',
' 有很多空格 的文件 .txt',
'a\tb\nc.txt',
'CON.txt', # Windows 保留设备名
]
for name in test_names:
print(f"'{name}' -> '{sanitize_filename(name)}'")
# 统一大小写和替换空格
def normalize_case(folder, mode='lower'):
"""统一文件名大小写"""
mode_funcs = {
'lower': lambda s: s.lower(),
'upper': lambda s: s.upper(),
'title': lambda s: s.title(),
}
case_func = mode_funcs.get(mode, mode_funcs['lower'])
for file in Path(folder).iterdir():
if file.is_file():
name, ext = os.path.splitext(file.name)
# 扩展名通常保持小写
ext = ext.lower()
new_name = case_func(name) + ext
if new_name != file.name:
file.rename(Path(folder) / new_name)
def replace_spaces(folder, replacement='_'):
"""将文件名中的空格替换为指定字符"""
for file in Path(folder).iterdir():
if file.is_file() and ' ' in file.name:
new_name = file.name.replace(' ', replacement)
file.rename(Path(folder) / new_name)
# 全角转半角
def fullwidth_to_halfwidth(filename):
"""全角字符转半角"""
result = []
for char in filename:
code = ord(char)
# 全角字母数字转半角
if 0xFF01 <= code <= 0xFF5E:
result.append(chr(code - 0xFEE0))
elif code == 0x3000: # 全角空格
result.append(chr(0x0020))
else:
result.append(char)
return ''.join(result)
# 综合清洗管线
def pipeline_clean(folder):
"""执行完整清洗流程"""
for file in Path(folder).iterdir():
if file.is_file():
name = file.name
name = fullwidth_to_halfwidth(name) # 全角转半角
name = sanitize_filename(name) # 去非法字符
name = re.sub(r'\s+', '_', name) # 空格替换为下划线
name = re.sub(r'_{2,}', '_', name) # 合并连续下划线
name, ext = os.path.splitext(name)
name = name.lower() # 统一小写
ext = ext.lower()
new_name = name + ext
if new_name != file.name:
file.rename(Path(folder) / new_name)
# 中文乱码修复(常见场景)
def fix_garbled_text(text):
"""尝试修复常见编码错误导致的中文乱码"""
# 场景1: UTF-8 被解析为 Latin-1
try:
fixed = text.encode('latin-1').decode('utf-8')
if any('一' <= c <= '鿿' for c in fixed):
return fixed
except:
pass
# 场景2: GBK 被解析为 Latin-1
try:
fixed = text.encode('latin-1').decode('gbk')
if any('一' <= c <= '鿿' for c in fixed):
return fixed
except:
pass
return text
# 批量清洗并重命名
def clean_rename_batch(folder, rules=None):
"""
批量清洗重命名
rules: 需要应用的规则列表 ['lower', 'spaces', 'illegal', 'fullwidth', 'truncate']
"""
default_rules = ['fullwidth', 'illegal', 'spaces', 'lower']
rules = rules or default_rules
rule_funcs = {
'fullwidth': fullwidth_to_halfwidth,
'illegal': lambda s: sanitize_filename(s),
'spaces': lambda s: re.sub(r'\s+', '_', s),
'lower': lambda s: s.lower(),
'truncate': lambda s: s[:200] if len(s) > 200 else s,
}
for file in Path(folder).iterdir():
if file.is_file():
name = file.name
for rule in rules:
if rule in rule_funcs:
name = rule_funcs[rule](name)
if name != file.name:
file.rename(Path(folder) / name)
print(f" {file.name} -> {name}")
clean_rename_batch('./downloads', rules=['fullwidth', 'illegal', 'spaces'])
文件名清洗中有一个容易被忽视的问题:Windows 下的保留名称。CON、PRN、AUX、NUL、COM1-COM9、LPT1-LPT9 是系统保留的设备名称,不能用作文件名。如果清洗后的文件名恰好等于这些保留名,需要额外添加前缀或后缀。另外,文件名开头或结尾的空格在 Windows 资源管理器中会被自动去除,在文件名末尾加点的操作同样不被支持,这些边界情况都需要在清洗函数中妥善处理。
七、重复文件检测
重复文件是磁盘空间的隐形杀手。随着使用电脑时间的增长,下载目录、备份目录、文档目录中日积月累产生了大量重复文件——同样的照片保存了多个副本、同一份文档在不同文件夹中多次出现、下载的安装包和解压后的内容重复等。使用 Python 进行重复文件检测和清理,可以系统地解决这一问题,释放宝贵的存储空间。
检测重复文件的核心技术是文件哈希值比对。哈希函数(如 MD5、SHA1、SHA256)可以将任意大小的文件映射为一个固定长度的摘要字符串,内容完全相同的文件会产生相同的哈希值。MD5 运算速度最快但理论上存在哈希碰撞的可能;SHA1 更安全一些;SHA256 安全性最高但计算速度较慢。对于一般的重复文件检测来说,MD5 已经足够使用。为了提高效率,通常会采用两阶段策略:先按文件大小初筛(不同大小的文件一定不是重复的),再对大小相同的文件计算哈希精确比对。
除了精确的哈希比对,模糊相似度检测可以找到内容相近但不完全相同的文件(如图片的不同分辨率版本、文档的不同修订版本)。这需要更复杂的算法支持,如图片的感知哈希(pHash)、文本文件的相似度计算等。在实际应用中,精确比对已经能满足大多数场景的需求,模糊匹配作为辅助手段用于特定场景。
import os
import hashlib
from pathlib import Path
from collections import defaultdict
# 单文件哈希计算
def file_hash(filepath, algorithm='md5', chunk_size=8192):
"""计算文件的哈希值,支持 MD5/SHA1/SHA256"""
h = hashlib.new(algorithm)
with open(filepath, 'rb') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
h.update(chunk)
return h.hexdigest()
# 完整重复文件检测
def find_duplicates(folder, algorithm='md5'):
"""
检测文件夹中的重复文件
策略:先按大小分组,再对同组文件计算哈希
"""
# 第一步:按大小分组
size_map = defaultdict(list)
for file in Path(folder).rglob('*'): # 递归所有子目录
if file.is_file():
size_map[file.stat().st_size].append(file)
# 第二步:对大小相同的文件计算哈希
hash_map = defaultdict(list)
for size, files in size_map.items():
if len(files) < 2: # 没有重复可能
continue
for file in files:
h = file_hash(file, algorithm)
hash_map[h].append(file)
# 提取真正的重复组
duplicates = {h: files for h, files in hash_map.items() if len(files) > 1}
return duplicates
# 使用示例
dups = find_duplicates('./downloads')
if dups:
print(f"发现 {len(dups)} 组重复文件:")
for h, files in dups.items():
print(f"\n哈希: {h[:12]}...")
for f in files:
print(f" - {f} ({f.stat().st_size / 1024:.1f}KB)")
else:
print("未发现重复文件")
# 生成重复文件报告
def duplicate_report(folder, output='duplicate_report.txt'):
"""生成重复文件报告并估算可释放空间"""
dups = find_duplicates(folder)
total_wasted = 0
with open(output, 'w', encoding='utf-8') as f:
f.write("=" * 60 + "\n")
f.write("重复文件检测报告\n")
f.write(f"扫描目录: {folder}\n")
f.write("=" * 60 + "\n\n")
for h, files in dups.items():
size = files[0].stat().st_size
wasted = size * (len(files) - 1)
total_wasted += wasted
f.write(f"重复组 (大小: {size/1024:.1f}KB, 浪费: {wasted/1024:.1f}KB):\n")
for file in files:
f.write(f" {file}\n")
f.write("\n")
f.write("=" * 60 + "\n")
f.write(f"总计: {len(dups)} 组重复, 可释放空间: {total_wasted/1024/1024:.2f}MB\n")
print(f"报告已生成: {output}")
print(f"可释放空间: {total_wasted/1024/1024:.2f}MB")
return dups
# 去重策略:保留一个,其他移动到回收区域
def remove_duplicates(folder, action='move', backup_dir='./duplicates_backup'):
"""执行去重操作
action: 'delete' 直接删除, 'move' 移动到备份目录, 'report' 仅报告
"""
dups = duplicate_report(folder)
if action == 'report':
return dups
backup_path = Path(backup_dir)
for h, files in dups.items():
original = files[0] # 保留第一个
for dup in files[1:]:
if action == 'delete':
dup.unlink()
print(f" 删除: {dup}")
elif action == 'move':
backup_path.mkdir(parents=True, exist_ok=True)
import shutil
shutil.move(str(dup), str(backup_path / dup.name))
print(f" 移动: {dup} -> backup/")
# 安全去重(交互确认)
def safe_deduplicate(folder):
"""交互式去重,让用户选择保留哪些"""
dups = duplicate_report(folder)
for h, files in dups.items():
print(f"\n重复组 ({files[0].stat().st_size/1024:.1f}KB):")
for i, f in enumerate(files):
print(f" [{i}] {f}")
choice = input("保留哪个文件? (输入编号,默认0): ").strip()
if not choice:
choice = 0
else:
choice = int(choice)
for i, f in enumerate(files):
if i != choice:
f.unlink()
print(f" 已删除: {f}")
# 高效的增量检测(使用缓存)
import json
class DuplicateCache:
"""使用缓存文件加速重复检测"""
def __init__(self, cache_file='.dup_cache.json'):
self.cache_file = cache_file
self.cache = self._load_cache()
def _load_cache(self):
try:
with open(self.cache_file, 'r') as f:
return json.load(f)
except:
return {}
def _save_cache(self):
with open(self.cache_file, 'w') as f:
json.dump(self.cache, f)
def scan(self, folder):
"""增量扫描,只计算新文件和被修改的文件"""
results = []
for file in Path(folder).rglob('*'):
if file.is_file():
stat = file.stat()
key = str(file.absolute())
cached = self.cache.get(key)
# 检查文件是否被修改过
if cached and cached['mtime'] == stat.st_mtime and cached['size'] == stat.st_size:
results.append((key, cached['hash']))
else:
h = file_hash(file)
self.cache[key] = {
'hash': h,
'mtime': stat.st_mtime,
'size': stat.st_size,
}
results.append((key, h))
self._save_cache()
# 分析重复
hash_groups = defaultdict(list)
for path, h in results:
hash_groups[h].append(path)
return {h: paths for h, paths in hash_groups.items() if len(paths) > 1}
# 模糊相似度检测(文件名层级)
def find_name_similar_duplicates(folder, threshold=0.85):
"""检测文件名相似的重复文件(比如不同版本的同一文件)"""
from difflib import SequenceMatcher
files = list(Path(folder).rglob('*'))
similar_pairs = []
for i in range(len(files)):
if not files[i].is_file():
continue
for j in range(i + 1, len(files)):
if not files[j].is_file():
continue
name1, name2 = files[i].stem, files[j].stem
ratio = SequenceMatcher(None, name1, name2).ratio()
if ratio >= threshold:
similar_pairs.append((files[i], files[j], ratio))
return similar_pairs
去重操作具有一定的风险性,特别是当涉及"删除"操作时。建议采取"三步走"的安全策略:第一步仅生成报告并估算可释放空间;第二步将重复文件移至备份目录而非直接删除;第三步在确认无误后再清空备份目录。对于不确定是否要删除的重复文件,可以在文件名中添加 ".duplicate" 标记而非直接删除,这样既不影响正常使用,又便于后续审查。
八、整理流程自动化
将文件整理工作从手动操作升级为自动化运行,是提升长期效率的关键。一个完善的自动化整理系统应该包含以下几个核心组件:目录监控模块,持续监听指定目录的文件变化;规则引擎,根据预定义或动态配置的规则执行整理操作;日志系统,记录每次整理操作的详细信息;以及撤销机制,在误操作时能够快速恢复文件的原始状态。
目录监控可以通过两种方式实现:轮询(polling)和事件驱动(event-driven)。轮询方式定时扫描目录,比较文件列表的变化,实现简单但有一定延迟且消耗资源。事件驱动方式使用操作系统的文件系统通知机制(如 Windows 的 ReadDirectoryChangesW、Linux 的 inotify、macOS 的 FSEvents),能够实时响应文件变化。Python 的 watchdog 库封装了这些底层实现,提供统一的跨平台 API。
配置文件驱动的设计让自动化整理系统的规则无需修改代码即可调整。常用的配置格式包括 JSON、YAML 和 TOML。配置文件中定义分类规则、命名模板、忽略模式、目标路径等参数。系统的核心逻辑基于配置执行,不同的整理场景只需要切换不同的配置文件即可。这种设计也便于非开发人员通过修改配置文件来定制整理行为。
# 目录监控 + 自动整理
try:
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
except ImportError:
print("请安装 watchdog: pip install watchdog")
Observer = None
class AutoOrganizeHandler(FileSystemEventHandler):
"""文件变化事件处理器"""
def __init__(self, rules):
self.rules = rules
self.log = []
def on_created(self, event):
if event.is_directory:
return
self.process_file(event.src_path)
def on_modified(self, event):
if event.is_directory:
return
self.process_file(event.src_path)
def process_file(self, filepath):
path = Path(filepath)
if not path.exists():
return # 文件可能已被移动
# 应用整理规则
for rule in self.rules:
if self.match_rule(path, rule):
self.apply_rule(path, rule)
break
def match_rule(self, path, rule):
"""检查文件是否匹配规则条件"""
import fnmatch
# 匹配文件名模式
patterns = rule.get('patterns', ['*'])
matched = any(fnmatch.fnmatch(path.name, p) for p in patterns)
# 匹配大小条件
if matched and 'max_size' in rule:
matched = path.stat().st_size <= rule['max_size']
return matched
def apply_rule(self, path, rule):
target_dir = Path(rule['target'])
target_dir.mkdir(parents=True, exist_ok=True)
target = target_dir / path.name
import shutil
shutil.move(str(path), str(target))
self.log.append({
'time': datetime.now().isoformat(),
'source': str(path),
'target': str(target),
'rule': rule.get('name', 'unknown'),
})
print(f"已整理: {path.name} -> {target_dir.name}/")
# 启动监控
def start_monitor(watch_dir, rules, recursive=True):
if Observer is None:
return
event_handler = AutoOrganizeHandler(rules)
observer = Observer()
observer.schedule(event_handler, watch_dir, recursive=recursive)
observer.start()
print(f"监控已启动: {watch_dir}")
print("按 Ctrl+C 停止监控...")
try:
observer.join()
except KeyboardInterrupt:
observer.stop()
print("\n监控已停止")
observer.join()
return event_handler
# 示例规则
rules = [
{'name': '文档', 'patterns': ['*.pdf', '*.docx', '*.xlsx'], 'target': './organized/文档'},
{'name': '图片', 'patterns': ['*.jpg', '*.png', '*.gif'], 'target': './organized/图片'},
{'name': '小文件', 'patterns': ['*.zip', '*.rar'], 'max_size': 10*1024*1024, 'target': './organized/小包'},
]
# start_monitor('./watch_folder', rules)
# 配置文件驱动的整理系统 (config.json)
"""
config.json 示例:
{
"watch_dir": "./downloads",
"recursive": false,
"auto_clean": true,
"classification_rules": [
{
"name": "文档",
"patterns": ["*.pdf", "*.docx", "*.txt"],
"target": "./sorted/文档",
"rename_template": "${date}_${name}${ext}"
},
{
"name": "代码",
"patterns": ["*.py", "*.js", "*.html", "*.css"],
"target": "./sorted/代码",
"rename_template": "${name}${ext}"
}
],
"ignore_patterns": ["*.tmp", "*.swp", "~$*"],
"log_file": "./organize_log.json",
"max_log_entries": 1000
}
"""
import json
class ConfigDrivenOrganizer:
"""基于配置文件的自动整理器"""
def __init__(self, config_path):
with open(config_path, 'r', encoding='utf-8') as f:
self.config = json.load(f)
self.log = []
self.undo_stack = []
def run_once(self):
"""执行一次整理"""
watch_dir = self.config['watch_dir']
rules = self.config['classification_rules']
ignores = self.config.get('ignore_patterns', [])
import fnmatch
for file in Path(watch_dir).iterdir():
if file.is_file():
name = file.name
# 检查忽略规则
if any(fnmatch.fnmatch(name, p) for p in ignores):
continue
# 查找匹配规则
for rule in rules:
if any(fnmatch.fnmatch(name, p) for p in rule['patterns']):
target_dir = Path(rule['target'])
target_dir.mkdir(parents=True, exist_ok=True)
# 应用重命名模板
template = rule.get('rename_template', '${name}${ext}')
new_name = Template(template).safe_substitute({
'date': datetime.now().strftime('%Y%m%d'),
'name': file.stem,
'ext': file.suffix,
})
target = target_dir / new_name
shutil.move(str(file), str(target))
# 记录撤销信息
self.undo_stack.append({
'source': str(target),
'original': str(file),
})
break
def undo(self):
"""撤销上次操作"""
for action in reversed(self.undo_stack):
shutil.move(action['source'], action['original'])
self.undo_stack.clear()
print(f"已撤销,还原了 {len(self.undo_stack)} 个文件")
# 整理日志与撤销机制
import json
from datetime import datetime
class OrganizeManager:
"""完整的整理管理器,包含日志和撤销"""
def __init__(self, log_file='organize_history.json'):
self.log_file = log_file
self.history = self._load_history()
def _load_history(self):
try:
with open(self.log_file, 'r', encoding='utf-8') as f:
return json.load(f)
except:
return {'sessions': []}
def _save_history(self):
with open(self.log_file, 'w', encoding='utf-8') as f:
json.dump(self.history, f, ensure_ascii=False, indent=2)
def organize(self, folder, rules):
"""执行一次整理操作并记录日志"""
session = {
'id': datetime.now().strftime('%Y%m%d_%H%M%S'),
'timestamp': datetime.now().isoformat(),
'folder': folder,
'actions': []
}
for rule in rules:
for file in Path(folder).iterdir():
if file.is_file():
import fnmatch
if any(fnmatch.fnmatch(file.name, p) for p in rule['patterns']):
target = Path(rule['target'])
target.mkdir(parents=True, exist_ok=True)
dst = target / file.name
shutil.move(str(file), str(dst))
session['actions'].append({
'file': file.name,
'from': str(file),
'to': str(dst),
'rule': rule['name'],
})
self.history['sessions'].append(session)
self._save_history()
print(f"整理完成,操作 {len(session['actions'])} 个文件")
return session
def undo_session(self, session_id):
"""撤销指定次整理操作"""
for session in self.history['sessions']:
if session['id'] == session_id:
# 逆序还原
for action in reversed(session['actions']):
if Path(action['to']).exists():
shutil.move(action['to'], action['from'])
self.history['sessions'].remove(session)
self._save_history()
print(f"已撤销整理会话: {session_id}")
return True
print(f"未找到会话: {session_id}")
return False
def list_history(self):
"""查看整理历史"""
for session in self.history['sessions']:
print(f"[{session['id']}] {session['timestamp']} - {len(session['actions'])} 个文件")
for a in session['actions'][:5]:
print(f" {a['file']} -> {a['to']}")
if len(session['actions']) > 5:
print(f" ... 还有 {len(session['actions'])-5} 个文件")
def schedule(self, folder, rules, interval_hours=24):
"""定时执行整理(运行在后台线程)"""
import threading
def run_loop():
while True:
self.organize(folder, rules)
time.sleep(interval_hours * 3600)
thread = threading.Thread(target=run_loop, daemon=True)
thread.start()
print(f"定时整理已启动,每 {interval_hours} 小时执行一次")
构建自动化整理系统时,健壮性和安全性是最重要的考量。建议遵循"非破坏性原则":所有操作先记录日志,移动而非删除,保留撤销能力。对于生产环境使用的自动整理系统,还应该添加运行状态监控、异常告警、资源限制(防止占用过多 CPU/磁盘IO)等特性。另外,建议在正式运行前先使用测试目录全面验证规则的正确性,避免正式目录中的文件被误整理。
九、实战案例
理论知识的最终价值在于解决实际问题。本章通过三个完整的实战案例——照片整理工具、下载目录自动分类工具、项目文件规范化工具——展示如何将前面学到的重命名、分类、清洗、去重等技术整合到一个完整的自动化工具中。每个案例都提供可直接运行的代码和详细的设计思路说明。
照片整理工具是日常生活中最常用的文件整理需求之一。手机、相机、无人机等设备产生的大量照片通常命名混乱(如 IMG_0001.jpg、DSC_0001.jpg 等),需要按拍摄时间、地点等信息重新组织。一个完善的照片整理工具应该能读取 Exif 信息中的拍摄时间和 GPS 数据,按"年/月/日"或"年/月/主题"的目录结构归档,并自动去除连拍产生的重复照片。
下载目录自动分类解决的是几乎所有电脑用户都会面临的痛点——下载文件夹日益膨胀、文件类型混杂、查找困难。通过文件类型自动识别和归档,可以让下载目录始终保持整洁。分类规则需要能够覆盖常见的文件类型,并且对于不认识的扩展名也能妥善处理。
# 案例一:照片整理工具
"""
功能:
1. 读取照片 Exif 信息获取拍摄日期和 GPS
2. 按年/月/日创建目录结构
3. 可选择按地点添加子分类
4. 自动去除重复照片
5. 生成整理报告
"""
from pathlib import Path
import shutil
import hashlib
from datetime import datetime
class PhotoOrganizer:
def __init__(self, source, dest=None):
self.source = Path(source)
self.dest = Path(dest) if dest else self.source / 'organized'
self.stats = {'total': 0, 'organized': 0, 'duplicates': 0, 'errors': 0}
self.photo_exts = {'.jpg', '.jpeg', '.png', '.tiff', '.tif',
'.bmp', '.gif', '.webp', '.heic', '.raw', '.cr2'}
def get_photo_date(self, photo_path):
"""从 Exif 或文件系统获取拍摄日期"""
try:
from PIL import Image
from PIL.ExifTags import TAGS
img = Image.open(photo_path)
exif = img._getexif()
if exif:
for tag_id, value in exif.items():
tag = TAGS.get(tag_id)
if tag == 'DateTimeOriginal':
return datetime.strptime(value, '%Y:%m:%d %H:%M:%S')
except:
pass
# 降级使用文件修改时间
mtime = photo_path.stat().st_mtime
return datetime.fromtimestamp(mtime)
def get_gps_location(self, photo_path):
"""从 Exif 提取 GPS 信息(简化版)"""
try:
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
img = Image.open(photo_path)
exif = img._getexif()
if not exif:
return None
gps_info = {}
for tag_id, value in exif.items():
tag = TAGS.get(tag_id)
if tag == 'GPSInfo':
for gps_tag_id, gps_value in value.items():
gps_tag = GPSTAGS.get(gps_tag_id, gps_tag_id)
gps_info[gps_tag] = gps_value
if 'GPSLatitude' in gps_info and 'GPSLongitude' in gps_info:
lat = sum(v[0]/v[1] for v in gps_info['GPSLatitude']) / len(gps_info['GPSLatitude'])
lon = sum(v[0]/v[1] for v in gps_info['GPSLongitude']) / len(gps_info['GPSLongitude'])
if gps_info.get('GPSLatitudeRef') == 'S':
lat = -lat
if gps_info.get('GPSLongitudeRef') == 'W':
lon = -lon
return f"{lat:.4f}_{lon:.4f}"
except:
pass
return None
def file_hash(self, path):
h = hashlib.md5()
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
h.update(chunk)
return h.hexdigest()
def run(self):
"""执行照片整理主流程"""
print("开始整理照片...")
hashes_seen = set()
for file in self.source.iterdir():
if file.suffix.lower() not in self.photo_exts:
continue
self.stats['total'] += 1
try:
# 去重检测
fhash = self.file_hash(file)
if fhash in hashes_seen:
self.stats['duplicates'] += 1
duplicate_dir = self.dest / '_重复文件'
duplicate_dir.mkdir(parents=True, exist_ok=True)
shutil.move(str(file), str(duplicate_dir / file.name))
continue
hashes_seen.add(fhash)
# 获取日期
date = self.get_photo_date(file)
date_dir = f"{date.year}/{date.month:02d}/{date.day:02d}"
# 获取位置
location = self.get_gps_location(file)
if location:
date_dir += f"_{location}"
# 创建目录并移动
target_dir = self.dest / date_dir
target_dir.mkdir(parents=True, exist_ok=True)
shutil.move(str(file), str(target_dir / file.name))
self.stats['organized'] += 1
print(f" ✓ {file.name} -> {date_dir}/")
except Exception as e:
self.stats['errors'] += 1
print(f" ✗ {file.name}: {e}")
self.report()
def report(self):
print("\n" + "=" * 40)
print("照片整理完成报告")
print("=" * 40)
print(f"总计处理: {self.stats['total']} 张")
print(f"已整理: {self.stats['organized']} 张")
print(f"重复文件: {self.stats['duplicates']} 张")
print(f"错误: {self.stats['errors']} 个")
# 使用
org = PhotoOrganizer('./photos', './photo_library')
org.run()
# 案例二:下载目录自动分类
"""
功能:
1. 监控下载目录的文件变化
2. 按文件类型自动分类到对应子目录
3. 自动清理临时文件
4. 支持自定义规则扩展
"""
import time
import json
from pathlib import Path
from collections import defaultdict
class DownloadOrganizer:
def __init__(self, download_dir='./downloads', config=None):
self.download_dir = Path(download_dir)
self.config = config or self.default_config()
self.type_stats = defaultdict(int)
def default_config(self):
return {
'分类规则': {
'文档': {
'扩展名': ['.pdf', '.docx', '.xlsx', '.pptx', '.txt', '.md'],
'子分类': {
'报表': ['*报告*', '*报表*', '*总结*'],
'合同': ['*合同*', '*协议*', '*契约*'],
'资料': ['*手册*', '*指南*', '*教程*'],
}
},
'图片': {
'扩展名': ['.jpg', '.jpeg', '.png', '.gif', '.svg'],
},
'视频': {
'扩展名': ['.mp4', '.avi', '.mkv', '.mov'],
},
'音频': {
'扩展名': ['.mp3', '.wav', '.flac'],
},
'压缩包': {
'扩展名': ['.zip', '.rar', '.7z', '.tar', '.gz'],
},
'安装程序': {
'扩展名': ['.exe', '.msi', '.dmg', '.AppImage'],
},
'代码': {
'扩展名': ['.py', '.js', '.html', '.css', '.java', '.cpp', '.json', '.xml'],
},
},
'临时文件': ['*.tmp', '*.temp', '*.swp', '~$*', '*.crdownload', '*.part'],
'忽略文件': ['desktop.ini', 'thumbs.db', '.ds_store'],
'自动清理临时文件': True,
'按时间子分类': False, # 若启用则按日期再分子目录
}
def organize(self):
"""执行一次整理"""
import fnmatch
files = [f for f in self.download_dir.iterdir() if f.is_file()]
if not files:
print("没有需要整理的文件")
return
print(f"开始整理 {len(files)} 个文件...")
for file in files:
# 跳过忽略文件
if any(fnmatch.fnmatch(file.name.lower(), ig.lower())
for ig in self.config['忽略文件']):
continue
# 检查是否为临时文件
if self.config['自动清理临时文件']:
if any(fnmatch.fnmatch(file.name, p) for p in self.config['临时文件']):
file.unlink()
print(f" 删除临时文件: {file.name}")
continue
# 分类移动
suffix = file.suffix.lower()
target_category = '其他'
for category, rules in self.config['分类规则'].items():
if suffix in rules['扩展名']:
target_category = category
# 检查子分类
if '子分类' in rules:
for sub_cat, patterns in rules['子分类'].items():
if any(fnmatch.fnmatch(file.stem, p) for p in patterns):
target_category = f"{category}/{sub_cat}"
break
break
target_path = self.download_dir / target_category
target_path.mkdir(parents=True, exist_ok=True)
# 处理同名冲突
dest = target_path / file.name
counter = 1
while dest.exists():
stem = file.stem
dest = target_path / f"{stem}_{counter}{suffix}"
counter += 1
shutil.move(str(file), str(dest))
self.type_stats[target_category] += 1
self.print_summary()
def print_summary(self):
print("\n整理结果:")
for category, count in sorted(self.type_stats.items()):
print(f" {category}: {count} 个文件")
def start_watch(self, interval=10):
"""持续监控模式"""
print(f"启动下载目录监控(扫描间隔: {interval}秒)...")
print("按 Ctrl+C 停止")
try:
while True:
self.organize()
time.sleep(interval)
except KeyboardInterrupt:
print("\n监控已停止")
# 使用
org = DownloadOrganizer('./downloads')
org.organize()
# 持续监控模式
# org.start_watch(interval=30)
# 案例三:项目文件规范化工具
"""
功能:
1. 扫描项目目录中所有文件
2. 统一为蛇形命名(snake_case)
3. 按文件类型整理到规范目录结构
4. 生成项目文件清单
"""
import os
import re
from pathlib import Path
class ProjectNormalizer:
"""项目文件规范化工具"""
# 推荐的 Python 项目目录结构
PROJECT_STRUCTURE = {
'src': ['.py', '.js', '.ts', '.go', '.rs'],
'tests': ['test_*.py', '*_test.py', 'test_*.js'],
'docs': ['.md', '.rst', '.txt', '.pdf', '.docx'],
'config': ['.json', '.yaml', '.yml', '.toml', '.ini', '.cfg'],
'data': ['.csv', '.xlsx', '.json', '.xml', '.db', '.sqlite'],
'scripts': ['.sh', '.bat', '.ps1'],
'assets': ['.png', '.jpg', '.svg', '.ico', '.css', '.scss'],
}
def __init__(self, project_root, dry_run=True):
self.root = Path(project_root)
self.dry_run = dry_run # 默认为模拟模式
self.log = []
def to_snake_case(self, name):
"""文件名转蛇形命名"""
name_no_ext, ext = os.path.splitext(name)
# 驼峰转蛇形
s1 = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name_no_ext)
s2 = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', s1)
# 替换所有非字母数字字符为下划线
s3 = re.sub(r'[^\w]', '_', s2)
# 合并下划线并转小写
s4 = re.sub(r'_+', '_', s3).lower()
# 去除首尾下划线
s4 = s4.strip('_')
return s4 + ext.lower()
def normalize(self):
"""执行规范化"""
print(f"{'[模拟] ' if self.dry_run else ''}开始规范化项目: {self.root}")
all_files = [f for f in self.root.rglob('*') if f.is_file()]
for file in all_files:
# 跳过 .git 等隐藏目录
rel = file.relative_to(self.root)
if any(p.startswith('.') for p in rel.parts):
continue
# 标准化文件名
new_name = self.to_snake_case(file.name)
if new_name != file.name:
new_path = file.with_name(new_name)
if not self.dry_run:
file.rename(new_path)
self.log.append(f" {file.name} -> {new_name}")
file = new_path # 更新路径用于后续操作
# 生成文件清单报告
self.generate_inventory()
def organize_to_structure(self):
"""按标准目录结构整理(可选)"""
if self.dry_run:
print("[模拟] 创建标准目录结构并移动文件...")
else:
for dir_name in self.PROJECT_STRUCTURE:
(self.root / dir_name).mkdir(exist_ok=True)
for file in self.root.iterdir():
if not file.is_file():
continue
suffix = file.suffix.lower()
target_dir = None
for dir_name, exts in self.PROJECT_STRUCTURE.items():
if suffix in exts:
target_dir = dir_name
break
if target_dir and target_dir != 'tests': # 测试文件特殊处理
target = self.root / target_dir / file.name
if not self.dry_run:
shutil.move(str(file), str(target))
self.log.append(f" {file.name} -> {target_dir}/")
def generate_inventory(self):
"""生成项目文件清单"""
inventory_file = self.root / 'FILE_INVENTORY.md'
if self.dry_run:
print(f"[模拟] 生成文件清单: {inventory_file}")
return
lines = ["# 项目文件清单", "", f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", ""]
tree = {}
for file in sorted(self.root.rglob('*')):
if file.is_file():
rel = file.relative_to(self.root)
parts = rel.parts
current = tree
for part in parts[:-1]:
current = current.setdefault(part, {})
current[parts[-1]] = file.stat().st_size
def write_tree(d, indent=0):
for name, value in sorted(d.items()):
if isinstance(value, dict):
lines.append(" " * indent + f"- **{name}/**")
write_tree(value, indent + 1)
else:
lines.append(" " * indent + f"- {name} ({value/1024:.1f}KB)")
write_tree(tree)
with open(inventory_file, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
print(f"文件清单已生成: {inventory_file}")
def undo(self):
"""撤销重命名操作"""
if not self.log:
print("没有可撤销的操作")
return
for entry in reversed(self.log):
match = re.match(r'\s*(\S+) -> (\S+)', entry)
if match:
old, new = match.groups()
old_path = self.root / old
new_path = self.root / new
if new_path.exists():
if not self.dry_run:
new_path.rename(old_path)
print(f" {new} -> {old}")
# 使用
normalizer = ProjectNormalizer('./my_project', dry_run=True)
normalizer.normalize()
print(f"\n计划执行 {len(normalizer.log)} 项修改")
print("确认后设置 dry_run=False 执行实际操作")
以上三个实战案例展示了文件自动化整理在不同场景下的应用。实际使用中,可以根据具体需求组合这些工具的功能,例如将照片整理与下载分类整合到一个统一的文件管理系统中。建议在正式使用前始终先在测试目录中充分验证,同时为每个工具都添加"模拟运行"模式(dry_run),让用户在实际执行前可以预览操作结果。通过持续迭代和优化,最终构建出一个稳定可靠的个人文件管理系统。
核心要点总结:
1. os.rename / shutil.move / pathlib 是三种基础的重命名方式,根据场景选择使用
2. 正则表达式是处理不规范文件名的利器,清洗和格式转换都需要用到
3. 序号命名时务必使用填充位(如 001)保证排序正确
4. 模板命名(日期+序号+自定义标签)提供最大灵活性
5. 自动分类推荐"扩展名映射表 + 可配置规则"的设计模式
6. 文件名清洗要覆盖非法字符、控制字符、全半角、编码等问题
7. 重复文件检测采用"先按大小分组,再计算哈希"的两阶段策略
8. 自动化整理系统必须具备日志记录和撤销机制
9. 所有批量操作前先模拟运行,确认无误后再正式执行