Python使用ftplib实现隐式FTPS时上传文件失败(仅首文件可能成功,其余报错)
Python使用ftplib实现隐式FTPS时上传文件失败(仅首文件可能成功,其余报错)
嘿,我看了你的问题和代码,结合FileZilla的提示,你遇到的其实是隐式FTPS下ftplib的连接管理问题,尤其是被动模式下服务器返回不可路由IP导致的多文件上传失败。让我一步步给你解决思路和修改方案:
问题根源分析
首先,你的下载功能正常,但上传多文件时失败,结合FileZilla的提示“Server sent passive reply with unroutable address”,核心问题有两个:
- 被动模式IP不可路由:服务器返回的是内部私有IP,你的客户端无法直接访问,FileZilla会自动替换成服务器的公网主机名,但你的脚本虽然设置了
ftp.host = host,但手动包装socket的方式可能让ftplib没正确应用这个设置。 - ftplib隐式FTPS连接的状态管理缺陷:你手动创建socket并包装SSL的方式,虽然能建立初始连接,但ftplib内部的一些状态(比如数据连接的SSL会话)没有被正确初始化,导致多次上传后连接异常,出现SSL EOF或426传输失败错误。
解决方案:重构连接逻辑+优化上传循环
下面是具体的修改步骤,直接对应你的代码:
1. 替换手动socket包装为标准隐式FTPS连接
放弃原来手动创建socket、wrap、赋值给ftp.sock的复杂操作,用ftplib官方支持的方式连接隐式FTPS(Python 3.9+支持implicit=True参数,非常方便):
# --- Ensure local download folder exists --- os.makedirs(local_download_dir, exist_ok=True) # --- Create SSL context for Implicit FTPS --- context = ssl.create_default_context() context.check_hostname = False context.verify_mode = ssl.CERT_NONE # --- 标准隐式FTPS连接(Python 3.9+) --- try: ftp = ftplib.FTP_TLS(host=host, port=port, context=context, implicit=True) ftp.login(username, password) ftp.set_pasv(True) ftp.host = host # 强制用服务器公网主机名替换不可路由的内部IP(和FileZilla一样) ftp.prot_p() # 启用数据连接的SSL加密,确保上传下载都用SSL print("✅ 成功连接到FTPS服务器") except Exception as e: print(f"❌ 连接服务器失败: {e}") exit(1)
如果你的Python版本是3.8或更早,没有implicit=True参数,就用下面的替代方案:
# Python 3.8及以下的隐式FTPS连接 try: ftp = ftplib.FTP() ftp.connect(host, port) # 立即将控制连接包装为SSL ftp.sock = context.wrap_socket(ftp.sock, server_hostname=host) ftp.login(username, password) ftp.set_pasv(True) ftp.host = host ftp.prot_p() print("✅ 成功连接到FTPS服务器") except Exception as e: print(f"❌ 连接服务器失败: {e}") exit(1)
2. 优化上传循环,保持连接活跃
原来的上传循环中,多次传输后可能出现连接超时或SSL会话中断,我们可以在每次上传后发送NOOP命令保持连接活跃,同时增加错误重试逻辑:
import time # 记得在顶部导入time模块 # ========== UPLOAD SECTION ========== print(f"\n Switching to upload directory: {upload_to_dir}") try: ftp.cwd(upload_to_dir) except ftplib.error_perm as e: print(f"❌ 无法切换到上传目录: {e}") else: local_files = [f for f in os.listdir(local_upload_dir) if os.path.isfile(os.path.join(local_upload_dir, f))] print(f" 找到 {len(local_files)} 个待上传文件...\n") for idx, fname in enumerate(local_files): local_path = os.path.join(local_upload_dir, fname) if not os.path.isfile(local_path): print(f"⚠ 跳过非文件: {fname}") continue safe_name = fname.replace(" ", "_").replace(":", "-") print(f"⬆ 正在上传 ({idx+1}/{len(local_files)}): {safe_name}") upload_success = False retry_count = 2 # 重试2次 while retry_count > 0 and not upload_success: try: with open(local_path, 'rb') as f: ftp.storbinary(f"STOR {safe_name}", f, blocksize=BLOCKSIZE) # 上传成功后发送NOOP保持连接活跃 ftp.voidcmd("NOOP") print(f"✅ 上传完成: {safe_name}") upload_success = True except ftplib.error_temp as e: retry_count -= 1 print(f"⚠ 临时错误,剩余重试次数 {retry_count}: {e}") time.sleep(1) # 重试前短暂休眠 except (ftplib.error_perm, ssl.SSLEOFError) as e: print(f"❌ 上传失败 {safe_name}: {e}") break # 权限或SSL错误直接跳出重试 except Exception as e: print(f"❌ 未知错误: {e}") # 未知错误尝试重新连接 ftp.close() # 重新建立连接 ftp = ftplib.FTP_TLS(host=host, port=port, context=context, implicit=True) ftp.login(username, password) ftp.set_pasv(True) ftp.host = host ftp.prot_p() ftp.cwd(upload_to_dir) retry_count -= 1 print(f"🔄 重新连接后重试,剩余次数 {retry_count}")
3. 额外注意事项
- 如果你不需要跳过证书验证,建议去掉
context.check_hostname = False和context.verify_mode = ssl.CERT_NONE,换成正确的证书校验,提升安全性 - 确保你的Python版本是3.6以上,
ssl.create_default_context()是3.6引入的 - 上传前可以先检查服务器上是否已存在同名文件,避免覆盖(如果业务需要的话)
为什么原来的代码不行?
你原来手动创建socket并包装SSL的方式,虽然能建立初始控制连接,但ftplib内部的很多状态(比如数据连接的SSL上下文、被动模式的IP处理逻辑)没有被正确初始化,导致第一次上传后,数据连接的SSL会话没有正确关闭或重置,后续的STOR命令无法建立有效连接,从而出现426或SSL EOF错误。而改用标准的FTPS连接方式,ftplib会自动处理这些细节,和FileZilla、FluentFTP的逻辑一致。
内容来源于stack exchange
相关产品推荐
相关产品推荐

