Python 与 FTP 服务器 -- ftplib 模块

郎喜
2023-12-01

Python 与 FTP 服务器 – ftplib 模块,文件上传下载

需求分析

需求:已建立 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 也没有提供解决方法,再做处理。

 类似资料: