ftplib 可实现基础 FTP 同步,但需手动比对文件修改时间或大小;优先用 MLSD 获取时间戳,注意时区与精度问题,下载前确保本地路径存在,上传时用 .tmp 临时名+rename 实现原子性。

用 ftplib 连接并判断文件是否需要同步
Python 标准库的 ftplib 足够完成基础 FTP 同步,但不自动比对文件差异——它不会告诉你远程文件是否更新了。你得自己比对修改时间或大小。常见错误是直接覆盖本地文件,结果把新改的本地内容冲掉了。
关键点:FTP 服务器通常不支持精确的毫秒级 mtime,MLSD 命令返回的时间精度常为秒级,且时区可能不一致。别依赖 STOR 后立刻 MDTM 拿到准确时间。
- 优先用
MLSD(而非旧式LIST)获取带时间戳的目录列表,它格式规范、易解析 - 若服务器不支持
MLSD,降级用LIST+ 正则提取时间字段,但要注意不同 FTP 服务(vsftpd/proftpd/FileZilla)输出格式差异 - 本地文件用
os.path.getmtime(),但注意系统时区与 FTP 服务器时区偏差可能导致误判;稳妥做法是加上 ±2 秒容差
下载时跳过未变更的远程文件
同步下载方向(远程 → 本地)时,典型错误是每次无条件 retrbinary,浪费带宽且拖慢流程。实际只需在满足任一条件时才下载:本地不存在、远程 mtime > 本地 mtime + 容差、大小不一致。
注意 ftplib 的 retrbinary 不会自动创建父目录,如果远程路径含多层子目录(如 /data/reports/2024/06/),而本地 reports/ 不存在,会抛出 OSError: No such file or directory。
立即学习“Python免费学习笔记(深入)”;
- 下载前用
os.makedirs(os.path.dirname(local_path), exist_ok=True)确保路径存在 - 用
ftp.size(remote_path)获取远程文件大小(部分匿名 FTP 会拒绝此命令,需捕获error_perm) - 避免在循环里反复调用
ftp.cwd()切换目录,改用完整路径传给retrbinary
上传时防止覆盖正在被写的远程文件
上传(本地 → 远程)更危险:如果远程文件正被其他进程写入(比如日志轮转中),直接 storbinary 可能截断它。FTP 协议本身没有“原子重命名”或“临时文件上传后切换”机制,得靠命名约定模拟。
常见翻车点:用 storbinary 上传同名文件时,FTP 服务可能先清空原文件再写入,中间状态导致其他客户端读到损坏内容。
- 上传时先传到带
.tmp后缀的临时名(如report.csv.tmp) - 上传成功后,用
ftp.rename('report.csv.tmp', 'report.csv')原子替换(前提是 FTP 服务支持RENAME,绝大多数都支持) - 务必检查
rename返回值或捕获异常;失败时手动清理.tmp文件,否则残留垃圾 - 不要依赖
pwd()或cwd()的当前路径做相对路径操作,显式传绝对路径更稳
处理被动模式、超时和连接中断
默认主动模式(PORT)在 NAT 或防火墙后大概率失败,必须显式启用被动模式(PASV)。但被动模式下,FTP 服务器返回的 IP 和端口可能仍是内网地址(比如 192.168.1.100),导致客户端连不上——这不是 Python 的错,是服务器配置问题。
另一个高频问题是连接卡住:比如服务器响应慢,或中间网络抖动,ftp.connect() 或 retrbinary() 无限等待。
- 初始化时传
timeout=30:FTP(host, user, passwd, timeout=30) - 连接后立即调用
ftp.set_pasv(True)(这是默认值,但显式写出更清晰) - 每个 I/O 操作(
retrbinary,storbinary,mlsd)都要包在try/except ftplib.error_temp中,这类错误通常可重试 - 别复用一个
FTP实例跑完整个同步任务;大任务建议每同步一个目录就新建连接,避免长连接失效
真正麻烦的是 FTP 服务器返回的时间格式不统一、被动模式地址不可达、以及没有真正的文件锁机制——这些没法靠 Python 代码绕过,得提前确认服务端能力或换 SFTP。


















