python通过FTP和SFTP并行下载远程主机文件

       先说一下程序背景:最近项目上需要做业务切换,以前的自动调度系统不能再继续提供服务了,计划通过Python写一个远程下载程序,再通过crontab起定时任务来取代之前的调度系统。我们将需要下载的文件叫接口文件,接口文件位于集团接口机,数据提供者会不定时不定量的向集团接口机传送文件,为了保证我们下载的文件的完整性、及时性和可维护性,需要在控制文件下载的速度,校验文件的完整性和程序运行的可配置性。

        目前大体的规划是以上描述的背景,还有很多细节我就不详细描述了。整个项目的结构如下:

|

|——download_file.py         运行主程序,读取配置文件,下载远程接口文件

|——configclass.py             配置类,将配置文件中的配置项抽象为该类对象

|——FTPConnection.py      FTP连接工具类

|——SFTPConnection.py   SFTP连接工具类

|——readconfigfile.py         配置文件读取工具类

|——pylog.py                     日志工具类

|——utils.py                       常用工具类

里面比较重要的一个实体是通过读取配置文件创建的,我贴出来可以对下面的程序有些帮助:

# -*- coding: utf-8 -*-

import re
import logging
import os
import shutil

"""
@Description:配置文件中的配置项
"""

logger = logging.getLogger()
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
logger.setLevel(logging.INFO)


class ConfigClass(object):
    def __init__(self, business_name, deal_date, period):
        super(ConfigClass, self).__init__()
        self.__deal_date = deal_date
        self.__business_name = business_name
        self.__period = period
        self.__host = ''
        self.__username = ''
        self.__password = ''
        self.__port = ''
        self.__src_dir = ''
        self.__temp_dir = ''
        self.__tgt_dir = ''
        self.__file_name = ''
        self.__cycle = ''
        self.__client = ''
        self.__number_processes = 5

    def get_deal_date(self):
        return self.__deal_date

    def get_host(self):
        return self.__host

    def set_host(self, host):
        self.__host = host

    def get_username(self):
        return self.__username

    def set_username(self, username):
        self.__username = str(username)

    def get_password(self):
        return self.__password

    def set_password(self, password):
        self.__password = str(password)

    def get_port(self):
        return self.__port

    def set_port(self, port):
        self.__port = int(port)

    def get_src_dir(self):
        return self.__src_dir

    def set_src_dir(self, src_dir):
        self.__src_dir = str(src_dir).strip()

    def get_temp_dir(self):
        return self.__temp_dir

    def set_temp_dir(self, temp_dir):
        self.__temp_dir = os.path.join(temp_dir.strip(), self.__deal_date.strftime("%Y%m%d"))
        # 创建临时文件保存目录
        if not os.path.exists(self.__temp_dir):
            try:
                os.makedirs(self.__temp_dir)
            except Exception as exception:
                logger.error("Error:" + exception.message)
        else:
            shutil.rmtree(self.__temp_dir)
            try:
                os.makedirs(self.__temp_dir)
            except Exception as exception:
                logger.error("Error:" + exception.message)

    def get_tgt_dir(self):
        return self.__tgt_dir

    def set_tgt_dir(self, tgt_dir):
        self.__tgt_dir = str(tgt_dir).strip()

    def get_file_name(self):
        return self.__file_name

    def set_file_name(self, file_name):
        self.__file_name = str(file_name)

    def get_cycle(self):
        return self.__cycle

    def set_cycle(self, cycle):
        self.__cycle = str(cycle).upper()

    def get_client(self):
        return self.__client

    def set_client(self, client):
        self.__client = str(client).upper()

    def get_period(self):
        return self.__period

    def set_period(self, period):
        self.__period = period

    def set__number_processes(self, number_processes):
        self.__number_processes = number_processes

    def get_number_processes(self):
        return self.__number_processes

    # 校验配置文件中的数据是否满足基本要求
    def is_valid_cfg(self):
        flag = True
        compile_ip = re.compile(r'^(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|[1-9])\.(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)$')
        if compile_ip.match(self.__host) is None:
            logger.error("IP address is incorrect!")
            flag = False

        if self.__client == 'SFTP' and self.__port != 22:
            logger.error("The remote connection mode is inconsistent with the port number!")
            flag = False
        elif self.__client == 'FTP' and self.__port != 21:
            logger.error("The remote connection mode is inconsistent with the port number!")
            flag = False

        if self.__cycle not in ['M', 'D', 'H', 'MM']:
            logger.error("The cycle property is incorrect!")
            flag = False

        if 'YYYY' in (self.__src_dir.split('/')[-1]).upper():
            if self.__cycle == 'M' and 'YYYYMM' != (self.__src_dir.split('/')[-1]).upper():
                logger.error("The cycle property is mismatching src_dir property!")
                flag = False
            elif self.__cycle == 'D' and 'YYYYMMDD' != (self.__src_dir.split('/')[-1]).upper():
                logger.error("The cycle property is mismatching src_dir property!")
                flag = False
            elif self.__cycle == 'H' and 'YYYYMMDDHH' != (self.__src_dir.split('/')[-1]).upper():
                logger.error("The cycle property is mismatching src_dir property!")
                flag = False
            elif self.__cycle == 'MM' and 'YYYYMMDDHHMM' != (self.__src_dir.split('/')[-1]).upper():
                logger.error("The cycle property is mismatching src_dir property!")
                flag = False
        return flag

    def record_config(self):
        logger.info("host:" + self.get_host())
        logger.info("src_dir:" + self.get_src_dir())
        logger.info("tmp_dir:" + self.get_temp_dir())
        logger.info("tgt_dir:" + self.get_tgt_dir())
        logger.info("cycle:"+self.get_cycle())
        logger.info("client:"+self.get_client())

