每周一个python 小工具--定时批量挪文件

27 阅读5分钟

Function list:

  • 📁 支持文件和目录的移动/复制
  • ⏰ 支持 cron 定时任务
  • 📝 基于 JSON 配置文件
  • 🔄 支持覆盖已存在的文件
  • 📋 支持批量操作

requirements.txt

APScheduler==3.11.1

config.json

  • filename: 文件名(可选,如果 source_path 是目录则必需)
  • source_path: 源文件/目录路径
  • destination_path: 目标路径
  • overwrite: 是否覆盖已存在的文件(0=否,1=是)
  • copy_or_remove: 操作模式("copy"=复制保留源文件,"remove"=移动删除源文件)
  • cron: 定时任务表达式(可选,格式:分 时 日 月 周秒 分 时 日 月 周
{
  "files_to_move": [
    {
      "filename": "test.txt",
      "source_path": "C:/Users/87104/Desktop/code/move-file/test file",
      "destination_path": "C:/Users/87104/Desktop/code/move-file/test file/archive",
      "overwrite": 0,
      "copy_or_remove": "copy",
      "cron": "11 * * * *"
    },
    {
      "filename": "test1.txt",
      "source_path": "C:/Users/87104/Desktop/code/move-file/test file",
      "destination_path": "C:/Users/87104/Desktop/code/move-file/test file/archive",
      "overwrite": 1,
      "copy_or_remove": "remove",
        "cron": "11 * * * *"
    }
  ]
}

main.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
文件移动工具
从配置文件读取文件列表并执行移动操作
"""

import shutil
import sys
import json
import time
from pathlib import Path
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger

# 配置文件路径
CONFIG_FILE = 'config.json'


def load_config():
    """
    加载配置文件
    
    Returns:
        dict: 配置字典,包含 files_to_move 列表
    """
    default_config = {"files_to_move": []}
    
    # 检查配置文件是否存在
    if not Path(CONFIG_FILE).exists():
        return default_config
    
    try:
        with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
            config = json.load(f)
            # 确保 files_to_move 键存在
            if "files_to_move" not in config:
                config["files_to_move"] = []
            return config
    except (json.JSONDecodeError, IOError):
        # 配置文件格式错误或读取失败,返回空配置
        return default_config


def execute_single_file(file_info):
    """
    执行单个文件的移动或复制操作
    
    Args:
        file_info (dict): 文件配置信息
    """
    filename = file_info.get("filename", "")
    source = file_info.get("source_path", "")
    destination = file_info.get("destination_path", "")
    
    # 检查配置信息是否完整
    if not source or not destination:
        print(f"跳过: 配置信息不完整 - {filename}")
        return False
    
    # 从文件配置中读取 overwrite 字段
    file_overwrite = bool(file_info.get("overwrite", 0))
    # 从文件配置中读取 copy_or_remove 字段,默认为 "remove"(删除源文件,即移动)
    copy_or_remove = file_info.get("copy_or_remove", "remove")
    is_copy = copy_or_remove.lower() == "copy"
    
    source_path = Path(source)
    
    # 如果源路径是目录且提供了文件名,则移动目录内的指定文件
    if source_path.exists() and source_path.is_dir() and filename:
        actual_source = source_path / filename
        # 检查文件是否存在
        if not actual_source.exists():
            print(f"跳过: 文件不存在 - {filename}")
            return False
        source = str(actual_source)
        display_name = filename
    else:
        # 使用文件名或路径名作为显示名称
        display_name = filename if filename else source_path.name
    
    operation = "复制" if is_copy else "移动"
    print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 正在{operation}: {display_name}")
    print(f"  源路径: {source}")
    print(f"  目标路径: {destination}")
    print(f"  覆盖模式: {'是' if file_overwrite else '否'}")
    print(f"  操作模式: {copy_or_remove}")
    
    # 执行移动或复制操作
    success = move_file(source, destination, file_overwrite, is_copy)
    print()
    return success


def execute_from_config():
    """
    从配置文件读取待移动文件列表并执行移动
    
    Returns:
        bool: 是否所有文件都移动成功
    """
    config = load_config()
    files_to_move = config.get("files_to_move", [])
    
    if not files_to_move:
        print("配置文件中没有待移动的文件")
        return False
    
    print(f"从配置文件中读取到 {len(files_to_move)} 个待移动文件\n")
    
    success_count = sum(1 for file_info in files_to_move if execute_single_file(file_info))
    fail_count = len(files_to_move) - success_count
    
    print(f"\n移动完成: 成功 {success_count} 个, 失败 {fail_count} 个")
    return fail_count == 0


def move_file(source, destination, overwrite=False, is_copy=False):
    """
    移动或复制文件或目录
    
    Args:
        source (str): 源文件/目录路径
        destination (str): 目标路径
        overwrite (bool): 是否覆盖已存在的文件
        is_copy (bool): 是否复制。True=复制(保留源文件),False=移动(删除源文件)
    
    Returns:
        bool: 操作是否成功
    """
    try:
        source_path = Path(source).resolve()
        dest_path = Path(destination).resolve()
        
        if not source_path.exists():
            print(f"错误: 源路径不存在: {source}")
            return False
        
        if dest_path.exists() and dest_path.is_dir():
            dest_path = dest_path / source_path.name
        
        if dest_path.exists():
            if not overwrite:
                print(f"错误: 目标路径已存在: {dest_path}")
                print("提示: 在配置文件中设置 overwrite 为 1 可以覆盖已存在的文件")
                return False
            if dest_path.is_dir():
                shutil.rmtree(dest_path)
            else:
                dest_path.unlink()
        
        if is_copy:
            if source_path.is_dir():
                shutil.copytree(str(source_path), str(dest_path))
            else:
                shutil.copy2(str(source_path), str(dest_path))
            print(f"成功: 已将 '{source_path}' 复制到 '{dest_path}'")
        else:
            shutil.move(str(source_path), str(dest_path))
            print(f"成功: 已将 '{source_path}' 移动到 '{dest_path}'")
        return True
        
    except PermissionError:
        print(f"错误: 没有权限访问文件: {source}")
        return False
    except Exception as e:
        operation = "复制" if is_copy else "移动"
        print(f"错误: {operation}文件时发生异常: {e}")
        return False


def setup_scheduler(files_to_move):
    """
    设置定时任务调度器
    
    Args:
        files_to_move (list): 文件配置列表
    
    Returns:
        BlockingScheduler: 调度器对象,如果没有有效任务则返回 None
    """
    scheduler = BlockingScheduler(timezone='Asia/Shanghai')
    scheduled_count = 0
    
    for i, file_info in enumerate(files_to_move, 1):
        cron = file_info.get("cron", "")
        if not cron:
            continue
        
        try:
            cron_parts = cron.strip().split()
            if len(cron_parts) == 5:
                minute, hour, day, month, day_of_week = cron_parts
                trigger = CronTrigger(minute=minute, hour=hour, day=day, month=month, day_of_week=day_of_week, timezone='Asia/Shanghai')
            elif len(cron_parts) == 6:
                second, minute, hour, day, month, day_of_week = cron_parts
                trigger = CronTrigger(second=second, minute=minute, hour=hour, day=day, month=month, day_of_week=day_of_week, timezone='Asia/Shanghai')
            else:
                print(f"警告: 文件 {i} 的 cron 格式不正确,跳过: {cron}")
                continue
            
            filename = file_info.get("filename", f"文件{i}")
            job = scheduler.add_job(execute_single_file, trigger=trigger, args=[file_info], id=f"file_{i}", name=f"移动文件: {filename}")
            scheduled_count += 1
            next_run = job.next_run_time.strftime('%Y-%m-%d %H:%M:%S') if job.next_run_time else "未知"
            print(f"已设置定时任务: {filename} - cron: {cron} - 下次执行: {next_run}")
        except Exception as e:
            print(f"警告: 文件 {i} 的 cron 配置错误,跳过: {e}")
            import traceback
            traceback.print_exc()
    
    if scheduled_count == 0:
        return None
    
    print(f"\n共设置了 {scheduled_count} 个定时任务")
    print(f"当前时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
    print("定时任务已启动,等待执行...\n")
    return scheduler


def main():
    """
    主函数
    从配置文件读取并执行文件移动操作
    支持定时任务模式
    """
    config = load_config()
    files_to_move = config.get("files_to_move", [])
    
    if not files_to_move:
        print("配置文件中没有待移动的文件")
        sys.exit(1)
    
    has_cron = any(file_info.get("cron", "") for file_info in files_to_move)
    
    if has_cron:
        print("启动定时任务模式...\n")
        scheduler = setup_scheduler(files_to_move)
        if scheduler:
            print("=" * 50)
            print("调度器已启动,程序将持续运行")
            print("按 Ctrl+C 可以停止定时任务")
            print("=" * 50 + "\n")
            try:
                scheduler.start()
            except KeyboardInterrupt:
                print("\n定时任务已停止")
                scheduler.shutdown()
        else:
            print("没有有效的定时任务,执行一次立即执行模式")
            execute_from_config()
    else:
        success = execute_from_config()
        sys.exit(0 if success else 1)


if __name__ == '__main__':
    main()