文章提供一个基于 Python 3 的文件批量重命名与归类方案,按“生成计划、预览确认、执行操作、日志回滚”流程降低误操作风险。脚本可依据扩展名、文件名关键词和修改日期分类命名,目标冲突时跳过且不覆盖,并支持 Windows 路径处理、测试目录验证及操作日志回滚。
— 此摘要由AI分析文章内容生成,仅供参考。
如果你的下载目录里同时有文档、图片、压缩包和项目素材,手动逐个改名、移动文件很容易出现重名、误删或放错位置。使用 Python 批量重命名和归类时,可以把流程拆成“生成方案—预览方案—执行操作—按日志回滚”四步:脚本先根据扩展名、关键词或文件日期计算目标路径,确认无误后才真正移动文件。
先准备环境和测试目录
你只需要安装 Python 3,不需要额外安装第三方库。Windows 用户可以从 Python 官网安装,也可以使用系统已有的 Python 环境。
在命令行中检查是否安装成功:
python --version
如果 Windows 中提示找不到 python,可以尝试:
py --version
建议先建立一个测试目录,复制少量文件进去验证脚本。例如:
待整理/
├── 项目说明.docx
├── 产品照片.jpg
├── 会议记录.pdf
├── backup.zip
└── demo.png
不要一开始就对整个“下载”目录或项目源文件夹执行操作。先复制一份测试数据,确认重命名规则、分类位置和回滚流程都正常,再处理正式文件。
Windows 路径设置方法
Windows 路径中的反斜杠容易与 Python 的转义字符混淆。下面三种写法都可以使用:
from pathlib import Path
root = Path(r"D:下载待整理")
root = Path("D:/下载/待整理")
root = Path("D:\下载\待整理")
更推荐使用 Path 和正斜杠,或者使用带 r 的原始字符串。不要直接写成下面这样:
root = Path("D:newfiles")
其中 n 可能会被解释为换行,导致路径错误。
一个支持预览和回滚的脚本
下面的脚本默认只生成预览,不会移动或删除文件。只有加入 --execute 参数后,才会执行计划中的移动和重命名操作。
它包含以下规则:
- 按文件扩展名归类;
- 按文件名中的关键词归类;
- 在文件名前增加文件修改日期;
- 遇到目标文件已存在时跳过,不覆盖;
- 执行后记录操作日志;
- 根据日志把已完成的操作回滚。
将代码保存为 organize_files.py:
from __future__ import annotations
import argparse
import json
import shutil
from datetime import datetime
from pathlib import Path
# 修改为你要整理的目录
ROOT = Path(r"D:下载待整理")
# 操作日志会保存到待整理目录中
LOG_FILE = ROOT / "file_operation_log.json"
# 扩展名分类规则
EXTENSION_GROUPS = {
".jpg": "图片",
".jpeg": "图片",
".png": "图片",
".gif": "图片",
".webp": "图片",
".doc": "文档",
".docx": "文档",
".pdf": "文档",
".txt": "文档",
".xls": "表格",
".xlsx": "表格",
".csv": "表格",
".zip": "压缩包",
".rar": "压缩包",
".7z": "压缩包",
".mp4": "视频",
".mov": "视频",
".mp3": "音频",
".wav": "音频",
}
# 文件名关键词分类规则
KEYWORD_GROUPS = {
"会议": "会议资料",
"合同": "合同",
"发票": "发票",
"项目": "项目资料",
}
def get_file_group(path: Path) -> str:
"""根据文件名关键词和扩展名判断分类。关键词优先。"""
filename = path.name.lower()
for keyword, group in KEYWORD_GROUPS.items():
if keyword.lower() in filename:
return group
return EXTENSION_GROUPS.get(path.suffix.lower(), "其他")
def get_date_prefix(path: Path) -> str:
"""使用文件修改时间生成 YYYY-MM-DD 前缀。"""
modified_time = datetime.fromtimestamp(path.stat().st_mtime)
return modified_time.strftime("%Y-%m-%d")
def make_target_path(path: Path) -> Path:
"""生成目标路径。"""
group = get_file_group(path)
date_prefix = get_date_prefix(path)
# 如果原文件名已经以日期开头,就不重复添加
if path.name.startswith(date_prefix + "_"):
new_name = path.name
else:
new_name = f"{date_prefix}_{path.name}"
return ROOT / group / new_name
def build_plan() -> list[dict]:
"""只扫描 ROOT 的直接文件,不递归处理子目录。"""
plan = []
for source in ROOT.iterdir():
if not source.is_file():
continue
# 避免把日志文件再次纳入整理范围
if source.name == LOG_FILE.name:
continue
target = make_target_path(source)
plan.append({
"source": str(source),
"target": str(target),
"group": get_file_group(source),
})
return plan
def show_plan(plan: list[dict]) -> None:
"""打印预览结果。"""
if not plan:
print("没有找到需要处理的文件。")
return
print("即将执行的文件操作:")
print("-" * 80)
for index, item in enumerate(plan, start=1):
source = Path(item["source"])
target = Path(item["target"])
if target.exists():
status = "跳过:目标已存在"
else:
status = "待执行"
print(f"{index}. {source.name}")
print(f" 分类:{item['group']}")
print(f" 目标:{target}")
print(f" 状态:{status}")
def check_conflicts(plan: list[dict]) -> list[str]:
"""检查目标冲突,不覆盖已有文件,也不允许多个源文件指向同一目标。"""
errors = []
target_sources: dict[str, str] = {}
for item in plan:
source = Path(item["source"])
target = Path(item["target"])
target_key = str(target.resolve())
if target.exists() and target.resolve() != source.resolve():
errors.append(f"目标已存在:{target}")
if target_key in target_sources:
errors.append(
f"多个文件指向同一目标:{target}n"
f" 文件一:{target_sources[target_key]}n"
f" 文件二:{source}"
)
target_sources[target_key] = str(source)
return errors
def execute_plan(plan: list[dict]) -> None:
"""执行计划,并将成功的操作写入日志。"""
conflicts = check_conflicts(plan)
if conflicts:
print("发现冲突,已停止执行:")
for error in conflicts:
print(f"- {error}")
print("请修改规则或处理冲突后,再次运行预览。")
return
records = []
for item in plan:
source = Path(item["source"])
target = Path(item["target"])
if not source.exists():
print(f"跳过:源文件不存在:{source}")
continue
if target.exists():
print(f"跳过:目标已存在:{target}")
continue
target.parent.mkdir(parents=True, exist_ok=True)
try:
shutil.move(str(source), str(target))
records.append({
"source": str(source),
"target": str(target),
"time": datetime.now().isoformat(timespec="seconds"),
})
print(f"已处理:{source.name} -> {target}")
except OSError as error:
print(f"处理失败:{source}")
print(f"原因:{error}")
LOG_FILE.write_text(
json.dumps(records, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"n操作完成,成功记录 {len(records)} 项。")
print(f"回滚日志:{LOG_FILE}")
def rollback() -> None:
"""按照日志反向移动文件。"""
if not LOG_FILE.exists():
print(f"找不到回滚日志:{LOG_FILE}")
return
records = json.loads(LOG_FILE.read_text(encoding="utf-8"))
if not records:
print("日志为空,没有可回滚的操作。")
return
# 倒序回滚,避免多个操作之间产生依赖
for record in reversed(records):
source = Path(record["source"])
target = Path(record["target"])
if not target.exists():
print(f"跳过:目标文件不存在:{target}")
continue
if source.exists():
print(f"跳过:原位置已有文件:{source}")
continue
try:
source.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(target), str(source))
print(f"已回滚:{target.name} -> {source}")
except OSError as error:
print(f"回滚失败:{target}")
print(f"原因:{error}")
print("回滚流程结束。请手动检查分类目录是否为空。")
def main() -> None:
parser = argparse.ArgumentParser(
description="先预览、后执行的文件整理脚本"
)
parser.add_argument(
"--execute",
action="store_true",
help="确认预览无误后,执行文件移动和重命名",
)
parser.add_argument(
"--rollback",
action="store_true",
help="根据操作日志回滚上一次执行的操作",
)
args = parser.parse_args()
if not ROOT.exists():
print(f"目录不存在:{ROOT}")
return
if args.rollback:
rollback()
return
plan = build_plan()
show_plan(plan)
if not args.execute:
print("n当前是预览模式,没有修改任何文件。")
print("确认无误后,运行:python organize_files.py --execute")
return
answer = input("n确认执行以上操作?请输入 YES:")
if answer != "YES":
print("未输入 YES,操作已取消。")
return
execute_plan(plan)
if __name__ == "__main__":
main()
按步骤运行:先预览,再执行
1. 设置待整理目录
修改代码中的这一行:
ROOT = Path(r"D:下载待整理")
替换为你的实际路径。例如:
ROOT = Path(r"C:Users你的用户名Downloads待整理")
路径中的用户名、磁盘盘符和目录名称都需要替换成实际值。
2. 运行预览模式
在脚本所在目录打开 PowerShell 或命令提示符:
python organize_files.py
Windows 也可以使用:
py organize_files.py
预览模式只会列出源文件、分类和目标路径,例如:
1. 产品照片.jpg
分类:图片
目标:D:下载待整理图片2025-03-08_产品照片.jpg
状态:待执行
此时不会创建分类目录,也不会移动、重命名或删除文件。
3. 检查预览结果
重点检查以下内容:
- 日期是否符合预期;
- 关键词是否被正确识别;
- 文件是否进入正确分类;
- 是否出现目标已存在;
- 是否有不应该处理的文件;
- 文件名中是否包含不适合使用的特殊字符。
如果结果不符合要求,先修改 EXTENSION_GROUPS、KEYWORD_GROUPS 或命名规则,再重新运行预览。
4. 确认后执行
预览无误后运行:
python organize_files.py --execute
脚本会再次要求输入:
确认执行以上操作?请输入 YES:
只有输入完整的 YES 才会继续执行。执行过程中,脚本会创建分类目录,并把文件移动到目标位置,同时按日期前缀重命名。
如何调整分类和重命名规则
按扩展名分类
在 EXTENSION_GROUPS 中增加规则:
EXTENSION_GROUPS = {
".psd": "设计源文件",
".ai": "设计源文件",
".fig": "设计源文件",
".md": "项目文档",
}
扩展名建议统一写成小写,并包含开头的点号。
按关键词分类
在 KEYWORD_GROUPS 中增加文件名关键词:
KEYWORD_GROUPS = {
"会议": "会议资料",
"合同": "合同",
"客户A": "客户A项目",
"短视频": "视频素材",
}
关键词匹配的是文件名,不会读取 Word、PDF 或图片内部内容。比如文件名为 客户A_报价单.xlsx,可以匹配到“客户A”;但文件名没有关键词,即使文档内容包含相关文字,也不会被识别。
取消日期重命名
如果你只想归类,不想更改文件名,可以把 make_target_path 改成:
def make_target_path(path: Path) -> Path:
group = get_file_group(path)
return ROOT / group / path.name
这样脚本仍会移动文件,但会保留原文件名。
使用创建时间而不是修改时间
脚本当前使用文件的修改时间:
path.stat().st_mtime
如果你希望使用创建时间,可以在 Windows 中改为:
def get_date_prefix(path: Path) -> str:
created_time = datetime.fromtimestamp(path.stat().st_ctime)
return created_time.strftime("%Y-%m-%d")
需要注意,文件复制、下载或同步后,创建时间和原始文件的创建时间可能不同。整理项目资料时,修改时间通常更容易反映最近编辑时间,但具体应根据你的文件来源判断。
重名冲突和安全处理
脚本默认不会覆盖已有文件。例如,目标位置已经存在:
图片2025-03-08_产品照片.jpg
脚本会显示“目标已存在”,并跳过该文件。这样可以避免因为同名文件被覆盖而造成数据丢失。
如果多个源文件经过重命名后得到相同目标路径,脚本也会停止执行并报告冲突。你可以采用以下方式处理:
- 修改命名规则,让文件名包含原始名称或序号;
- 手动移动其中一个文件;
- 修改关键词分类,让文件进入不同目录;
- 删除测试目录中的重复文件后重新预览。
不要直接把 shutil.move 改成会覆盖目标文件的写法。批量处理的核心目标是可控,而不是强行完成所有操作。
执行后的回滚方法
执行成功后,脚本会在根目录生成:
file_operation_log.json
这个文件记录每次成功移动的原路径和目标路径。需要撤销上一次操作时运行:
python organize_files.py --rollback
或者:
py organize_files.py --rollback
回滚时,脚本会根据日志倒序把文件移回原位置。倒序处理可以降低多个操作之间相互影响的风险。
回滚前不要手动修改日志内容,也不要在原位置新建同名文件。否则脚本发现原位置已有文件时会跳过该项,以避免覆盖新文件。
需要注意,日志回滚只适用于这次脚本执行的移动和重命名。如果你在执行后手动删除、再次改名或移动了文件,脚本无法自动推断这些变化。
常见问题排查
提示没有找到文件
脚本只处理 ROOT 目录下的直接文件,不会递归进入已有子目录。这样做是为了避免重复处理已经归类的文件。
如果你的文件位于更深的目录中,需要先确认路径是否设置正确,或者明确修改扫描逻辑。不要在不了解影响的情况下直接改成递归扫描,否则可能把备份目录、分类目录和隐藏的项目文件一并处理。
提示路径不存在
检查以下内容:
- 磁盘盘符是否正确;
- 文件夹名称是否拼写正确;
- 路径是否包含了错误的引号;
- 外接硬盘或网络磁盘是否已连接;
- 当前 Windows 用户是否有访问权限。
可以在 Python 中临时加入:
print(ROOT.resolve())
print(ROOT.exists())
如果第二行输出 False,说明脚本找不到设置的目录。
中文文件名显示异常
代码已经使用:
json.dumps(records, ensure_ascii=False, indent=2)
并以 UTF-8 保存日志。Windows 终端本身可能仍然存在编码显示问题,但通常不会影响文件实际名称。
如果命令行中的中文显示为乱码,可以改用 PowerShell,或在运行前执行:
chcp 65001
权限不足
如果文件正在被 Word、Excel、视频编辑软件或其他程序占用,移动操作可能失败。先关闭正在使用这些文件的软件,再重新运行。
如果目标目录位于受保护位置,也可能需要调整文件夹权限。但不要为了运行脚本就长期使用管理员权限;更稳妥的做法是把待处理文件复制到当前用户有权限的目录中测试。
脚本中途失败怎么办
先不要重复执行。查看终端输出和 file_operation_log.json,确认哪些文件已经成功移动。由于日志只记录成功的操作,重新运行前应先进行预览,并检查目标目录中的现有文件。
如果部分操作已经完成,可以先运行回滚:
python organize_files.py --rollback
确认文件恢复后,再修改规则并重新预览。
建议采用的安全流程
对于重要的工作资料,可以固定使用下面的流程:
- 先备份原始目录,保留一份未修改的副本;
- 在测试目录中运行脚本;
- 不带参数运行,查看预览结果;
- 处理重名、路径和分类错误;
- 确认无误后使用
--execute; - 检查分类目录和文件数量;
- 保留操作日志,必要时使用
--rollback; - 验证文件可以正常打开后,再考虑清理备份。
这样使用 Python 批量重命名和文件整理,重点不是一次处理尽可能多的文件,而是让每一步都能被预览、确认和撤销。对于下载目录、素材文件夹和项目资料,这种“先生成方案、再执行”的方式比直接编写删除或覆盖脚本更适合日常自动化办公。

评论列表 (0条):
加载更多评论 Loading...