最后我来说一下里面用到的FTP下载功能和SFTP下载功能是怎么实现的,话不多说,直接上代码。如果paramiko包不太了解的话,我可以把学习视频分享给你哦!QQ:1242902939添加时写备注哦!

FTPConnection.py

# -*- coding: utf-8 -*-

from ftplib import FTP
import ftplib
import socket
import os
import sys
import logging
import shutil
from utils import Utils


"""
@Description:创建FPT连接
"""
logger = logging.getLogger()
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
logger.setLevel(logging.INFO)


class FTPConnection:

    def __init__(self, host, username, password, port):
        self.host = host
        self.username = username
        self.password = password
        self.port = port

    def get_connection(self):
        ftp = FTP()
        # 不开启调试模式
        ftp.set_debuglevel(0)
        try:
            ftp.connect(host=self.host, port=self.port)
            ftp.login(self.username, self.password)
            ftp.encoding = 'gbk'
            # ftp有主动 被动模式 需要调整
            ftp.set_pasv(False)
            logger.info("FTP Connected Successful!")
        except (socket.error, socket.gaierror):
            logger.error('Error: can not connect %s %d' % (self.host, self.port))
            logger.error("FTP Connected Failed!")
            sys.exit(1)
        except ftplib.error_perm:
            logger.error('Error: user Authentication failed')
            logger.error("FTP Connected Failed!")
            sys.exit(1)
        else:
            print(ftp.getwelcome())
        return ftp

    """
        获得远程目录下待下载文件列表
        config:配置文件对象
    """
    def get_download_file_list(self, config):
        down_file_list = []
        # 文件关键字/文件夹目录
        file_name_keys = Utils.get_file_keys(config.get_deal_date(), config.get_cycle(), config.get_period())
        ftp = self.get_connection()
        # 显示登录ftp信息
        logger.info(ftp.getwelcome())
        schema_file_name = config.get_file_name()
        # 列出待下载的文件路径
        for file_name_key in file_name_keys:
            ftp_dir_path = config.get_src_dir()
            dst_dir_path = config.get_tgt_dir()
            if "YYYYMMDDHHMM" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMMDDHHMM", file_name_key)
                dst_dir_path = dst_dir_path.replace("YYYYMMDDHHMM", file_name_key)
            elif "YYYYMMDDHH" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMMDDHH", file_name_key)
                dst_dir_path = dst_dir_path.replace("YYYYMMDDHH", file_name_key)
            elif "YYYYMMDD" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMMDD", file_name_key)
                dst_dir_path = dst_dir_path.replace("YYYYMMDD", file_name_key)
            elif "YYYYMM" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMM", file_name_key)
                dst_dir_path = dst_dir_path.replace("YYYYMM", file_name_key)
            if not os.path.exists(dst_dir_path):
                os.makedirs(dst_dir_path)
            # 获得远程目录下的文件列表
            ftp_file_list = ftp.nlst(ftp_dir_path)
            for file_path in ftp_file_list:
                try:
                    file_name = os.path.basename(file_path)
                    dst_file_path = os.path.join(dst_dir_path, file_name)
                    # 是否为文件、是否与配置的文件模式一样,是否包含文件关键字,本地是否已经存在该文件
                    if Utils.is_file(file_name) and Utils.is_target_file(schema_file_name, file_name) and \
                       file_name_key in file_name and not os.path.exists(dst_file_path):
                        # 将待下载的文件路径添加到待下载的文件列表中
                        down_file_list.append(file_path)
                except Exception as e:
                    logger.error(e.message)
        ftp.quit()
        return down_file_list

    """
        从ftp下载文件到本地
        download_file_path: 远程目标文件路径列表
        tmp_file_path: 临时文件保存目录列表
    """
    def download_file(self, download_file_path, tmp_file_path):
        ftp = self.get_connection()
        # 显示登录ftp信息
        logger.info(ftp.getwelcome())
        try:
            logger.info("Downloading {0} ...".format(download_file_path))
            with open(tmp_file_path, "wb") as f:
                ftp.retrbinary('RETR {0}'.format(download_file_path), f.write, 10240)
                ftp.set_debuglevel(0)
            logger.info("Download Success: "+download_file_path)
        except Exception as e:
            logger.error(e.message)
            logger.error("Download Failed: "+download_file_path)
        ftp.quit()
        logger.info("FTP Connection Has Been Close!")

    """
        移动文件夹内的数据文件,判断本地文件与远程文件的大小是否一致,如果不一致则删除临时文件
        config:配置文件对象
    """
    def sync_file(self, config):
        sync_file_list = []
        # 文件关键字/文件夹目录
        file_name_keys = Utils.get_file_keys(config.get_deal_date(), config.get_cycle(), config.get_period())

        # 获得本地目录下文件的大小
        tmp_file_map_size = {}
        for file_name in os.listdir(config.get_temp_dir()):
            tmp_file_path = os.path.join(config.get_temp_dir(), file_name)
            tmp_file_map_size[file_name] = os.stat(tmp_file_path).st_size

        # 远程目录下文件大小
        ftp_file_map_size = {}
        ftp = self.get_connection()
        # 列出待下载的文件路径
        for file_name_key in file_name_keys:
            ftp_dir_path = config.get_src_dir()
            if "YYYYMMDDHHMM" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMMDDHHMM", file_name_key)
            elif "YYYYMMDDHH" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMMDDHH", file_name_key)
            elif "YYYYMMDD" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMMDD", file_name_key)
            elif "YYYYMM" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMM", file_name_key)
            for ftp_file_path in ftp.nlst(ftp_dir_path):
                file_name = os.path.basename(ftp_file_path)
                if file_name_key in file_name and Utils.is_target_file(config.get_file_name(), file_name):
                    ftp_file_map_size[file_name] = ftp.size(ftp_file_path)
        ftp.quit()
        # 对比远程目录和本地目录文件大小,相同则移动文件,不同则删除文件
        tmp_file_dir = config.get_temp_dir()
        for file_name, size in tmp_file_map_size.items():
            for file_name_key in file_name_keys:
                dst_dir_path = config.get_tgt_dir()
                if "YYYYMMDDHHMM" == dst_dir_path.split("/")[-1]:
                    dst_dir_path = dst_dir_path.replace("YYYYMMDDHHMM", file_name_key)
                elif "YYYYMMDDHH" == dst_dir_path.split("/")[-1]:
                    dst_dir_path = dst_dir_path.replace("YYYYMMDDHH", file_name_key)
                elif "YYYYMMDD" == dst_dir_path.split("/")[-1]:
                    dst_dir_path = dst_dir_path.replace("YYYYMMDD", file_name_key)
                elif "YYYYMM" == dst_dir_path.split("/")[-1]:
                    dst_dir_path = dst_dir_path.replace("YYYYMM", file_name_key)
                # 文件大小相同且文件名中包含文件关键字,则将临时目录下的文件移动到正式目录中
                if size == ftp_file_map_size[file_name] and file_name_key in file_name:
                    shutil.move(os.path.join(tmp_file_dir, file_name), os.path.join(dst_dir_path, file_name))
                    sync_file_list.append(os.path.join(tmp_file_dir, file_name))
                else:
                    # 删除文件
                    logger.info("{file_name} size is not same!".format(file_name=file_name))
                    os.remove(os.path.join(tmp_file_dir, file_name))
                    logger.info("{file_name} has been deleted!".format(file_name=file_name))
        return sync_file_list

SFTPConnection.py

# -*- coding: utf-8 -*-

import paramiko
import os
import sys
import logging
import shutil
from utils import Utils


logger = logging.getLogger()
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
logger.setLevel(logging.INFO)


class SFTPConnection(object):
    def __init__(self, host, username, password, port):
        self._host = host
        self._port = port
        self._username = username
        self._password = password
        self._transport = None

    # 获得sftp连接对象
    def get_connection(self):
        try:
            self._transport = paramiko.Transport((self._host, self._port))
            self._transport.connect(username=self._username, password=self._password)
            sftp = paramiko.SFTPClient.from_transport(self._transport)
        except Exception as e:
            logger.error("The sftp connection failed!")
            logger.error(e.message)
            sys.exit(1)
        return sftp

    """
            获得远程目录下待下载文件列表
            config:配置类
    """
    def get_download_file_list(self, config):
        down_file_list = []
        # 文件关键字/文件夹目录
        file_name_keys = Utils.get_file_keys(config.get_deal_date(), config.get_cycle(), config.get_period())
        sftp = self.get_connection()

        schema_file_name = config.get_file_name()
        # 列出待下载的文件路径
        for file_name_key in file_name_keys:
            ftp_dir_path = config.get_src_dir()
            dst_dir_path = config.get_tgt_dir()
            if "YYYYMMDDHHMM" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMMDDHHMM", file_name_key)
                dst_dir_path = dst_dir_path.replace("YYYYMMDDHHMM", file_name_key)
            elif "YYYYMMDDHH" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMMDDHH", file_name_key)
                dst_dir_path = dst_dir_path.replace("YYYYMMDDHH", file_name_key)
            elif "YYYYMMDD" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMMDD", file_name_key)
                dst_dir_path = dst_dir_path.replace("YYYYMMDD", file_name_key)
            elif "YYYYMM" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMM", file_name_key)
                dst_dir_path = dst_dir_path.replace("YYYYMM", file_name_key)
            if not os.path.exists(dst_dir_path):
                os.makedirs(dst_dir_path)
            # 获得远程目录下的文件列表
            files = sftp.listdir_attr(ftp_dir_path)
            for file_obj in files:
                try:
                    file_name = file_obj.filename
                    dst_file_path = os.path.join(dst_dir_path, file_name)
                    # 是否为文件、是否与配置的文件模式一样,是否包含文件关键字,本地是否已经存在该文件
                    if Utils.is_file(file_name) and Utils.is_target_file(schema_file_name, file_name) and \
                            file_name_key in file_name and not os.path.exists(dst_file_path):
                        # 将待下载的文件路径添加到待下载的文件列表中
                        down_file_list.append(os.path.join(ftp_dir_path, file_name))
                except Exception as e:
                    logger.error(e.message)
        self.close()
        return down_file_list

    """
        下载接口文件
        download_file_path: 远程目标文件路径
        tmp_file_path: 本地临时文件存放路径
    """
    def download_file(self, download_file_path, tmp_file_path):
        sftp = self.get_connection()
        try:
            logger.info("Downloading {0} ...".format(download_file_path))
            sftp.get(download_file_path, tmp_file_path)
            logger.info("Download success: {0}".format(download_file_path))
        except Exception as e:
            logger.error("Download {remote_file_path} failed!".format(remote_file_path=download_file_path))
            logger.error(e.message)
        self.close()

    """
        移动文件夹内的数据文件,判断本地文件与远程文件的大小是否一致,如果不一致则删除临时文件
        ftp_file_dir:远程数据文件所在文件夹
        tmp_file_dir:临时数据文件所在文件夹
        dst_file_dir:最终数据文件保存文件夹
    """
    def sync_file(self, config):
        sync_file_list = []
        file_name_keys = Utils.get_file_keys(config.get_deal_date(), config.get_cycle(), config.get_period())
        # 获得本地目录下文件的大小
        tmp_file_map_size = {}
        for file_name in os.listdir(config.get_temp_dir()):
            tmp_file_path = os.path.join(config.get_temp_dir(), file_name)
            tmp_file_map_size[file_name] = os.stat(tmp_file_path).st_size

        # 获得远程目录下文件大小
        ftp_file_map_size = {}
        sftp = self.get_connection()
        # 列出待下载的文件路径
        for file_name_key in file_name_keys:
            ftp_dir_path = config.get_src_dir()
            if "YYYYMMDDHHMM" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMMDDHHMM", file_name_key)
            elif "YYYYMMDDHH" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMMDDHH", file_name_key)
            elif "YYYYMMDD" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMMDD", file_name_key)
            elif "YYYYMM" == ftp_dir_path.split("/")[-1]:
                ftp_dir_path = ftp_dir_path.replace("YYYYMM", file_name_key)
            for file_name in sftp.listdir(ftp_dir_path):
                if file_name_key in file_name and Utils.is_target_file(config.get_file_name(), file_name):
                    ftp_file_path = os.path.join(ftp_dir_path, file_name)
                    info = sftp.stat(ftp_file_path)
                    ftp_file_map_size[file_name] = info.st_size
        self.close()

        # 对比远程目录和本地目录文件大小,相同则移动文件,不同则删除文件
        tmp_file_dir = config.get_temp_dir()
        for file_name, size in tmp_file_map_size.items():
            for file_name_key in file_name_keys:
                dst_dir_path = config.get_tgt_dir()
                if "YYYYMMDDHHMM" == dst_dir_path.split("/")[-1]:
                    dst_dir_path = dst_dir_path.replace("YYYYMMDDHHMM", file_name_key)
                elif "YYYYMMDDHH" == dst_dir_path.split("/")[-1]:
                    dst_dir_path = dst_dir_path.replace("YYYYMMDDHH", file_name_key)
                elif "YYYYMMDD" == dst_dir_path.split("/")[-1]:
                    dst_dir_path = dst_dir_path.replace("YYYYMMDD", file_name_key)
                elif "YYYYMM" == dst_dir_path.split("/")[-1]:
                    dst_dir_path = dst_dir_path.replace("YYYYMM", file_name_key)
                # 文件大小相同且文件名中包含文件关键字,则将临时目录下的文件移动到正式目录中
                if size == ftp_file_map_size[file_name] and file_name_key in file_name:
                    shutil.move(os.path.join(tmp_file_dir, file_name), os.path.join(dst_dir_path, file_name))
                    sync_file_list.append(os.path.join(tmp_file_dir, file_name))
                else:
                    # 删除文件
                    logger.info("{file_name} size is not same!".format(file_name=file_name))
                    os.remove(os.path.join(tmp_file_dir, file_name))
                    logger.info("{file_name} has been deleted!".format(file_name=file_name))
        return sync_file_list

    # 关闭
    def close(self):
        if self._transport:
            self._transport.close()

 最后的并行下载我简单的举个FTP例子,SFTP的例子类似就不重复写了:

