需求:
1、将本地文件夹的内容上传到FTP指定目录下,包括子目录及文件,要求支持中文文件名。
2、如果FTP上文件存在,对比大小不一致则覆盖上传,如果一致则不用上传。
3、要有日志功能方便查看
4、尽量以函数的方式实现
思路:
1、先上传一级目录,子目录使用递归的方式层层上传。FTP上如果目录不存在则新建。
2、上传单个文件时进行文件大小对比。如果大小不一致则覆盖上传。
3、完成后统计上传文件的类型和数量,并打印上传结果。
本程序的改进:
此程序是我参考了网上多个实现方式,花了半个月时间改进过来的。对以前的功能进行了大幅完善。
1、解决了复制文件夹时目录顺序错乱的问题。
2、解决了不支持复制中文文件名的文件的问题。
3、优化了日志系统,将日志改为了中文,增加了显示上传结果,日志文件为单个文件,大小循环限制为5M。
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