python3实现将本地文件夹全部内容上传到FTP

需求:

1、将本地文件夹的内容上传到 FTP 指定目录下,包括子目录及文件,要求支持中文文件名。

2、如果 FTP 上文件存在,对比大小不一致则覆盖上传,如果一致则不用上传。

3、要有日志功能方便查看

4、尽量以函数的方式实现

思路:

1、先上传一级目录,子目录使用递归的方式层层上传。FTP 上如果目录不存在则新建。

2、上传单个文件时进行文件大小对比。如果大小不一致则覆盖上传。

3、完成后统计上传文件的类型和数量,并打印上传结果。

本程序的改进:

此程序是我参考了网上多个实现方式,花了半个月时间改进过来的。对以前的功能进行了大幅完善。

1、解决了复制文件夹时目录顺序错乱的问题。
2、解决了不支持复制中文文件名的文件的问题。
3、优化了日志系统,将日志改为了中文,增加了显示上传结果,日志文件为单个文件,大小循环限制为 5 M。
4、大量的代码优化

代码如下:

# power by luo_tao 20211111
import os
from ftplib import FTP
import traceback
import logging
import sys
from logging.handlers import RotatingFileHandler
 
 
# 如需要支持中文名文件传输,需要将ftplib.py文件中的  encoding = "latin-1"  改为   encoding = "utf-8"
class FTP1(FTP):     # 继承FTP类,修改解码参数,解决不识别文件名中文的问题
    encoding = "utf-8"
 
 
# 比对本地文件和上传文件的大小
def is_same_size(ftp, local_file, remote_file, app_log):
    try:
        remote_file_size = ftp.size(remote_file)       # 获取远端文件大小
    except Exception as err:
        #app_log.debug("获取远程文件大小失败, 原因为:%s" % err)
        remote_file_size = -1     # 如果获取FTP文件失败,则返回-1
    try:
        local_file_size = os.path.getsize(local_file)     # 获取本地文件大小
    except Exception as err:
        app_log.debug("获取本地文件大小失败, 原因为:%s" % err)
        local_file_size = -1      # 如果获取本地文件失败,则返回-1
    # 三目运算符
    result = True if (remote_file_size == local_file_size) else False    # 文件大小对比
    return result, remote_file_size, local_file_size    # 返回对比结果,FTP文件和本地文件的大小
 
 
# 上传单个文件的函数,里面调用了比对本地文件和上传文件的大小的函数
def upload_file(ftp, local_file, remote_file, app_log):
    global upload_file_count
    global fail_count
    # 检查本地是否有此文件
    if not os.path.exists(local_file):     # 如果不存在本地文件,记录日志并返回False
        app_log.debug(f'上传文件:本地待上传的文件:{local_file}不存在。')
    result, remote_file_size, local_file_size = is_same_size(ftp, local_file, remote_file, app_log)  # FTP如果文件已存在,则对比大小
    if True != result:     # 如果对比大小不一致,则上传文件
        print(f'上传文件:远程文件 {remote_file} 不存在,现在开始上传...')
        app_log.debug(f'上传文件:远程文件 {remote_file} 不存在,现在开始上传...')
        global FTP_PERFECT_BUFF_SIZE    # 把全局变量传进来
        try:    # 上传文件到FTP
            with open(local_file, 'rb') as f:   # 打开本地文件
                # ftp.retrbinary('RETR %s' % remote_file, f.write, buffsize)    #下载FTP文件
                if ftp.storbinary('STOR ' + remote_file, f):   # 上传本地文件到FTP
                    result, remote_file_size, local_file_size = is_same_size(ftp, local_file, remote_file, app_log)
                # 打印上传失败或成功的日志
                app_log.debug(f'{remote_file}文件上传成功, 远程文件大小 = {remote_file_size}, 本地文件大小 = {local_file_size}')
                print(f'{remote_file}文件上传成功, 远程文件大小 = {remote_file_size}, 本地文件大小 = {local_file_size}')
                upload_file_count += 1
        except Exception as err:
            app_log.debug(f'上传文件有错误发生:{local_file}, 错误:{err}')
            print(f'上传文件有错误发生:{local_file}, 错误:{err}')
            fail_count += 1
            result = False
    else:
        print(f'{local_file}文件已存在,无需上传!')
        app_log.debug(f'{local_file}文件已存在,无需上传!')
 
 
