Python os模块 设计文件夹自动备份、同步工具

背景
我们经常使用U盘来储存和备份文件。但是备份一个文件夹到U盘的时候, 如果文件夹之前已经放到U盘, 那么怎么办?
多数读者会选择替换U盘中原有的文件。但是:

首先, 这种方式速度慢。如果文件夹中有几十上百个文件, 全部复制到U盘, 还不如只复制最近修改的几个文件。
其次, 这种方法不能自动处理文件夹的变动。如新编辑的文件, 删除的文件, 以及重命名的文件等, 可能会导致重复文件等问题。当然, 一些读者会先删除U盘内原有的文件夹, 再复制一遍, 但速度仍较低。
另外, 在其他电脑修改了U盘的文件后, 如果继续替换, 修改会消失。

0.基础知识

  1. os.path.join(path,path2,path3,...)
    合并多个路径。与代码path+path2+path3+...的区别是不需考虑pathpath2的首尾是否带反斜杠\。所以, 建议拼接目录时os.path.join(), 不用+号。
  2. os.walk(path)
    遍历一个路径下的所有文件, 配合for循环使用。每次for迭代生成一个元组, 包含根目录名、子目录名、文件名。
  3. os.path.split(path)
    分割目录的最后一项和前面的项, 返回一个列表。如os.path.split("c:\\users\\admin")返回["c:\\users", "admin"]
  4. shutil.copy(src,dst)shutil.copy2(src,dst)
    src处的文件复制到dst
    区别: copy()只复制文件内容, 而copy2()会复制源文件的属性、修改日期、权限等信息到dst
  5. os.stat(file)
    获取文件的状态信息, 如大小、日期。返回一个os.stat_result对象, 例如os.stat("e:\\").st_mtime即可获得"e:\"的修改日期。

1.程序原理


假设有两个目录, 目录1是较新的, 就需要把目录1的修改应用到目录2中, 最终使目录1与目录2完全一致。
1.找出源目录(目录1)中新编辑的文件, 复制至目标目录(目录2)下。
2.找出目标目录中新编辑的文件, 并询问是否复制至源目录下。
3.删除目标目录中存在, 而源目录相同位置不存在的文件。

2.初次实现

初次实现的程序使用源目录更新目标目录。

import sys,os,shutil
def direc(path,dirs=True,files=True):
    #迭代器, 基于os.walk(), 列出path下的所有子目录和文件名。
    for root,_dirs,_files in os.walk(os.path.realpath(path)):
        if dirs:
            for name in _dirs:
                yield os.path.join(root, name)
        if files:
            for name in _files:
                yield os.path.join(root, name)
def copy2(src,dst):
    # 重写shutil.copy2()函数, 目标文件所在文件夹不存在时, 直接创建
    if not os.path.isdir(os.path.split(dst)[0]):
        os.makedirs(os.path.split(dst)[0],exist_ok=True)
    shutil.copy2(src,dst)
def normpath(path):# 重写os.path.normpath()函数
    path=os.path.normpath(path).strip('"')
    if path.endswith(':'):
        path += '\\'
    return path
src = normpath(input('输入源目录: '))
dst = normpath(input('输入目标目录: '))
for file in direc(src,dirs=False):
    dst_file = os.path.join(dst, file.replace(src,'')) # dst 拼接 源文件file去掉src的部分
    if os.path.isfile(dst_file):
        # 用源目录中新的文件替换旧的文件
        if os.stat(file).st_mtime > os.stat(dst_file).st_mtime:
            print('已复制:',file,dst_file)
            copy2(file,dst_file)
        elif os.stat(file).st_mtime < os.stat(dst_file).st_mtime:
            # 目标目录中文件较新时
            ans=input('是否复制 %s 到 %s ? (Y/N)' % (dst_file,file))
            if ans.lower().startswith('y'):
                copy2(dst_file,file)
    else: # 目标目录中旧文件不存在时
        print('已复制:',file,dst_file)
        copy2(file,dst_file)

# 删除目标目录中存在, 而源目录相同位置不存在的文件
for file in direc(dst,dirs=False):
    if not os.path.isfile(
        os.path.join(src, file.replace(dst,'').lstrip('\\'))):
        ans=input('删除 %s ? (Y/N)' % (file))
        if ans.lower().startswith('y'):
            os.remove(file)
            print('已删除 '+file)

