需求:已建立 FTP 服务器,通过 ip、用户名、密码连接后,上传与下载文件,要求能上传或下载多个文件或多个文件夹。以及上传文件的断点续传,断点续传是个坑,暂时没有实现,留个坑在这里。
Python 与 ftp 服务器相关的有个内置模块:ftplib。ftplib 无需下载安装,直接导入即可使用,不过功能较少,像 os 中 isdir、isfile、makedirs 等方法都没有,了解到的比较实用的方法如下:
retrbinary() # 下载
storbinary() # 上传
nlst() # 返回目录列表
dir() # 以长格式列出目录
delete() # 删除文件
cwd() # 改变工作目录
size() # 返回文件大小
mkd() # 创建目录
rmd() # 删除目录
pwd() # 返回当前工作目录
FTP 服务器有两种情况,一种是建立在 Windows 服务器上,另一种就是建立在 Linux 服务器上了。这两种 FTP 服务器类型使得以上方法可能有不同的表现。如 dir() 方法:
Windows 服务器类型下,使用方法 ftp.dir('/test_dir')
,得到的结果如下:
03-17-21 02:00PM <DIR> dir1
03-17-21 02:00PM 51412 m1.mb
由以上结果可以看到,每条数据以时间开始,文件夹类型会有 <DIR>
标志,而文件类型没有标志。这可以作为判断是文件夹还是文件的标志。行末是文件夹名或文件名。
而 Linux 服务器类型下,得到的结果为:
-rw------- 1 ftp ftp 81485666 Mar 02 03:16 ChenNan_Mod.ma
drwx------ 2 ftp ftp 4096 Mar 04 08:31 a1
drwx------ 2 ftp ftp 4096 Mar 02 07:00 a2
这个结果也很明显,就像类 Linux 系统(如 Ubuntu)终端执行命令 ls -l
得到的结果。其中每条数据 -
开始的为文件,而 d
开始的为文件夹。
ftplib 虽然方法较少,但是并非不能完成任务。有些方法暂时用不到,如:delete()、pwd() 等。而有些方法需要自己实现,如:递归创建目录,判断文件夹、文件是否存在等。要实现的这些方法可以通过已有的方法实现,例如,通过 dir() 方法便可以实现像 os.listdir() 方法一样的功能。而通过 cwd() 方法,可以实现像 os.path.exists() 方法一样的功能;再结合 mkd() 方法可以实现递归创建目录(os.makedirs())。具体看以下代码。
代码如下(Linux):
import os
from ftplib import FTP
def ftp_makedirs(ftp, remote_path):
""" 递归创建目录,同 os.makedirs()。"""
ftp.cwd('/') # 切换 FTP 根目录
dires = remote_path.split('/')
for dire in dires: # 遍历给定路径的每一层目录
try:
ftp.cwd(dire) # 切换给定路径的目录,如果没有,进入 except
except:
ftp.mkd(dire) # 没有目录,创建后再切换,最终实现递归创建给定目录
ftp.cwd(dire)
def ftp_path_exists(ftp, remote_path):
""" 判断是否存在 ftp 路径,同 os.path.exists(),但是只能判断 dir,不能判断 file。"""
try:
ftp.cwd(remote_path) # 能切换给定路径,说明存在
return True
except:
return False
def ftp_file_exists(ftp, remote_file):
""" 判断是否存在 ftp 文件,同 os.path.exists(),但是只能判断 file,不能判断 dir。"""
try:
ftp.size(remote_file) # 能获取给定文件大小,说明存在
return True
except:
return False
def get_local_path(local_path, file_list=None):
""" 给定一个本地目录,递归获取所有文件。"""
files = os.listdir(local_path)
for file in files:
if not os.path.isdir(local_path + os.sep + file):
file_list.append(local_path + os.sep + file)
else:
if os.listdir(local_path + os.sep + file):
get_local_path(local_path + os.sep + file, file_list)
return file_list
def get_remote_path(ftp, remote_path, file_list=None):
""" 给定一个 ftp 文件夹路径,获取路径下的所有文件。"""
file_infos = []
ftp.cwd(remote_path.replace('\\', '/')) # 切换到下载目录
ftp.dir(remote_path.replace('\\', '/'), file_infos.append) # 列出文件夹内容,存入列表
for file_info in file_infos:
if file_info.startswith('-'): # 以 - 开始,为文件
file_list.append(remote_path + '\\' + file_info.split(' ')[-1])
if file_info.startswith('d'): # 以 d 开始,为文件夹
folder = file_info.split(' ')[-1]
get_remote_path(ftp, remote_path + '\\' + folder, file_list)
return file_list
def upload_tracker(block, dst):
""" 上传回调函数,实现上传进度。"""
global write_size, total_size
write_size += 64 * 1024 # 文件每次写入的大小,用来实现进度条,必须和 blocksize 大小一样
progress = round((write_size / total_size) * 100)
if progress >= 100:
print('\rUpload ' + dst + ' ' + '100%', end='')
else:
print('\rUpload ' + dst + ' ' + '%3s%%' % str(progress), end='')
def upload_file(ftp, src, dst):
""" 上传文件 """
global write_size, total_size
write_size = 0 # 文件每次写入的大小,初始为 0
total_size = os.path.getsize(src.replace('\\', '/'))
blocksize = 64 * 1024 # 文件每次写入缓冲区的大小,64 KB
with open(src, "rb") as f:
ftp.storbinary('STOR ' + dst.replace('\\', '/'), f, blocksize, lambda block: upload_tracker(block, dst))
def download_tracker(block, f, dst):
""" 下载回调函数,实现上传进度。"""
f.write(block)
global write_size, total_size
write_size += 64 * 1024
progress = round((write_size / total_size) * 100)
if progress >= 100:
print('\rDownload ' + dst + ' ' + '100%', end='')
else:
print('\rDownload ' + dst + ' ' + '%3s%%' % str(progress), end='')
def download_file(ftp, src, dst):
""" 下载文件。"""
global write_size, total_size
write_size = 0
total_size = ftp.size(src.replace('\\', '/'))
blocksize = 64 * 1024
with open(dst, 'wb') as f:
ftp.retrbinary('RETR ' + src.replace('\\', '/'), lambda block: download_tracker(block, f, dst), blocksize)
def upload(ftp, data):
""" 处理上传,可以是文件列表或文件夹列表,不可以混合上传。"""
if os.path.isfile(data["local_path"][0]): # 上传列表中,第一个是文件且存在
if not ftp_path_exists(ftp, data["remote_path"].replace('\\', '/')):
ftp_makedirs(ftp, data["remote_path"].replace('\\', '/'))
for filepath in data["local_path"]:
dst = data["remote_path"] + '\\' + filepath.split('\\')[-1]
upload_file(ftp, filepath, dst)
elif os.path.isdir(data["local_path"][0]): # 上传列表中,第一个是文件夹且存在
if not ftp_path_exists(ftp, data["remote_path"].replace('\\', '/')):
ftp_makedirs(ftp, data["remote_path"].replace('\\', '/'))
file_list = []
for local_path in data["local_path"]:
file_list.extend(get_local_path(local_path, []))
for filepath in file_list:
dst = data["remote_path"] + '\\' + filepath.split('\\')[-1]
upload_file(ftp, filepath, dst)
def download(ftp, data):
""" 处理下载,可以下载文件列表或者文件夹列表,而不可混合下载。 """
if not os.path.exists(data["local_path"]):
os.makedirs(data["local_path"])
if ftp_file_exists(ftp, data["remote_path"][0].replace('\\', '/')): # 下载列表中,第一个是文件且存在
for filepath in data["remote_path"]:
dst = data['local_path'] + '\\' + filepath.split('\\')[-1]
if ftp_file_exists(ftp, filepath.replace('\\', '/')):
download_file(ftp, filepath, dst)
elif ftp_path_exists(ftp, data["remote_path"][0].replace('\\', '/')): # 下载列表中,第一个是文件夹且存在
file_list = []
for remote_path in data["remote_path"]:
file_list.extend(get_remote_path(ftp, remote_path, []))
for filepath in file_list:
dst = data['local_path'] + '\\' + filepath.split('\\')[-1]
if ftp_file_exists(ftp, filepath.replace('\\', '/')):
download_file(ftp, filepath, dst)
if __name__ == '__main__':
host, username, password = '*.*.*.*', '******', '******'
ftp = FTP()
ftp.encoding = "utf-8"
ftp.connect(host)
ftp.login(username, password)
data1 = {"action": "upload", "local_path": ["C:\\test\\mayaFile\\ChenNan_Mod.ma"], "remote_path": "\\test1\\t1\\a1"}
data = {"action": "download", "local_path": "C:\\test\\download\\a", "remote_path": ['\\test1\\t1\\a2\\ChenNan_Mod.ma']}
print('Start...')
if data["action"] == 'upload': upload(ftp, data1)
if data["action"] == 'download': download(ftp, data)
ftp.close()
print('End...')
以上就是所有代码,注释算是很详细了,就不一一解释了,代码以 FTP 架设在 Linux 上为例,与 Windows 差别不大,稍微改动即可使用。
最后发现另一个问题,文件名与文件夹名出现大小写区别时,会有一些怪象,这大概与 Windows 文件系统不区分大小写,而 Linux 系统区分大小写有关。由于发现一个第三方模块 ftputil,更好用一些,决定使用 ftputil 重新开发,放弃 ftplib,所以这个问题暂时不在 ftplib 里解决,如果 ftputil 也没有提供解决方法,再做处理。