Python FTP文件上传自动化:从ftplib基础到生产级脚本实战

发布时间:2026/7/30 4:36:11
Python FTP文件上传自动化:从ftplib基础到生产级脚本实战 1. 项目概述为什么用Python玩转FTP文件上传如果你每天都需要手动把一堆日志、报表或者备份文件传到某个固定的FTP服务器上重复点击、拖拽、等待是不是觉得既枯燥又浪费时间我之前接手过一个数据同步的项目每天凌晨需要把十几G的监控数据从边缘服务器推送到中心分析节点最开始用图形化FTP工具不仅容易出错还经常因为网络波动中断得半夜爬起来重传。后来我决定用Python把这个过程自动化从此“解放双手”还能把上传日志、失败重试、速度监控这些功能都集成进去稳定性和效率直接拉满。这个“Python每日练习之FTP文件上传”项目核心就是利用Python的标准库ftplib或更现代的第三方库编写脚本实现自动化的、可定制的文件传输。它解决的远不止是“上传文件”这个动作而是流程的标准化、自动化和可靠性保障。无论是运维工程师需要定时备份还是数据分析师要同步数据集甚至是开发者搭建简单的文件分发服务都能从这个练习中获得直接可用的解决方案。接下来我会带你从原理到实践一步步拆解如何构建一个健壮的Python FTP上传工具并分享那些只有踩过坑才知道的细节。2. FTP协议核心与Python库选型在动手写代码之前我们得先搞清楚要用的“工具”是什么以及为什么选它。FTP文件传输协议是一个老牌但广泛使用的协议它的工作模式主要分两种主动模式Port和被动模式Pasv。简单理解主动模式下服务器主动连接客户端的数据端口这在客户端位于防火墙或NAT后时容易失败而被动模式下服务器开放一个端口范围等待客户端连接对现代网络环境更友好。我们编程时绝大多数情况都使用被动模式。Python提供了几种方式来操作FTP1. 标准库ftplib这是最直接、无需额外安装的选择。它提供了FTP客户端的基本功能足够完成登录、目录操作、文件上传/下载。from ftplib import FTP ftp FTP(ftp.example.com) ftp.login(userusername, passwdpassword)它的优点是轻量、无需依赖。缺点是API略显老旧对于复杂的错误处理、连接池、断点续传等高级功能需要自己封装并且默认不支持FTP over TLS加密。2. 第三方库paramiko(用于SFTP)注意SFTP虽然是“SSH File Transfer Protocol”和FTP不是一回事但它常被用作更安全的替代方案。paramiko是一个强大的SSHv2库自然支持SFTP。import paramiko transport paramiko.Transport((hostname, 22)) transport.connect(usernameusername, passwordpassword) sftp paramiko.SFTPClient.from_transport(transport)如果你的服务器是SFTP那么paramiko是首选。它支持密钥认证传输加密安全性远高于普通FTP。3. 第三方库ftputil这个库在ftplib基础上进行了高层封装提供了更“Pythonic”的接口比如类似os模块的文件系统操作。import ftputil with ftputil.FTPHost(hostname, username, password) as host: host.upload(local_file, remote_file)它简化了递归上传、目录遍历等操作适合需要高层次抽象的场景。选型建议与实操心得对于“每日练习”和大多数自动化上传场景我强烈建议从ftplib开始。原因有三第一它是标准库环境兼容性100%部署时没有依赖烦恼第二它的基础功能完全满足定时上传、监控等需求学习成本低第三理解了ftplib的底层操作以后用到paramiko或ftputil时会更容易上手。本项目的核心演示也将基于ftplib。但请记住如果传输敏感数据务必使用SFTPparamiko或FTPSFTP over SSLftplib的FTP_TLS类明文传输的FTP在生产环境中是极不安全的。注意很多教程和热词里提到的FileZilla Server、CentOS配置FTP等属于服务器端搭建知识。作为客户端上传脚本的开发者我们通常只需要服务器的地址、端口、用户名、密码以及是否支持被动模式即可无需深究服务端配置除非你需要自建测试环境。3. 基础脚本搭建从零实现单文件上传让我们从一个最简单的脚本开始实现连接FTP服务器并上传一个文件。这是所有复杂功能的地基。3.1 建立连接与登录首先我们需要导入库并建立连接。这里会涉及几个关键参数和异常处理。from ftplib import FTP, error_perm import logging # 配置日志方便追踪运行情况 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) def upload_single_file(host, username, password, local_file_path, remote_file_path, port21): 上传单个文件到FTP服务器 :param host: FTP服务器地址 :param username: 用户名 :param password: 密码 :param local_file_path: 本地文件完整路径 :param remote_file_path: 远程文件路径包含文件名 :param port: 端口默认21 ftp None try: # 1. 实例化FTP对象并连接 logger.info(f正在连接服务器 {host}:{port}...) ftp FTP() # 设置调试级别2为显示所有交互信息生产环境建议设为0 ftp.set_debuglevel(0) # 连接服务器timeout建议设置避免网络不佳时无限等待 ftp.connect(host, port, timeout30) # 2. 登录 logger.info(f正在登录用户 {username}...) ftp.login(username, password) # 登录后可以打印欢迎信息确认连接成功 logger.info(f服务器欢迎信息: {ftp.getwelcome()}) # 3. 设置为被动模式现代网络环境推荐 ftp.set_pasv(True) # 4. 上传文件 logger.info(f开始上传 {local_file_path} - {remote_file_path}) with open(local_file_path, rb) as local_file: # 必须以二进制模式读取 # storbinary 用于上传二进制文件STOR是FTP上传命令 # 回调函数可用于显示上传进度 ftp.storbinary(fSTOR {remote_file_path}, local_file, blocksize8192) logger.info(文件上传成功) except error_perm as e: logger.error(fFTP权限错误: {e}) # 可能是路径不存在、权限不足等 except Exception as e: logger.error(f上传过程中发生未知错误: {e}) finally: # 5. 确保连接被关闭 if ftp: ftp.quit() logger.info(已断开FTP连接。) # 使用示例 if __name__ __main__: # 替换为你自己的服务器信息 upload_single_file( hostyour_ftp_server.com, usernameyour_username, passwordyour_password, local_file_path./daily_report_20231027.csv, remote_file_path/uploads/report.csv )3.2 关键参数与原理剖析ftp.set_pasv(True) 这行代码至关重要。它告诉客户端使用被动模式。在被动模式下数据连接由客户端向服务器发起能够更好地穿透常见的防火墙或NAT设备。如果上传失败并出现[Errno 10060]或[Errno 10061]这类连接超时或拒绝连接的错误首先检查这个设置。blocksize8192 这是传输数据块的大小单位是字节。8KB是一个比较均衡的值。增大它如32768可能提升大文件传输效率但会占用更多内存减小它则可能增加网络交互次数。通常保持默认或8KB即可。二进制模式rb 用rb读取二进制模式打开文件是通用做法。即使用于上传文本文件这样做也能保证文件内容的原样传输避免因操作系统换行符差异如Windows的\r\n和Linux的\n导致文件内容被意外修改。ftplib也提供了storlines()方法用于文本模式上传但除非你明确知道自己在做什么否则一律用storbinary。3.3 第一个实操陷阱远程目录不存在直接使用STOR /uploads/report.csv命令时如果服务器上的/uploads目录不存在FTP服务器会返回550错误文件不可用。因此一个健壮的上传函数需要具备自动创建目录的能力。我们可以写一个辅助函数来确保远程路径存在def ensure_remote_path(ftp, remote_path): 确保FTP服务器上存在指定的目录路径。 :param ftp: 已连接的FTP对象 :param remote_path: 远程目录路径如 /var/www/uploads/2023-10 # 如果路径是文件名包含后缀则分离出目录部分 if . in remote_path.split(/)[-1]: # 简单判断假设最后一段带点的是文件 dir_path /.join(remote_path.split(/)[:-1]) else: dir_path remote_path if not dir_path or dir_path /: return # 逐级创建目录 folders [f for f in dir_path.split(/) if f] # 过滤掉空字符串 current_path for folder in folders: current_path / folder try: ftp.cwd(current_path) # 尝试切换目录 logger.debug(f目录已存在: {current_path}) except error_perm: # 目录不存在创建它 try: ftp.mkd(current_path) logger.info(f创建目录: {current_path}) except error_perm as e: logger.error(f创建目录失败 {current_path}: {e}) raise # 重新抛出异常让上层处理然后在上传主函数中在storbinary之前调用ensure_remote_path(ftp, remote_file_path)。实操心得FTP服务器的响应码是排查问题的金钥匙。200系列是成功500系列是服务器错误400系列是客户端错误。比如550是权限/路径问题530是登录失败。在你的代码里捕获error_perm异常后打印出异常信息里面通常就包含了这些响应码和说明这是你诊断问题的第一步。4. 进阶功能实现打造生产级上传工具一个只能上传单个文件的脚本离“生产级”还差得远。日常任务中我们更常面对的是上传整个文件夹、需要覆盖或跳过已有文件、记录上传日志、控制传输速度等。下面我们来逐一实现。4.1 递归上传整个目录这是最常见的需求之一。思路是深度优先遍历本地目录遇到文件就上传遇到目录就在远程创建对应目录并递归进入。import os def upload_directory(ftp, local_dir, remote_dir): 递归上传本地目录到远程FTP目录 :param ftp: 已连接并登录的FTP对象 :param local_dir: 本地目录路径 :param remote_dir: 远程目标目录路径 # 确保远程根目录存在 ensure_remote_path(ftp, remote_dir) # 遍历本地目录 for item in os.listdir(local_dir): local_path os.path.join(local_dir, item) remote_path remote_dir / item if os.path.isfile(local_path): # 上传文件 logger.info(f上传文件: {local_path} - {remote_path}) with open(local_path, rb) as f: ftp.storbinary(fSTOR {remote_path}, f) elif os.path.isdir(local_path): # 递归上传子目录 logger.info(f进入目录: {local_path}) upload_directory(ftp, local_path, remote_path)4.2 智能文件同步增量上传与覆盖策略每天上传时我们可能只希望上传新增或修改过的文件而不是全部重传。一个简单的增量策略是比较本地文件和远程文件的大小和修改时间如果服务器支持。def should_upload(ftp, local_path, remote_path): 判断是否需要上传文件。 简单策略如果远程文件不存在或本地文件大小与远程不同则上传。 更优策略如果服务器支持MDTM命令可以比较修改时间。 try: # 尝试获取远程文件大小 remote_size ftp.size(remote_path) local_size os.path.getsize(local_path) if remote_size local_size: logger.debug(f文件大小一致跳过: {remote_path}) return False else: logger.info(f文件大小不一致准备覆盖: {remote_path} (本地:{local_size}, 远程:{remote_size})) return True except error_perm: # 远程文件不存在size命令会报错 logger.debug(f远程文件不存在需要上传: {remote_path}) return True # 修改upload_directory函数在上传文件前调用should_upload进行判断需要注意的是ftp.size()方法并非所有FTP服务器都支持取决于是否实现了SIZE命令。更通用的方法是尝试获取文件列表并解析但比较复杂。对于时间戳比较可以使用ftp.sendcmd(MDTM remote_path)获取远程文件时间但同样需要服务器支持。在生产中如果服务器环境可控可以优先使用这些高级命令否则一个保守但可靠的策略是维护一个本地的“已上传文件清单”数据库比如一个JSON文件或SQLite记录每个文件上传时的哈希值下次上传前对比哈希值。4.3 加入进度显示与速度控制上传大文件时给用户一个进度反馈是很好的体验。我们可以通过给storbinary传入一个回调函数来实现。import time def upload_with_progress(ftp, local_file_path, remote_file_path): 带进度显示的上传函数 local_size os.path.getsize(local_file_path) uploaded 0 start_time time.time() def progress_callback(data): nonlocal uploaded chunk_size len(data) uploaded chunk_size percent (uploaded / local_size) * 100 elapsed time.time() - start_time # 计算即时速度KB/s和平均速度 speed chunk_size / 1024 / (time.time() - start_time) if elapsed 0 else 0 avg_speed uploaded / 1024 / elapsed if elapsed 0 else 0 # 使用 \r 实现行内刷新适合命令行输出 print(f\r进度: {percent:.2f}% | {uploaded}/{local_size} bytes | 即时速度: {speed:.2f} KB/s | 平均速度: {avg_speed:.2f} KB/s, end) with open(local_file_path, rb) as f: # 将回调函数传给storbinary ftp.storbinary(fSTOR {remote_file_path}, f, blocksize8192, callbackprogress_callback) print() # 上传完成后换行关于速度控制ftplib本身没有提供限速接口。一个变通的方法是在progress_callback函数中根据计算出的速度如果超过阈值则调用time.sleep()进行微小的延迟。但这会影响回调的实时性且控制不精确。如果限速是硬性需求可能需要考虑使用像paramikoSFTP这样的库或者使用操作系统级的流量控制工具。4.4 连接池与异常重试机制对于需要频繁上传的脚本每次都新建连接开销较大。虽然ftplib没有内置连接池但我们可以自己实现一个简单的连接复用。更关键的是异常重试机制网络传输不稳定偶尔失败是常态。import socket from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type # 使用tenacity库实现优雅的重试需安装pip install tenacity retry( stopstop_after_attempt(3), # 最多重试3次 waitwait_exponential(multiplier1, min2, max10), # 指数退避等待 retryretry_if_exception_type((socket.error, EOFError, error_perm)), # 针对特定异常重试 reraiseTrue # 重试次数用尽后抛出原始异常 ) def robust_upload(ftp, local_path, remote_path): 一个带有重试机制的上传函数 with open(local_path, rb) as f: ftp.storbinary(fSTOR {remote_path}, f) logger.info(f成功上传含重试: {local_path})这里我引入了tenacity库它让重试逻辑的编写变得非常清晰。我们指定了对网络错误socket.error、连接意外中断EOFError和特定FTP权限错误进行重试并采用指数退避策略等待2秒、4秒、8秒...避免在服务器临时故障时雪上加霜。5. 安全加固与配置管理将FTP密码硬编码在脚本里是极其危险的。我们需要更安全的方式来管理配置。5.1 使用配置文件与环境变量推荐使用配置文件如config.ini或config.yaml来存储服务器信息并将密码等敏感信息放在环境变量中。# config.yaml 示例 # ftp: # host: your_server.com # port: 21 # username: data_uploader # # password 从环境变量读取 # remote_base_dir: /incoming/data import yaml import os def load_config(): with open(config.yaml, r) as f: config yaml.safe_load(f) # 从环境变量获取密码 config[ftp][password] os.environ.get(FTP_UPLOADER_PASSWORD) if not config[ftp][password]: raise ValueError(未在环境变量中找到 FTP_UPLOADER_PASSWORD) return config然后在运行脚本前在终端设置环境变量# Linux/macOS export FTP_UPLOADER_PASSWORDyour_strong_password_here python upload_script.py # Windows (Command Prompt) set FTP_UPLOADER_PASSWORDyour_strong_password_here python upload_script.py # Windows (PowerShell) $env:FTP_UPLOADER_PASSWORDyour_strong_password_here python upload_script.py5.2 启用FTP over TLS (FTPS)如果服务器支持务必使用加密连接。ftplib提供了FTP_TLS类。from ftplib import FTP_TLS ftps FTP_TLS() ftps.connect(host, port) ftps.login(username, password) # 在登录后加密数据通道 ftps.prot_p()prot_p()命令将后续的数据传输连接也升级为加密连接。这是关键一步否则只有控制连接是加密的数据仍在明文传输。5.3 日志记录与监控一个自动化脚本必须有完善的日志否则出问题时无从查起。我们可以配置日志输出到文件并设置轮转避免日志文件无限膨胀。import logging from logging.handlers import RotatingFileHandler def setup_logging(): logger logging.getLogger(ftp_uploader) logger.setLevel(logging.INFO) # 控制台输出 ch logging.StreamHandler() ch_formatter logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - %(message)s) ch.setFormatter(ch_formatter) logger.addHandler(ch) # 文件输出最大10MB保留5个备份 fh RotatingFileHandler(ftp_upload.log, maxBytes10*1024*1024, backupCount5) fh_formatter logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s) fh.setFormatter(fh_formatter) logger.addHandler(fh) return logger这样每天的运行记录、成功上传的文件、遇到的错误都会清晰地记录在日志文件中便于后续审计和排查。6. 实战整合构建一个完整的每日上传任务现在我们把所有模块组合起来形成一个可以投入日常使用的脚本。假设场景是每天凌晨1点将本地/data/daily_backup目录下的所有新文件同步到FTP服务器的/backups目录下并按日期创建子文件夹。#!/usr/bin/env python3 每日FTP备份上传脚本 import os import sys import yaml import logging from datetime import datetime from ftplib import FTP, error_perm, FTP_TLS from logging.handlers import RotatingFileHandler from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type # 配置与日志初始化 def init(): 初始化配置和日志 # 加载配置 with open(config.yaml, r) as f: config yaml.safe_load(f) config[ftp][password] os.environ.get(FTP_PASSWORD) # 设置日志 logger logging.getLogger(daily_ftp_backup) logger.setLevel(logging.INFO) formatter logging.Formatter(%(asctime)s - %(levelname)s - %(message)s) # 控制台处理器 ch logging.StreamHandler() ch.setFormatter(formatter) logger.addHandler(ch) # 文件处理器轮转 fh RotatingFileHandler(backup_upload.log, maxBytes5*1024*1024, backupCount7) fh.setFormatter(formatter) logger.addHandler(fh) return config, logger # 核心工具函数 def ensure_remote_path(ftp, remote_path, logger): 确保远程路径存在 # ... (实现同前略) ... pass retry(stopstop_after_attempt(3), waitwait_exponential(min2, max10)) def upload_file_with_retry(ftp, local_path, remote_path, logger): 带重试的文件上传 try: with open(local_path, rb) as f: ftp.storbinary(fSTOR {remote_path}, f, blocksize8192) logger.info(f上传成功: {os.path.basename(local_path)}) return True except Exception as e: logger.warning(f上传失败 {local_path}: {e}准备重试...) raise def sync_directory(ftp, local_base, remote_base, logger): 同步目录增量逻辑 # ... (整合了目录遍历、should_upload判断、ensure_remote_path的完整实现) ... pass # 主函数 def main(): config, logger init() ftp_config config[ftp] local_backup_dir config[local][backup_dir] # 根据配置决定是否使用TLS if ftp_config.get(use_tls, False): ftp FTP_TLS() ftp.connect(ftp_config[host], ftp_config.get(port, 21)) ftp.login(ftp_config[username], ftp_config[password]) ftp.prot_p() # 加密数据通道 logger.info(已建立FTPS加密连接。) else: ftp FTP() ftp.connect(ftp_config[host], ftp_config.get(port, 21)) ftp.login(ftp_config[username], ftp_config[password]) logger.info(已建立FTP普通连接。) ftp.set_pasv(True) try: # 创建以日期命名的远程子目录例如 /backups/2023-10-27 today_str datetime.now().strftime(%Y-%m-%d) remote_today_dir f{ftp_config[remote_base_dir]}/{today_str} ensure_remote_path(ftp, remote_today_dir, logger) # 执行同步 logger.info(f开始同步目录: {local_backup_dir} - {remote_today_dir}) sync_directory(ftp, local_backup_dir, remote_today_dir, logger) logger.info(每日备份同步完成。) except Exception as e: logger.error(f同步过程发生严重错误: {e}, exc_infoTrue) sys.exit(1) # 返回非零退出码便于外部监控如cron job捕获失败 finally: ftp.quit() logger.info(连接已关闭。) if __name__ __main__: main()6.1 如何部署为每日任务在Linux服务器上最常用的方式是使用cron定时任务。将你的脚本例如daily_ftp_backup.py、配置文件 (config.yaml) 放在一个合适的目录如/opt/scripts/ftp_uploader/。设置环境变量。可以在脚本开头通过os.environ设置但更安全的方法是在cron任务中设置或者在系统级如/etc/environment或用户级~/.bashrc配置。对于cron可以在任务定义中直接设置# 编辑当前用户的cron任务 crontab -e添加一行例如每天凌晨1点30分运行30 1 * * * FTP_PASSWORDyour_password /usr/bin/python3 /opt/scripts/ftp_uploader/daily_ftp_backup.py /opt/scripts/ftp_uploader/cron.log 21这里 ... 21将脚本的标准输出和错误输出都重定向到日志文件方便查看运行结果。在Windows上可以使用“任务计划程序”来达到同样的效果。7. 常见问题排查与性能优化技巧即使脚本写得再完善在实际运行中还是会遇到各种问题。这里记录一些我踩过的坑和对应的解决方案。7.1 连接与登录问题问题现象可能原因排查步骤与解决方案连接超时 (socket.timeout)服务器地址/端口错误、防火墙阻断、网络不通1. 用ping或telnet命令测试服务器IP和端口默认21是否可达。2. 检查客户端防火墙是否放行了出站连接。3. 确认服务器防火墙是否允许来自你IP的入站连接。登录失败 (530 Login incorrect)用户名或密码错误、账户被禁用、IP被限制1. 使用FileZilla等图形化工具使用相同凭证测试。2. 检查密码是否包含特殊字符在脚本中可能需要转义或使用原始字符串。3. 联系服务器管理员确认账户状态和IP白名单。连接被拒绝 (ConnectionRefusedError)服务器FTP服务未启动、端口被占用1. 确认服务器端FTP服务如vsftpd, proftpd正在运行。2. 确认服务器监听的是你连接的端口。7.2 文件传输问题问题现象可能原因排查步骤与解决方案550权限错误上传/创建目录远程目录不存在、用户对该路径无写权限1. 使用ensure_remote_path函数确保目录存在。2. 用FTP客户端登录手动尝试在目标路径创建文件确认权限。3. 服务器端可能配置了chroot将用户限制在其家目录下请使用相对路径或咨询管理员。上传文件损坏或大小不一致传输模式错误、网络干扰、文件未以二进制模式打开1.始终使用rb模式和storbinary这是最重要的准则。2. 检查网络稳定性考虑加入重试和校验机制如下文。3. 上传后可以用ftp.size()核对远程文件大小。上传大文件中途失败网络会话超时、防火墙中断空闲连接、服务器配置限制1. 尝试减小blocksize增加网络交互频率避免单次传输数据包过大。2. 在服务器端或客户端调整TCP keepalive设置。3. 实现断点续传记录已传输的字节数失败后使用ftp.storbinary的rest参数从断点继续上传需要服务器支持REST命令。7.3 性能优化建议并发上传如果需要上传大量小文件顺序上传会非常慢。可以使用Python的concurrent.futures模块实现多线程上传。但要注意FTP协议本身并非为高并发设计服务器可能对连接数有限制过度并发可能导致连接被拒绝。建议先测试服务器能承受的并发连接数如2-5个并确保每个线程使用独立的FTP连接。from concurrent.futures import ThreadPoolExecutor, as_completed def upload_worker(file_info): local_path, remote_path file_info # 每个worker创建自己的FTP连接 with FTP() as ftp_conn: ftp_conn.connect(host, port) ftp_conn.login(user, passwd) ftp_conn.set_pasv(True) upload_single_file(ftp_conn, local_path, remote_path) file_list [...] # 需要上传的文件列表 [(local1, remote1), ...] with ThreadPoolExecutor(max_workers3) as executor: futures {executor.submit(upload_worker, f): f for f in file_list} for future in as_completed(futures): try: future.result() except Exception as e: logger.error(f文件上传失败: {e})压缩后上传如果上传的文件如日志、文本数据压缩率很高可以先在本地用gzip或zipfile库压缩上传压缩包然后在服务器端解压如果服务器有相应环境。这能极大减少网络传输时间。校验文件完整性对于关键数据上传后计算本地文件和远程文件的MD5或SHA256哈希值进行比对是最可靠的。虽然FTP协议不直接支持但可以上传完成后再下载一个校验文件或者如果服务器允许执行命令可以上传一个计算哈希的脚本并触发执行。更常见的做法是在上传完成后记录文件的哈希值到本地日志或数据库作为后续数据一致性的依据。7.4 一个真实的避坑案例编码问题我曾经遇到一个诡异的问题脚本在Windows开发机上运行正常放到Linux服务器上运行时遇到中文目录名就上传失败报550错误。原因是Linux系统默认编码是UTF-8而有些老旧的FTP服务器尤其是Windows Server上的可能默认使用不同的编码如GBK。ftplib在Python 3中默认使用UTF-8编码文件名。解决方案是在连接后主动设置服务器所需的编码ftp FTP() ftp.connect(host, port) ftp.login(user, passwd) # 尝试使用GBK编码根据服务器调整 ftp.encoding gbk或者更激进一点在遇到编码错误时尝试对路径进行编码转换remote_path_gbk remote_path_utf8.encode(gbk, errorsignore).decode(gbk)这个问题提醒我们在处理跨平台、跨环境的文件传输时字符编码是一个必须考虑的细节。