You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题分析与修复方案

核心问题点

  1. 文件未释放锁:读取EXIF的with open块包含了后续的移动操作,文件一直处于打开状态,导致shutil.move无法修改/删除原文件,触发PermissionError。
  2. 异常处理语法错误:一处except块引用了未定义的变量e,引发UnboundLocalError。
  3. 目录创建逻辑缺陷:os.mkdir无法自动创建多级目录,若目标路径的父目录不存在会报错;递归时的目录判断逻辑错误,会中断正常的子目录遍历。
  4. 冗余代码:重复调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 01:55:16