二次重命名对文件批量重命名

文章目录

使用二次重命名方法可以避免文件名冲突。第一遍先给每个文件分配临时哈希名,第二遍再按顺序重命名。让我重写代码:

python 复制代码
import os
from pathlib import Path
import re
import hashlib
import time
from typing import List, Tuple, Dict


def get_all_files_in_folder(folder_path):
    """获取文件夹下的所有文件(不递归子文件夹)"""
    files = []
    try:
        for item in os.listdir(folder_path):
            item_path = os.path.join(folder_path, item)
            if os.path.isfile(item_path):
                files.append(item_path)
    except Exception as e:
        print(f"读取文件夹失败 {folder_path}: {e}")
    return files


def natural_sort_key(filename):
    """自然排序:例如 file1, file2, file10 而不是 file1, file10, file2"""
    def convert(text):
        return int(text) if text.isdigit() else text.lower()
    
    def alphanum_key(key):
        return [convert(c) for c in re.split('([0-9]+)', key)]
    
    return alphanum_key(os.path.basename(filename))


def generate_temp_name(file_path):
    """生成临时文件名(基于文件内容哈希)"""
    try:
        # 计算文件内容的 MD5
        md5_hash = hashlib.md5()
        with open(file_path, 'rb') as f:
            # 分块读取大文件
            for chunk in iter(lambda: f.read(4096), b''):
                md5_hash.update(chunk)
        
        # 使用哈希值的前16个字符作为临时名,保留原扩展名
        file_ext = os.path.splitext(file_path)[1].lower()
        temp_name = f"_temp_{md5_hash.hexdigest()[:16]}{file_ext}"
        return temp_name
    except Exception as e:
        print(f"    计算哈希失败 {os.path.basename(file_path)}: {e}")
        # 如果计算失败,使用时间戳作为临时名
        file_ext = os.path.splitext(file_path)[1].lower()
        return f"_temp_{int(time.time() * 1000000)}_{os.path.basename(file_path)}"


def first_pass_temp_rename(folder_path, dry_run=False):
    """
    第一遍:将所有文件重命名为临时哈希名
    返回: (原文件路径列表, 临时文件路径列表)
    """
    all_files = get_all_files_in_folder(folder_path)
    
    if not all_files:
        return [], []
    
    # 按自然排序
    all_files.sort(key=natural_sort_key)
    
    print(f"    第一遍:生成临时文件名...")
    
    rename_tasks = []
    temp_files = []
    
    for file_path in all_files:
        old_name = os.path.basename(file_path)
        temp_name = generate_temp_name(file_path)
        temp_path = os.path.join(folder_path, temp_name)
        
        # 如果已经是临时文件格式,跳过
        if old_name.startswith('_temp_'):
            print(f"      跳过: {old_name} (已是临时文件)")
            temp_files.append(file_path)  # 直接使用原路径
            continue
        
        rename_tasks.append((file_path, temp_path, old_name, temp_name))
        temp_files.append(temp_path)
    
    # 执行第一遍重命名
    if rename_tasks:
        if dry_run:
            print(f"      [试运行] 将重命名 {len(rename_tasks)} 个文件为临时名")
            for old_path, temp_path, old_name, temp_name in rename_tasks[:5]:
                print(f"        {old_name} -> {temp_name}")
            if len(rename_tasks) > 5:
                print(f"        ... 还有 {len(rename_tasks) - 5} 个文件")
        else:
            success_count = 0
            for old_path, temp_path, old_name, temp_name in rename_tasks:
                try:
                    # 如果临时文件已存在,添加随机后缀
                    if os.path.exists(temp_path):
                        name, ext = os.path.splitext(temp_name)
                        temp_name = f"{name}_{int(time.time() * 1000000)}{ext}"
                        temp_path = os.path.join(folder_path, temp_name)
                    
                    os.rename(old_path, temp_path)
                    print(f"        ✓ {old_name} -> {temp_name}")
                    success_count += 1
                except Exception as e:
                    print(f"        ✗ 重命名失败: {old_name}, 错误: {e}")
            
            print(f"      成功重命名 {success_count}/{len(rename_tasks)} 个文件为临时名")
    
    # 返回临时文件列表(实际存在的文件)
    actual_temp_files = []
    for temp_file in temp_files:
        if os.path.exists(temp_file):
            actual_temp_files.append(temp_file)
    
    return all_files, actual_temp_files


