文章目录
使用二次重命名方法可以避免文件名冲突。第一遍先给每个文件分配临时哈希名,第二遍再按顺序重命名。让我重写代码:
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()
主要改进
二次重命名流程:
-
第一遍:临时哈希名
- 计算每个文件的 MD5 哈希值
- 重命名为
_temp_{哈希值前16位}.扩展名 - 避免文件名冲突(如果临时文件已存在,添加时间戳)
-
第二遍:最终顺序名
- 对所有临时文件进行自然排序
- 按顺序重命名为
0.pdf, 1.pdf, 2.pdf... - 如果目标文件已存在,自动添加后缀避免冲突
优点:
- 避免冲突:临时名基于哈希值,几乎不会重复
- 安全可靠:即使中途失败,临时文件也很好识别
- 支持断点续传:重新运行时会跳过已经是临时文件格式的文件
- 自然排序:正确排序包含数字的文件名(file1, file2, file10)
使用建议:
- 先设置
DRY_RUN = True预览效果 - 确认无误后设置
DRY_RUN = False实际执行 - 如果某个文件夹处理失败,重新运行即可(会跳过已处理的临时文件)
这样就能确保所有文件都能成功重命名,不会出现冲突或遗漏的问题。