You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python使用ftplib实现隐式FTPS时上传文件失败(仅首文件可能成功,其余报错)

Python使用ftplib实现隐式FTPS时上传文件失败(仅首文件可能成功,其余报错)

嘿,我看了你的问题和代码,结合FileZilla的提示,你遇到的其实是隐式FTPS下ftplib的连接管理问题,尤其是被动模式下服务器返回不可路由IP导致的多文件上传失败。让我一步步给你解决思路和修改方案:

问题根源分析

首先,你的下载功能正常,但上传多文件时失败,结合FileZilla的提示“Server sent passive reply with unroutable address”,核心问题有两个:

  1. 被动模式IP不可路由:服务器返回的是内部私有IP,你的客户端无法直接访问,FileZilla会自动替换成服务器的公网主机名,但你的脚本虽然设置了ftp.host = host,但手动包装socket的方式可能让ftplib没正确应用这个设置。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 12:43:07