shutil.move仅复制文件不移动,PermissionError问题求助
照片分类脚本权限错误排查与修复
我写了个Python脚本,用来把照片从一个文件夹移动到另一个文件夹,按拍摄日期分类。但现在脚本只复制文件不删原文件,还报PermissionError。试过管理员运行VSC、给文件夹开完全控制权限、取消只读,换照片和文件夹都没用,我是Python新手,应该是脚本本身有问题,帮忙看看。
原脚本代码
import os import shutil import exifread import datetime import sys import time import datetime # 设置源目录路径 source_path = os.path.join('D:', 'Test new') dest_path = os.path.join('F:', 'Test new') photo_extensions = ['.jpg', 'bmp', '.png', '.raw', '.jpeg','.heic','.gif'] def read_source_directory_recursive(directory_path, num_calls=0, max_recursive_calls=1000): # 如果递归调用次数超过最大值,提前返回 if num_calls > max_recursive_calls: return # 读取目录文件列表 files = os.listdir(directory_path) # 初始化已处理照片计数 num_photos_processed = 0 # 遍历文件 for dir_name in files: file_name = dir_name # 判断是否为图片文件 if any(file_name.lower().endswith(ext) for ext in photo_extensions): # 获取照片绝对路径 photo_path = os.path.join(directory_path, file_name) # 以二进制模式打开图片文件 with open(photo_path, 'rb') as f: tags = exifread.process_file(f) # 获取拍摄日期 try: date_taken = tags["EXIF DateTimeOriginal"] date_taken = datetime.datetime.strptime(str(date_taken), '%Y:%m:%d %H:%M:%S') except KeyError: date_taken = datetime.datetime.fromtimestamp(os.path.getmtime(photo_path)) year = date_taken.year month = date_taken.month # 构建目标文件夹路径 dest_folder = os.path.join(dest_path, f"{year}-{month:02d}") # 不存在则创建目标文件夹 if not os.path.exists(dest_folder): os.mkdir(dest_folder) # 生成新文件名 new_file_name = os.path.basename(photo_path) if os.path.exists(os.path.join(dest_folder, os.path.basename(photo_path))): new_file_name = f"{os.path.basename(photo_path).split('.')[0]}_{int(time.time())}.{os.path.basename(photo_path).split('.')[-1]}" # 移动照片到目标文件夹 try: shutil.move(photo_path, os.path.join(dest_folder, new_file_name)) except: # 处理移动时的错误 print(f'An error occurred: {sys.exc_info()[0]}') # 输出移动日志 print(f"{photo_path} has been moved to {os.path.join(dest_folder, new_file_name)}") num_photos_processed += 1 # 判断是否为目录 if os.path.isdir(os.path.join(directory_path, dir_name)): # 获取目录绝对路径 current_directory_path = os.path.join(directory_path, dir_name) # 读取子目录 if current_directory_path == source_path: return try: num_photos_processed += read_source_directory_recursive(current_directory_path, num_calls + 1, max_recursive_calls) except: # 处理递归读取目录的错误 print(f'An error occurred: {sys.exc_info()[0]}') print(f"{num_photos_processed} photos were processed.") return num_photos_processed # 调用递归函数处理源目录文件,设置最大递归次数为2000 read_source_directory_recursive(source_path, 0, 2000)
错误回溯信息
Traceback (most recent call last): File "C:\Python311\Lib\shutil.py", line 825, in move os.rename(src, real_dst) PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'D:Test\\(1).JPG' -> 'D:Test 2\\2015-05\\(1).JPG' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\Users\Ed\Documents\Code\organise_photos.py", line 54, in read_source_directory_recursive shutil.move(photo_path, os.path.join(dest_folder, new_file_name)) File "C:\Python311\Lib\shutil.py", line 846, in move os.unlink(src) PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'D:Test\\(1).JPG' Traceback (most recent call last): File "C:\Users\Ed\Documents\Code\organise_photos.py", line 77, in <module> read_source_directory_recursive(source_path, 0, 1) File "C:\Users\Ed\Documents\Code\organise_photos.py", line 59, in read_source_directory_recursive print(f'An error occurred: {e}') ^ UnboundLocalError: cannot access local variable 'e' where it is not associated with a value
问题分析与修复方案
核心问题点
- 文件未释放锁:读取EXIF的
with open块包含了后续的移动操作,文件一直处于打开状态,导致shutil.move无法修改/删除原文件,触发PermissionError。 - 异常处理语法错误:一处
except块引用了未定义的变量e,引发UnboundLocalError。 - 目录创建逻辑缺陷:
os.mkdir无法自动创建多级目录,若目标路径的父目录不存在会报错;递归时的目录判断逻辑错误,会中断正常的子目录遍历。 - 冗余代码:重复调用
os.path.basename(photo_path),可优化减少重复计算。
修复后的代码
import os import shutil import exifread import datetime import sys import time # 设置源目录和目标目录 source_path = os.path.join('D:', 'Test new') dest_path = os.path.join('F:', 'Test new') photo_extensions = ['.jpg', '.bmp', '.png', '.raw', '.jpeg','.heic','.gif'] def read_source_directory_recursive(directory_path, num_calls=0, max_recursive_calls=1000): if num_calls > max_recursive_calls: return 0 num_photos_processed = 0 # 获取当前目录绝对路径,减少重复拼接 abs_dir = os.path.abspath(directory_path) for entry in os.scandir(abs_dir): if entry.is_file(): file_name = entry.name # 判断是否为图片文件 if any(file_name.lower().endswith(ext) for ext in photo_extensions): photo_path = entry.path date_taken = None # 读取EXIF信息,读取完成后立即关闭文件 try: with open(photo_path, 'rb') as f: tags = exifread.process_file(f) if "EXIF DateTimeOriginal" in tags: date_taken = datetime.datetime.strptime(str(tags["EXIF DateTimeOriginal"]), '%Y:%m:%d %H:%M:%S') except Exception as e: print(f"读取EXIF失败 {photo_path}: {str(e)}") # 若EXIF读取失败,用文件修改时间替代 if not date_taken: date_taken = datetime.datetime.fromtimestamp(os.path.getmtime(photo_path)) # 构建目标文件夹路径,自动创建多级目录 dest_folder = os.path.join(dest_path, f"{date_taken.year}-{date_taken.month:02d}") os.makedirs(dest_folder, exist_ok=True) # 处理重复文件名,用计数器避免时间戳冲突 base_name, ext = os.path.splitext(file_name) new_file_name = file_name counter = 1 while os.path.exists(os.path.join(dest_folder, new_file_name)): new_file_name = f"{base_name}_{counter}{ext}" counter += 1 # 移动文件,捕获具体异常信息 try: shutil.move(photo_path, os.path.join(dest_folder, new_file_name)) print(f"已移动: {photo_path} -> {os.path.join(dest_folder, new_file_name)}") num_photos_processed += 1 except Exception as e: print(f"移动失败 {photo_path}: {str(e)}") elif entry.is_dir(): # 递归处理子目录,用绝对路径比较避免中断遍历 if os.path.abspath(entry.path) != os.path.abspath(source_path): try: num_photos_processed += read_source_directory_recursive(entry.path, num_calls + 1, max_recursive_calls) except Exception as e: print(f"递归目录失败 {entry.path}: {str(e)}") print(f"当前目录处理完成,共处理 {num_photos_processed} 张照片") return num_photos_processed # 执行脚本 read_source_directory_recursive(source_path, 0, 2000)
关键修改说明
- 将EXIF读取和文件移动操作分离,确保文件读取后立即关闭,释放文件锁。
- 用
os.makedirs替代os.mkdir,支持自动创建多级目录,添加exist_ok=True避免重复创建报错。 - 优化重复文件名处理逻辑,用计数器替代时间戳,避免同一秒内多个文件重名冲突。
- 用
os.scandir替代os.listdir,更高效地获取文件/目录信息,减少路径拼接次数。 - 修复异常处理语法错误,捕获具体异常并输出详细信息,便于排查问题。
- 调整递归目录的判断逻辑,用绝对路径比较,避免中断正常的子目录遍历。
内容的提问来源于stack exchange,提问作者Boswell
相关产品推荐
相关产品推荐