os.system('pause') # 等待用户退出程序

3.再次实现

上述程序能很好地实现更新目录功能。但是, 程序不支持排除某些特定的文件。需要将需排除的文件列表放入一个文件中, 待程序读取。

  • fnmatch.fnmatch(文件名, 要匹配的通配符模式)
    检测文件名与模式是否匹配, 返回True或False。符号*表示匹配多个字符, ?表示匹配单个字符。如fnmatch.fnmatch("E:\\python.pyw","E:\\*.p?w")返回True。
import sys,os,shutil,fnmatch,traceback

def read_ig(ignore_listfile): # 读取排除文件列表
    l=[]
    with open(ignore_listfile,encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line[0] not in ('$','#'): # 忽略注释
                l.append(line)
    return l

def check_ig(file, ignore_list): # 判断文件是否应被排除
    for ig in ignore_list:
        if fnmatch.fnmatch(file,ig):
            return True
    return False

def normpath(path):
    # --snip-- 代码同初次实现部分
def copy2(src,dst):
    # --snip--
def direc(path,dirs=True,files=True):
    # --snip--
def main():
    if len(sys.argv) >= 3: # 解析程序命令行参数 sys.argv
        src,dst = sys.argv[1:3]
        ignore_listfile = sys.argv[3] if len(sys.argv) >= 4 else None
    else:
        print('用法:%s <源目录> <目标目录>' % sys.argv[0])
        src = normpath(input('输入源目录: ')).strip()
        dst = normpath(input('输入目标目录: ')).strip()
        default = '.gitignore' # 仅支持通配符格式
        if not os.path.isfile(default):default=None
        ignore_listfile = input('排除文件的列表 (默认 %s): '%default) or default

    if ignore_listfile is not None: # 如果有排除列表文件
        ignore_list = read_ig(normpath(ignore_listfile).strip('"').strip())
    else:ignore_list = []

    all_=False;ignore_all=False
    for file in direc(src,dirs=False):
        if check_ig(file, ignore_list):continue

        dst_file = os.path.join(dst + file[len(src):]) # 相当于file.replace(src,'')
        if os.path.isfile(dst_file):
            # 用源目录中新的文件替换旧的文件
            if os.stat(file).st_mtime > os.stat(dst_file).st_mtime:
                print('已复制:',file,dst_file)
                copy2(file,dst_file)
            elif os.stat(file).st_mtime < os.stat(dst_file).st_mtime:
                # 目标目录中文件较新时

                if all_:
                    copy2(dst_file,file)
                elif not ignore_all:
                    ans=input('是否复制 %s 到 %s ? [Y/N/A(All)/I(Ignore all)]'\
                              % (dst_file,file))
                    if ans.lower().startswith('y'):
                        copy2(dst_file,file)
                    elif ans.lower() in ('a','all'):
                        all_=True;copy2(dst_file,file)
                    elif ans.lower() in ('i','ignore all'):
                        ignore_all=True
                else:
                    print('忽略 %s'%dst_file)
        else:
            # 目标目录中旧文件不存在时
            print('已复制:',file,dst_file)
            copy2(file,dst_file)

    # 删除目标目录中存在, 而源目录相同位置不存在的文件
    all_=False;ignore_all=False
    for file in direc(dst,dirs=False):
        if check_ig(file, ignore_list):continue

        if not os.path.isfile(
            os.path.join(src, file[len(dst):].lstrip('\\'))):
            if all_:
                print('已删除 '+file)
                os.remove(file)
            elif not ignore_all:
                ans=input('删除 %s ? [Y/N/A(All)/I(Ignore all)]' % (file))
                if ans.lower().startswith('y'):
                    os.remove(file)
                elif ans.lower() in ('a','all'):
                    all_=True;os.remove(file)
                elif ans.lower() in ('i','ignore all'):
                    ignore_all=True
            else:
                print('忽略 %s'%file)

    # 删除目标目录中存在, 而源目录不存在的空目录
    for dir_ in direc(dst,files=False):
        if check_ig(dir_, ignore_list):continue

        if not os.listdir(dir_)\
    and not os.path.isdir(os.path.join(src, dir_[len(dst):].lstrip('\\'))):
            os.removedirs(dir_)
            print('已删除空目录 %s'%dir_)

if __name__=="__main__":
    try:main()
    except Exception:
        traceback.print_exc() # 显示错误消息
    if not 'pythonw' in os.path.split(sys.executable)[1]: #在pythonw.exe(如IDLE)中运行时不暂停
        os.system('pause')

4.实现自动备份与同步

实现自动备份同步的代码省去了获取input()的部分, 通过修改sys.stderr为其他打开的文件记录日志和其他错误消息。
新建一个文本文件, 保存为.pyw扩展名, 目的是避免.py文件运行时显示黑色的控制台窗口。在其中加入以下程序:

import sys,os,shutil,fnmatch,traceback
import time
def direc(path,dirs=True,files=True):
    # --snip-- 代码同上
def read_ig(ignore_listfile):
    # --snip--
def check_ig(file, ignore_list):
    # --snip--
def normpath(path):
    # --snip--
def copy2(src,dst):
    # --snip--
def main(src,dst,ignore_listfile=None,flag_replace_src=True,flag_delete=True):
    if ignore_listfile:
        ignore_list = read_ig(normpath(ignore_listfile).strip('"').strip())
    else:ignore_list = []

    all_=False;ignore_all=False
    for file in direc(src,dirs=False):
        if check_ig(file, ignore_list):continue

        dst_file = os.path.join(dst + file[len(src):]) # 相当于file.replace(src,'')
        if os.path.isfile(dst_file):
            # 用源目录中新的文件替换旧的文件
            if os.stat(file).st_mtime > os.stat(dst_file).st_mtime:
                print('已复制:',file,dst_file, file=sys.stderr)
                copy2(file,dst_file)
            elif os.stat(file).st_mtime < os.stat(dst_file).st_mtime:
                # 目标目录中文件较新时

                if flag_replace_src:
                    copy2(dst_file,file)
        else:
            # 目标目录中旧文件不存在时
            print('已复制:',file,dst_file, file=sys.stderr)
            copy2(file,dst_file)

    # 删除目标目录中存在, 而源目录相同位置不存在的文件
    all_=False;ignore_all=False
    if flag_delete:
        for file in direc(dst,dirs=False):
            if check_ig(file, ignore_list):continue

            if not os.path.isfile(
                os.path.join(src, file[len(dst):].lstrip('\\'))):
            
                print('已删除 '+file, file=sys.stderr)
                os.remove(file)

        # 删除目标目录中存在, 而源目录不存在的空目录
        for dir_ in direc(dst,files=False):
            if check_ig(dir_, ignore_list):continue

            if not os.listdir(dir_)\
        and not os.path.isdir(os.path.join(src, dir_[len(dst):].lstrip('\\'))):
                os.removedirs(dir_)
                print('已删除空目录 %s'%dir_, file=sys.stderr)

if __name__=="__main__": # 判断是否作为主程序运行
    src = "E:\\python"
    dst = "F:\\python-备份" # 可以是外部存储目录(如U盘)
    ig = None # 排除列表文件
    flag_replace_src=True
    flag_delete=True
    interval = 300 # 秒

    sys.stderr = open("E:\\PyBackup.log","w",encoding="utf-8") # 不设为utf=8可能无法编码汉字
    while True:
        try:
            if os.path.isdir(dst): # 如果目标目录存在(即已插上U盘)
                print("==在 %s 开始备份==" % time.asctime(),file=sys.stderr)
                main(src,dst,ig,flag_replace_src,flag_delete)
                print("==备份完成 %s==\n" % time.asctime(),file=sys.stderr)
        except Exception:
            traceback.print_exc() # 将错误消息写入sys.stderr(即日志)
        sys.stderr.flush()
        time.sleep(interval)

将上述.pyw文件复制到C:\Users\<你的用户名>\AppData\Roaming\Microsoft\Windows\开始菜单\程序\启动这个目录, 就可以在开机时自动启动备份程序。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qfcy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值