# -*- coding: utf-8 -*-

import logging
import sys
import os
import datetime
from optparse import OptionParser
from utils import Utils
from FTPConnection import FTPConnection
from SFTPConnection import SFTPConnection
from multiprocessing import Pool
from itertools import repeat

# 例如FTP并行下载
def ftp_download(host, username, password, port, download_file_path, temp_file_path):
    ftp = FTPConnection(host, username, password, port)
    return ftp.download_file(download_file_path, temp_file_path)


def ftp_download_func(a):
    return ftp_download(*a)

def main(config_file_path, business_name, deal_date, period):
    config_entity = Utils.read_config(config_file_path, business_name, deal_date, period)
    config_entity.record_config()
    # 接口主机IP地址
    host = config_entity.get_host()
    # 接口主机用户名
    username = config_entity.get_username()
    # 接口主机密码
    password = config_entity.get_password()
    # 接口主机端口号
    port = config_entity.get_port()
    # 线程池
    p = Pool(config_entity.get_number_processes())
    ftp = FTPConnection(host, username, password, port)

    # 获得待下载的文件列表
    download_file_list = ftp.get_download_file_list(config_entity)

    # 临时文件待保存目录列表
    temp_file_list = []
    for download_file_path in download_file_list:
        download_file_name = os.path.basename(download_file_path)
        temp_file_list.append(os.path.join(config_entity.get_temp_dir(), download_file_name))

    start_time = datetime.datetime.now()
    try:
        # 多线程并发下载文件
        p.map(ftp_download_func, zip(repeat(host), repeat(username), repeat(password),
                                     repeat(port), download_file_list, temp_file_list))
        # 关闭进程池,不再接受新的进程
        p.close()
        # 主进程阻塞等待子进程的退出
        p.join()
    except Exception as e:
        logger.error(e.message)
        logger.error("文件下载失败!")
        sys.exit(1)
    end_time = datetime.datetime.now()

    logger.info("成功下载的接口文件个数:"+str(len(download_file_list)))
    logger.info("接口文件下载耗时:" + str(end_time - start_time))
    if len(download_file_list) > 0:
        logger.info("正在同步文件...")
        sync_file_list = ftp.sync_file(config_entity)
        logger.info("成功同步的接口文件个数:"+str(len(sync_file_list)))

你可能感兴趣的:(python,ftplib,paramiko,python,开发语言)