def second_pass_final_rename(folder_path, temp_files, start_num, dry_run=False):
    """
    第二遍:将临时文件按顺序重命名为最终的文件名
    返回: 成功重命名的文件数量
    """
    if not temp_files:
        return 0
    
    # 排序临时文件(确保重命名顺序一致)
    temp_files.sort(key=natural_sort_key)
    
    print(f"    第二遍:重命名为最终文件名...")
    
    rename_tasks = []
    
    for idx, temp_path in enumerate(temp_files):
        file_ext = os.path.splitext(temp_path)[1].lower()
        old_name = os.path.basename(temp_path)
        
        # 生成最终文件名
        final_name = f"{start_num + idx}{file_ext}"
        final_path = os.path.join(folder_path, final_name)
        
        # 如果已经是目标格式,跳过
        if old_name == final_name:
            print(f"      跳过: {old_name} (已是正确格式)")
            continue
        
        rename_tasks.append((temp_path, final_path, old_name, final_name))
    
    # 执行第二遍重命名
    if rename_tasks:
        if dry_run:
            print(f"      [试运行] 将重命名 {len(rename_tasks)} 个文件为最终名")
            for temp_path, final_path, old_name, final_name in rename_tasks[:5]:
                print(f"        {old_name} -> {final_name}")
            if len(rename_tasks) > 5:
                print(f"        ... 还有 {len(rename_tasks) - 5} 个文件")
        else:
            success_count = 0
            for temp_path, final_path, old_name, final_name in rename_tasks:
                try:
                    # 如果最终文件已存在,添加后缀
                    if os.path.exists(final_path) and final_path != temp_path:
                        name, ext = os.path.splitext(final_name)
                        counter = 1
                        while True:
                            alt_name = f"{name}_{counter}{ext}"
                            alt_path = os.path.join(folder_path, alt_name)
                            if not os.path.exists(alt_path):
                                final_name = alt_name
                                final_path = alt_path
                                break
                            counter += 1
                    
                    os.rename(temp_path, final_path)
                    print(f"        ✓ {old_name} -> {final_name}")
                    success_count += 1
                except Exception as e:
                    print(f"        ✗ 重命名失败: {old_name}, 错误: {e}")
            
            print(f"      成功重命名 {success_count}/{len(rename_tasks)} 个文件为最终名")
            return success_count
    
    return len(rename_tasks)


def rename_files_in_folder_two_pass(folder_path, start_num, dry_run=False):
    """
    使用二次重命名方法处理文件夹中的文件
    
    参数:
    folder_path: 文件夹路径
    start_num: 起始编号
    dry_run: 是否试运行模式
    
    返回:
    处理了多少个文件
    """
    print(f"\n  处理文件夹: {os.path.basename(folder_path)}")
    print(f"  起始编号: {start_num}")
    
    # 第一遍:重命名为临时哈希名
    original_files, temp_files = first_pass_temp_rename(folder_path, dry_run)
    
    if not temp_files:
        print(f"  没有文件需要处理")
        return 0
    
    print(f"  临时文件数量: {len(temp_files)}")
    
    # 第二遍:重命名为最终顺序名
    success_count = second_pass_final_rename(folder_path, temp_files, start_num, dry_run)
    
    if success_count > 0:
        print(f"  占用编号: {start_num} 到 {start_num + success_count - 1}")
    
    return len(temp_files)


def rename_folders_sequentially(base_directory, start_num=0, dry_run=False, 
                                skip_folders=None, only_folders=None):
    """
    按顺序重命名每个文件夹中的文件,编号连续递增

    参数:
    base_directory: 包含子文件夹的根目录
    start_num: 起始编号
    dry_run: 是否试运行模式
    skip_folders: 要跳过的文件夹名称列表
    only_folders: 只处理指定的文件夹名称列表
    """
    print("=" * 60)
    print("文件批量重命名工具 - 二次重命名模式")
    print("=" * 60)
    print(f"根目录: {base_directory}")
    print(f"起始编号: {start_num}")
    print(f"模式: {'试运行(不实际修改)' if dry_run else '实际重命名'}")
    print("=" * 60)

    # 获取所有子文件夹
    if not os.path.exists(base_directory):
        print(f"错误:路径 '{base_directory}' 不存在")
        return

    # 获取所有子文件夹(不包括文件)
    folders = []
    for item in os.listdir(base_directory):
        item_path = os.path.join(base_directory, item)
        if os.path.isdir(item_path):
            # 应用过滤条件
            if skip_folders and item in skip_folders:
                print(f"跳过文件夹(配置跳过): {item}")
                continue
            if only_folders and item not in only_folders:
                continue
            folders.append(item_path)

    # 按文件夹名称排序
    folders.sort()

    if not folders:
        print(f"警告:在 '{base_directory}' 中没有找到符合条件的子文件夹")
        return

    print(f"\n找到 {len(folders)} 个子文件夹:")
    for f in folders:
        print(f"  - {os.path.basename(f)}")

    # 处理每个文件夹
    current_num = start_num
    total_processed = 0
    folder_stats = []

    for idx, folder_path in enumerate(folders):
        folder_name = os.path.basename(folder_path)
        print(f"\n{'=' * 50}")
        print(f"处理第 {idx + 1}/{len(folders)} 个文件夹: {folder_name}")
        print(f"{'=' * 50}")

        # 使用二次重命名方法处理当前文件夹
        files_count = rename_files_in_folder_two_pass(folder_path, current_num, dry_run)

        if files_count > 0:
            folder_stats.append({
                'folder': folder_name,
                'start': current_num,
                'end': current_num + files_count - 1,
                'count': files_count
            })
            current_num += files_count
            total_processed += files_count
        else:
            print(f"  无文件处理")

    # 输出总结
    print("\n" + "=" * 60)
    print("处理完成!")
    print("=" * 60)
    print(f"处理的文件夹数: {len(folders)}")
    print(f"处理的文件总数: {total_processed}")
    print(f"使用的编号范围: {start_num} 到 {current_num - 1}")
    print(f"下一个可用编号: {current_num}")
    
    # 显示每个文件夹的编号分配
    if folder_stats:
        print("\n编号分配详情:")
        for stat in folder_stats:
            print(f"  {stat['folder']}: {stat['start']:04d}-{stat['end']:04d} ({stat['count']} 个文件)")

    if dry_run:
        print("\n⚠️  这是试运行模式,没有实际修改任何文件")
        print("   如需实际重命名,请设置 dry_run=False")