# 上传目录的函数,里面有调用上传单个文件的函数
def upload_file_tree(local_path, remote_path, ftp, IsRecursively, app_log):
    global fail_count
    # 有远端目录的话进入目录,没有目录的话创建目录
    print(f'upload_file_tree函数开始运行!FTP远程目录为:{remote_path}')
    # 切换到FTP的目标目录,如果没有的话则创建
    try:
        ftp.cwd(remote_path)  # 进入FTP工作目录
    except Exception as e:
        print(f'FTP目录:{remote_path}文件夹不存在,错误信息:', e)
        app_log.debug(f'FTP目录:{remote_path}文件夹不存在,错误信息:{e}')
        base_dir, part_path = ftp.pwd(), remote_path.split('/')
        for subpath in part_path:
            # 针对类似  '/home/billing/scripts/zhf/send' 和 'home/billing/scripts/zhf/send' 两种格式的目录
            # 如果第一个分解后的元素是''这种空字符,说明根目录是从/开始,如果最后一个是''这种空字符,说明目录是以/结束
            # 例如 /home/billing/scripts/zhf/send/ 分解后得到 ['', 'home', 'billing', 'scripts', 'zhf', 'send', ''] 首位和尾都不是有效名称
            if '' == subpath:     # 如果是空字符,跳出此循环,执行下一个。
                continue
            base_dir = os.path.join(base_dir, subpath)  # base_dir + subpath  # 拼接子目录
            try:
                ftp.cwd(base_dir)  # 进入目录
            except Exception as e:
                print(f'创建FTP目录:{base_dir}')
                app_log.debug(f'创建FTP目录:{base_dir}')
                ftp.mkd(base_dir)  # 不存在创建当前子目录 直到创建所有
                continue
        ftp.cwd(remote_path)   # 进入FTP工作目录
    # 本地目录切换
    try:
        # 远端目录通过ftp对象已经切换到指定目录或创建的指定目录
        file_list = os.listdir(local_path)     # 列出本地文件夹第一层目录的所有文件和目录
        for file_name in file_list:
            if os.path.isdir(os.path.join(local_path, file_name)):   # 判断是文件还是目录,是目录为真
                if IsRecursively:  # 递归变量,默认为Ture
                    # 使用FTP进入远程目录,如果没有远程目录则创建它
                    try:   # 如果是目录,则尝试进入到这个目录,再退出来。
                        cwd = ftp.pwd()     # 获取FTP当前路径
                        ftp.cwd(file_name)  # 如果cwd成功 则表示该目录存在 退出到上一级
                        ftp.cwd(cwd)       # 再返回FTP之前的目录
                    except Exception as e:
                        print(f'检查FTP远程目录{file_name}不存在, 现在创建,错误信息:{e}')
                        ftp.mkd(file_name)  # 建立目录
                        print(f'在{remote_path}目录中新建子目录 {file_name} ...')
                        app_log.debug(f'在{remote_path}目录中新建子目录 {file_name} ...')
                    p_local_path = os.path.join(local_path, file_name)    # 拼接本地第一层子目录,递归时进入下一层
                    p_remote_path = os.path.join(ftp.pwd(), file_name)    # 拼接FTP第一层子目录,递归时进入下一层
                    upload_file_tree(p_local_path, p_remote_path, ftp, IsRecursively, app_log)  # 递归
                    ftp.cwd("..")     # 对于递归 ftp 每次传输完成后需要切换目录到上一级
                else:
                    app_log.debug('传输模式是非递归模式,不会创建多级目录!')
                    continue
            else:
                # 是文件 直接上传
                local_file = os.path.join(local_path, file_name)
                remote_file = os.path.join(remote_path, file_name)
                upload_file(ftp, local_file, remote_file, app_log)
 
    except:
        app_log.debug(f'上传文件时有一些错误发生 :{file_name},错误:{traceback.format_exc()}')
        print(f'上传文件时有一些错误发生 :{file_name},错误:{traceback.format_exc()}')
        fail_count += 1
    return
 
 
# 计算本地文件夹中文件个数
def file_count(local_path, type_dict):
    global local_file_count                 # 声明全局变量
    file_list = os.listdir(local_path)  # 列出本地文件夹第一层目录的所有文件和目录
    for file_name in file_list:
        if os.path.isdir(os.path.join(local_path, file_name)):  # 判断是文件还是目录,是目录为真
            type_dict.setdefault("文件夹", 0)      # 如果字典key不存在,则添加并设置为初始值
            type_dict["文件夹"] += 1
            p_local_path = os.path.join(local_path, file_name)  # 拼接本地第一层子目录,递归时进入下一层
            file_count(p_local_path, type_dict)
        else:
            ext = os.path.splitext(file_name)[1]  # 获取到文件的后缀
            type_dict.setdefault(ext, 0)          # 如果字典key不存在,则添加并设置为初始值
            type_dict[ext] += 1
            local_file_count += 1                     # 计算总文件数量
    return local_file_count
 
 
# 日志函数
def log_fun(log_file):
    log_formatter = logging.Formatter('%(levelname)s %(asctime)s <----> %(message)s')
    log_handler = RotatingFileHandler(log_file, mode='a', maxBytes=5*1024*1024, backupCount=0,
                                      encoding=None, delay=False)
    # 生成一个RotatingFileHandler对象,限制日志文件大小为5M
    log_handler.setFormatter(log_formatter)     # 对象载入日志格式
    log_handler.setLevel(logging.DEBUG)          # 对象载入级别
    app_log = logging.getLogger('luo_tao')     # 初始化logging模块
    app_log.setLevel(logging.DEBUG)          # 设置初始化模块级别
    app_log.addHandler(log_handler)        # 载入RotatingFileHandler对象
    return app_log
 
