工具与效率

用 Python 批量重命名和归类文件:先预览结果,再安全执行

AI智能摘要

文章提供一个基于 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_GROUPSKEYWORD_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

脚本会显示“目标已存在”,并跳过该文件。这样可以避免因为同名文件被覆盖而造成数据丢失。

如果多个源文件经过重命名后得到相同目标路径,脚本也会停止执行并报告冲突。你可以采用以下方式处理:

  1. 修改命名规则,让文件名包含原始名称或序号;
  2. 手动移动其中一个文件;
  3. 修改关键词分类,让文件进入不同目录;
  4. 删除测试目录中的重复文件后重新预览。

不要直接把 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

确认文件恢复后,再修改规则并重新预览。

建议采用的安全流程

对于重要的工作资料,可以固定使用下面的流程:

  1. 先备份原始目录,保留一份未修改的副本;
  2. 在测试目录中运行脚本;
  3. 不带参数运行,查看预览结果;
  4. 处理重名、路径和分类错误;
  5. 确认无误后使用 --execute
  6. 检查分类目录和文件数量;
  7. 保留操作日志,必要时使用 --rollback
  8. 验证文件可以正常打开后,再考虑清理备份。

这样使用 Python 批量重命名和文件整理,重点不是一次处理尽可能多的文件,而是让每一步都能被预览、确认和撤销。对于下载目录、素材文件夹和项目资料,这种“先生成方案、再执行”的方式比直接编写删除或覆盖脚本更适合日常自动化办公。

52okp 是一名关注人工智能、开源软件与效率工具的技术内容创作者,长期实践 Stable Diffusion、ComfyUI、AI 智能体、MCP、Codex 和各类开源项目。通过实际安装、配置与测试,整理可复现的操作教程、问题排查方法和工具使用经验。

登录用户才能发表评论! 登录账户

取消回复

评论列表 (0条):

加载更多评论 Loading...

延伸阅读:

暂无内容!

    返回顶部