def main():
    """主函数"""
    # ==================== 配置区域 ====================
    # 根目录路径(包含所有需要处理的子文件夹)
    BASE_DIRECTORY = Path("D:/PDF_FILE/classified_pdfs")
    
    # 起始编号
    START_NUM = 0
    
    # 是否试运行模式
    # True: 只显示将要进行的操作,不实际重命名
    # False: 实际执行重命名操作
    DRY_RUN = True  # 建议先设为True查看效果,确认无误后改为False
    
    # 可选:指定要跳过的文件夹
    SKIP_FOLDERS = None  # 例如: ["temp", "backup"]
    
    # 可选:只处理指定的文件夹
    ONLY_FOLDERS = None  # 例如: ["folder1", "folder2"]
    # =================================================
    
    # 转换路径
    folder_path = str(BASE_DIRECTORY).strip('"').strip("'")
    
    print("=" * 60)
    print("二次重命名批量处理工具")
    print("=" * 60)
    print(f"根目录: {folder_path}")
    print(f"起始编号: {START_NUM}")
    print(f"模式: {'试运行' if DRY_RUN else '实际重命名'}")
    print("=" * 60)
    
    # 如果需要交互式确认
    if not DRY_RUN:
        print("\n⚠️  警告:即将实际重命名文件!")
        print("   流程:第一遍重命名为临时哈希名 → 第二遍重命名为顺序编号")
        confirm = input("确认继续?(y/n): ").strip().lower()
        if confirm != 'y':
            print("操作已取消")
            return
    
    # 执行重命名
    rename_folders_sequentially(folder_path, START_NUM, DRY_RUN, 
                                skip_folders=SKIP_FOLDERS, 
                                only_folders=ONLY_FOLDERS)
    
    # 如果不是试运行模式,给出提示
    if not DRY_RUN:
        print("\n✓ 重命名操作已完成")


if __name__ == "__main__":
    main()

主要改进

二次重命名流程:

  1. 第一遍:临时哈希名

    • 计算每个文件的 MD5 哈希值
    • 重命名为 _temp_{哈希值前16位}.扩展名
    • 避免文件名冲突(如果临时文件已存在,添加时间戳)
  2. 第二遍:最终顺序名

    • 对所有临时文件进行自然排序
    • 按顺序重命名为 0.pdf, 1.pdf, 2.pdf...
    • 如果目标文件已存在,自动添加后缀避免冲突

优点:

  1. 避免冲突:临时名基于哈希值,几乎不会重复
  2. 安全可靠:即使中途失败,临时文件也很好识别
  3. 支持断点续传:重新运行时会跳过已经是临时文件格式的文件
  4. 自然排序:正确排序包含数字的文件名(file1, file2, file10)

使用建议:

  1. 先设置 DRY_RUN = True 预览效果
  2. 确认无误后设置 DRY_RUN = False 实际执行
  3. 如果某个文件夹处理失败,重新运行即可(会跳过已处理的临时文件)

这样就能确保所有文件都能成功重命名,不会出现冲突或遗漏的问题。

相关推荐
加号31 小时前
【C#】 通过 Python.NET 调用 Python pyd 扩展模块:多类交互与参数传递实践指南
python·c#·.net
财经资讯数据_灵砚智能1 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年5月12日
人工智能·python·信息可视化·自然语言处理·ai编程
Hesionberger1 小时前
LeetCode98:验证二叉搜索树(多解)
java·开发语言·python·算法·leetcode·职场和发展
千寻girling1 小时前
周日那天参加的力扣周赛... —— 10号
java·javascript·c++·python·算法·leetcode·职场和发展
TechWayfarer1 小时前
订单未到、运力先行:IP精确地理位置在物流调度中的实战应用
服务器·网络·python·tcp/ip·交通物流
故事还在继续吗1 小时前
嵌入式 C 语言程序性能优化
c语言·开发语言·性能优化
逻辑驱动的ken1 小时前
Java高频面试考点场景题28
java·开发语言·面试·职场和发展·求职招聘
fly_over1 小时前
AI Agent 开发实战教程(二):Prompt 工程与工具调用
开发语言·python·langchain·prompt·ai编程·ai agent
csbysj20201 小时前
并查集基础
开发语言