if __name__ == '__main__':
    # 标注为$$$的地方需要修改参数才能正常运行
    # 配置log日志文件
    luo_tao = sys.path[0] + '\\log.log'    # 设置log日志保存的路径
    app_log = log_fun(luo_tao)             # 生成log实例
 
    # 配置连接FTP的参数
    host = '10.1.1.1'    # $$$ 配置FTP服务器IP
    port = 21
    username = 'username'      # $$$ 配置FTP帐号
    password = 'password'      # $$$ 配置FTP密码
    ftp = FTP1()
    ftp.connect(host, port)
    ftp.login(username, password)
 
    # 定义变量
    local_path = 'c:\\abc'          # $$$ 配置本地文件夹的路径
    remote_path = '\\abc'           # $$$ 配置远端FTP的文件夹路径
    IsRecursively = True            # 是否复制子目录下文件的开关,不需要可配置为False
    type_dict = dict()              # 定义一个保存文件类型及数量的空字典
    local_file_count = 0                # 计算本地总文件数,初始为0
    upload_file_count = 0               # 计算有多少文件上传到了FTP
    fail_count = 0                     # 失败计数
 
    # 本地文件计数和上传文件
    file_count(local_path, type_dict)   # 运行计算本地文件夹文件数量的函数
    upload_file_tree(local_path, remote_path, ftp, IsRecursively, app_log)
 
    # 打印文件上传结果
    for each_type in type_dict:
        if each_type == '文件夹':
            continue
        print(f"目录[{local_path}]中文件类型为[{each_type}]的数量有:{type_dict[each_type]} 个")
        app_log.info(f"目录[{local_path}]中文件类型为[{each_type}]的数量有:{type_dict[each_type]} 个")
    print(f"目录[{local_path}]本地文件数量为:{local_file_count},本次FTP文件上传数量为:{upload_file_count}")
    app_log.info(f"目录[{local_path}]本地文件数量为:{local_file_count},本次FTP文件上传数量为:{upload_file_count}")
    if not fail_count:
        print("本地文件上传FTP全部成功!")
        app_log.info("本地文件上传FTP全部成功!")
    else:
        print("本地文件上传FTP有失败记录,请检查日志!")
        app_log.info("本地文件上传FTP有失败记录,请检查日志!")
    app_log.info('==========================================================================')
 

运行结果:

upload_file_tree函数开始运行!FTP远程目录为:/abc/123/789\ggg
上传文件:远程文件 /abc/123/789\ggg\111.doc 不存在,现在开始上传...
/abc/123/789\ggg\111.doc文件上传成功, 远程文件大小 = 9216, 本地文件大小 = 9216
上传文件:远程文件 \abc\2.txt 不存在,现在开始上传...
\abc\2.txt文件上传成功, 远程文件大小 = 14, 本地文件大小 = 14
上传文件:远程文件 \abc\3.txt 不存在,现在开始上传...
\abc\3.txt文件上传成功, 远程文件大小 = 34, 本地文件大小 = 34
检查FTP远程目录456不存在, 现在创建,错误信息:550 CWD failed. "/abc/456": directory not found.
在\abc目录中新建子目录 456 ...
upload_file_tree函数开始运行!FTP远程目录为:/abc\456
上传文件:远程文件 /abc\456\1.log 不存在,现在开始上传...
/abc\456\1.log文件上传成功, 远程文件大小 = 288650, 本地文件大小 = 288650
上传文件:远程文件 /abc\456\2.txt 不存在,现在开始上传...
/abc\456\2.txt文件上传成功, 远程文件大小 = 0, 本地文件大小 = 0
上传文件:远程文件 /abc\456\3.txt 不存在,现在开始上传...
/abc\456\3.txt文件上传成功, 远程文件大小 = 0, 本地文件大小 = 0
上传文件:远程文件 \abc\中天国富安全助手1.3(办公网)_0.log 不存在,现在开始上传...
\abc\中天国富安全助手1.3(办公网)_0.log文件上传成功, 远程文件大小 = 18, 本地文件大小 = 18
上传文件:远程文件 \abc\我爱你.txt 不存在,现在开始上传...
\abc\我爱你.txt文件上传成功, 远程文件大小 = 0, 本地文件大小 = 0
目录[c:\abc]中文件类型为[.log]的数量有:4 个
目录[c:\abc]中文件类型为[.txt]的数量有:13 个
目录[c:\abc]中文件类型为[.doc]的数量有:1 个
目录[c:\abc]本地文件数量为:18,本次FTP文件上传数量为:18
本地文件上传FTP全部成功!
 
进程已结束,退出代码为 0

文章来源:python3实现将本地文件夹全部内容上传到FTP