Python 与 FTP 服务器 – ftputil 模块,文件上传下载
前一篇文章分析了用 Python 内置的模块 ftplib 实现上传下载等功能,本篇文章就来看看另一个高水平的 FTP 库 ———— ftputil。它的官网:ftputil。
项目需求与分析
在之前 ftplib 的文章已经分析过,而且已经说明在本次文章中要处理一些其他问题,不再详述。
ftputil
ftputil 是第三方模块,是 ftplib 的扩展,完善了许多方法,os 中操作文件的主要方法,ftputil 几乎都有,包括 shutil 的部分方法,目前 v4.0.0 中只缺少一个 copytree() 主要方法,比较可惜,不过上传下载暂时用不到,不过相比 ftplib 已是好了很多。部分方法如下:
ftp.path.exists()
ftp.path.isdir()
ftp.path.isfile()
ftp.path.getsize()
ftp.makedirs()
ftp.listdir()
ftp.chdir()
ftp.rmtree()
ftp.rename()
本以为 ftputil 的代码实现会轻松许多,因为提供了许多方法,用起来也比较方便,和 os 相似。不过最后还是大意了,发现一些方法存在一些 bug,这里先来了解一下上传下载中用到的方法。
在 Windows 系统下,使用 os 的方法操作文件,不区分文件名与文件夹名大小写,这应该和 Windows 系统不区分大小写相关。但是 ftputil 提供的方法部分区分大小写。假设 FTP 上有一个路径为 /test_dir/dir1/a.txt
,根据测试,ftp.path.exists(’/test_dir/dir1’)、ftp.path.isdir(’/test_dir/dir1’)、ftp.listdir(’/test_dir/dir1’) 这三个方法,都可以返回 True,但是如果将路径改为 /test_dir/Dir1
或者 /Test_dir/dir1
都返回 False。ftp.path.isfile() 对文件的操作与上述方法一样,但 ftp.makedirs()、ftp.path.basename()、ftp.chdir() 三个方法却不区分,大小写没有区别。而 ftp.path.getsize() 方法,对目录不区分大小写,但是对文件区分大小写,即 /test_dir/dir1
、/test_dir/Dir1
和 /Test_dir/dir1
是一个目录,而 /test_dir/dir1/a.txt
和 /test_dir/dir1/A.txt
却是两个文件。
测试得知,ftp.makedirs()、ftp.path.basename()、ftp.chdir() 这三个方法不区分大小写。而 ftp.path.getsize() 文件夹名不区分大小写,文件名区分大小写。
总之,对于 Windows 建立的 FTP 来说,部分方法无法使用,可以利用上述的三个不区分大小写的方法,重写部分方法。
功能实现
代码如下:
import os
import ftplib
import ftputil
def ftp_path_isdir(path, ftp):
""" 同 os.path.isdir """
try:
ftp.chdir(path)
return True
except:
return False
def ftp_path_isfile(file, ftp):
""" 同 os.path.isfile """
try:
filename = ftp.path.basename(file)
if ftp_path_isdir(file.replace(filename, ''), ftp):
ftp.path.getsize(file)
return True
return False
except:
return False
def get_local_path(local_path, file_list=None):
""" 给定一个本地文件夹,递归列出所有文件 """
files = os.listdir(local_path)
for file in files:
temp_path = local_path + os.sep + file
if not os.path.isdir(temp_path): # 文件
file_list.append(temp_path)
else: # 文件夹
get_local_path(temp_path, file_list)
return file_list
def get_remote_path(ftp, remote_path, file_list=None):
""" 给定一个 ftp 文件夹,递归列出所有文件 """
ftp.chdir(remote_path.replace('\\', '/')) # 解决 ftp.listdir 大小写无法区分问题
files = ftp.listdir(remote_path.replace('\\', '/'))
for file in files:
temp_path = remote_path + '\\' + file
if not ftp_path_isdir(temp_path.replace('\\', '/'), ftp): # 文件
file_list.append(temp_path)
else: # 路径
get_remote_path(ftp, temp_path, file_list)
return file_list
def upload_tracker(block, src):
""" 上传回调函数,实现上传进度 """
global file_write, total_size
file_write += 64 * 1024
progress = round((file_write / total_size) * 100)
if progress >= 100:
print('Upload ' + src + ' ' + '100%')
else:
print('Upload ' + src + ' ' + '%3s%%' % str(progress))
def upload_file(ftp, src, dst):
"""上传文件,src, dst 均以文件名结尾"""
global file_write, total_size
file_write = 0
total_size = os.path.getsize(os.path.abspath(src))
ftp.upload(src, dst.replace('\\', '/'), lambda block: upload_tracker(block, src))
def download_file(ftp, src, dst):
""" 下载文件,src、dst 均以文件名结尾 """
global file_write, total_size
file_write = 0
total_size = ftp.path.getsize(src.replace('\\', '/'))
ftp.download(src.replace('\\', '/'), dst, lambda block: download_tracker(block, src))
def download_tracker(block, src):
""" 下载回调函数,实现下载进度 """
global file_write, total_size
file_write += 64 * 1024
progress = round((file_write / total_size) * 100)
if progress >= 100:
print('Download ' + src + ' ' + '100%')
else:
print('Download ' + src + ' ' + '%3s%%' % str(progress))
def upload(ftp, data):
""" 处理上传,可以是文件列表或文件夹列表,不可以混合上传。 """
if not ftp_path_isdir(data['remote_path'].replace('\\', '/'), ftp):
ftp.makedirs(data['remote_path'].replace('\\', '/'))
is_file, is_dir = False, False
if 'local_path' in data and data['local_path']:
# 循环遍历,判断 local_path 中的路径类型,文件还是文件夹
for local_path in data['local_path']:
if os.path.isfile(local_path):
is_file = True
break
elif os.path.isdir(local_path):
is_dir = True
break
if is_file: # 文件
for filepath in data['local_path']:
dst = data['remote_path'] + '\\' + filepath.split('\\')[-1]
if os.path.exists(filepath):
upload_file(ftp, filepath, dst)
elif is_dir: # 文件夹
filt_list = []
for local_path in data['local_path']:
if os.path.exists(local_path):
file_list.extend(get_local_path(local_path, []))
for filepath in file_list:
dst = data['remote_path'] + '\\' + filepath.split('\\')[-1]
if os.path.exists(filepath):
upload_file(ftp, filepath, dst)
def download(ftp, data):
""" 处理下载,可以下载文件列表或者文件夹列表,而不可混合下载。 """
if not os.path.exists(data['local_path']):
os.makedirs(data['local_path'])
is_file, is_dir = False, False
if 'remote_path' in data and data['remote_path']:
# 循环遍历,判断 remote_path 中的路径类型,文件还是文件夹
for remote_path in data['remote_path']:
if ftp_path_isfile(remote_path.replace('\\', '/'), ftp):
is_file = True
break
elif ftp_path_isdir(remote_path.replace('\\', '/'), ftp):
is_dir = True
break
if is_file: # 文件
for filepath in data['remote_path']:
dst = data['local_path'] + '\\' + filepath.split('\\')[-1]
if ftp_path_isfile(filepath.replace('\\', '/'), ftp):
download_file(ftp, filepath, dst)
elif is_dir: # 文件夹
file_list = []
for remote_path in data['remote_path']:
if ftp_path_isdir(remote_path.replace('\\', '/'), ftp):
file_list.extend(get_remote_path(ftp, remote_path, []))
for filepath in file_list:
dst = data['local_path'] + '\\' + filepath.split('\\')[-1]
if ftp_path_isfile(filepath.replace('\\', '/'), ftp):
download_file(ftp, filepath, dst)
class DefaultFTP(ftplib.FTP):
""" 修改 ftplib 的默认编码 """
encoding = 'gbk'
if __name__ == '__main__':
host, username, password = '***.***.***.***', '******', '********'
ftp = ftputil.FTPHost(host, username, password, session_factory=DefaultFTP)
data = {"action": "upload", "local_path": ["C:\\TencentFile\\ChenNan_Mod.ma"], "remote_path": "\\test1\\t1\\a1"}
# data = {"action": "download", "local_path": "C:\\test\\download\\c", "remote_path": ["\\test1\\t1\\a2", "\\test1\\t1\\a1"]}
print('Start...')
if data["action"] == 'upload': upload(ftp, data)
if data["action"] == 'download': download(ftp, data)
print('End...')
ftp.close()
以上就是全部代码,重要的地方添加了一些注释。本次代码测试环境为 Windows 下的 FTP 服务器。
断点续传
断点续传的功能未实现,断点续传涉及到很多问题,要根据具体情况解决,原理并不难。这里简单说一下,利用 try/except 语法,try 中上传出错,进入到 except 中,获取已经上传文件的大小,上传方法中添加偏移量,从中断处继续上传。本文中,ftp.upload() 方法,并未提供偏移量参数,可以使用 ftp.open() 方法,与 open 方法相似,但是没有提供回调方法,需要重新写上传进度功能。
如果使用 ftplib,其中的 ftp.storbinary() 上传方法,既提供了回调方法,也有偏移量,可以酌情考虑。