代码: 全选
import os
import shutil
import time
import logging
import sys
import io
# 强制设置控制台输出编码,防止控制台打印畸形字符时崩溃
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
# 【核心续传机制】:在脚本同级目录下生成进度记录文件
PROGRESS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "move_progress.log")
def load_progress():
"""启动时加载已处理的文件夹清单"""
if os.path.exists(PROGRESS_FILE):
try:
with open(PROGRESS_FILE, 'r', encoding='utf-8', errors='replace') as f:
return set(line.strip() for line in f if line.strip())
except Exception as e:
logging.error(f"❌ 加载进度文件失败,将重新开始: {e}")
return set()
return set()
def save_progress(processed_dirs):
"""
终极安全保存:原子写入 + 强制替换畸形字符,彻底解决 Windows 下的 surrogates 崩溃
"""
temp_file = PROGRESS_FILE + '.tmp'
try:
# 1. 先写入临时文件
with open(temp_file, 'w', encoding='utf-8') as f:
for d in processed_dirs:
# 【核心修复】:在写入前,强制将任何无法编码的孤立代理字符替换为 '?'
# 这不会影响实际的文件移动,但能保证进度文件绝对不报错
safe_d = d.encode('utf-8', errors='replace').decode('utf-8')
f.write(safe_d + '\n')
f.flush()
os.fsync(f.fileno()) # 强制将数据写入磁盘
# 2. 写入成功后,用临时文件原子性地替换原文件
shutil.move(temp_file, PROGRESS_FILE)
except Exception as e:
logging.error(f"❌ 保存进度时发生错误: {e}")
if os.path.exists(temp_file):
try:
os.remove(temp_file)
except:
pass
def move_old_files_safely(source_dir, target_dir, years=2):
"""
终极断点续传版:彻底修复 Windows 网络路径下的畸形文件名和断网问题
"""
current_time = time.time()
threshold_time = current_time - (years * 365.25 * 24 * 60 * 60)
moved_count = 0
skipped_count = 0
folder_skipped_count = 0
error_count = 0
# 1. 加载上次的进度
processed_dirs = load_progress()
if processed_dirs:
logging.info(f"📂 检测到上次进度,已加载 {len(processed_dirs)} 个已处理目录,将自动跳过...")
if not os.path.exists(source_dir):
logging.error(f"❌ 无法访问源目录: {source_dir}")
return
logging.info(f"🚀 开始扫描 (支持本地进度续传)...")
for root, dirs, files in os.walk(source_dir):
# 【核心续传优化】:如果当前目录在已处理清单中,直接跳过
if root in processed_dirs:
folder_skipped_count += 1
continue
# 控制台打印时替换畸形字符,防止控制台崩溃
safe_root = root.encode('utf-8', 'replace').decode('utf-8')
print(f"🔍 正在扫描新目录: {safe_root}", end='\r')
dir_has_old_files = False
for file in files:
file_path = os.path.join(root, file)
relative_path = os.path.relpath(file_path, source_dir)
target_file_path = os.path.join(target_dir, relative_path)
max_retries = 3
for attempt in range(max_retries):
try:
if os.path.exists(target_file_path):
skipped_count += 1
break
target_sub_dir = os.path.dirname(target_file_path)
if not os.path.exists(target_sub_dir):
os.makedirs(target_sub_dir, exist_ok=True)
file_mtime = os.path.getmtime(file_path)
if file_mtime >= threshold_time:
break
shutil.copy2(file_path, target_file_path)
os.remove(file_path)
moved_count += 1
dir_has_old_files = True
safe_rel_path = relative_path.encode('utf-8', 'replace').decode('utf-8')
print(f"✅ 已移动: {safe_rel_path} ")
break
except OSError as e:
# 针对网络错误 (53, 59, 64) 进行重试
if hasattr(e, 'winerror') and e.winerror in (59, 53, 64) and attempt < max_retries - 1:
wait_time = (attempt + 1) * 10 # 网络错误等待时间加长
logging.warning(f"⚠️ 网络波动,{wait_time}秒后重试...")
time.sleep(wait_time)
continue
else:
safe_rel_path = relative_path.encode('utf-8', 'replace').decode('utf-8')
logging.error(f"❌ 移动失败: {safe_rel_path}, 错误: {e}")
error_count += 1
break
except Exception as e:
safe_rel_path = relative_path.encode('utf-8', 'replace').decode('utf-8')
logging.error(f"❌ 未知错误: {safe_rel_path}, 错误: {e}")
error_count += 1
break
# 【关键步骤】:当前文件夹处理完毕后,将其加入已处理清单并安全保存
processed_dirs.add(root)
save_progress(processed_dirs)
print("\n" + "="*50)
logging.info("🏁 任务结束!")
logging.info(f"成功移动: {moved_count} 个 | 单文件跳过: {skipped_count} 个 | 整个目录跳过: {folder_skipped_count} 个 | 失败: {error_count} 个")
if __name__ == "__main__":
source_directory = r"\path\to\your\source\folder"
target_directory = r"\path\to\your\target\folder"
print(f"正在处理: {source_directory}")
move_old_files_safely(source_directory, target_directory)