news 2026/10/20 3:59:41

Python实现FTP大文件上传进度条与断点续传实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python实现FTP大文件上传进度条与断点续传实战

简介:FTP上传实例(带进度条)是一套基于C#窗体应用的FTP客户端工程,面向需要在桌面程序中实现文件上传可视化反馈的.NET开发者。工程围绕FTP交互流程展开:通过套接字建立控制连接,使用CWD指令切换远端目录、STOR指令发送数据,并依据已上传与总字节数的比值实时计算传输百分比,驱动进度条控件更新,使上传状态一目了然。压缩包共35个文件,核心是12个C#源码,覆盖主窗体、辅助界面与上传逻辑类,配套界面资源文件、可执行程序、调试符号及工程配置,整体仅78KB,轻量便于研读。目前已有868人学习下载。除直接运行体验外,工程还封装了可复用的FTP帮助类,提炼出建立连接、身份验证、切换目录、上传、断开连接等方法。配合错误处理与分块传输的思路,读者能快速掌握带进度反馈的上传模块设计,并迁移到实际项目中使用。

1. 别让 FTP 上传变成“黑匣子”:进度条是刚需

做后台系统时接了一个临时需求:某项目组要将本地生成的离线数据包传到远端存储,文件小则几百 MB,大则 2-3 GB,网络环境还是跨机房专线。最开始用的是最基础的 FTP 上传命令,传大文件时控制台一片空白,运维同事问“传了多少了”,我只能回答“不知道,要么等,要么断”。这种黑匣子体验在 FTP 文件上传下载场景里太常见了。后来我把这套代升级成带进度条、支持断点续传和校验的完整实例,才发现问题不在 FTP 本身,而在实现方式。这篇就把这套实例的选型、核心代码、参数调整和踩过的坑完整拆给你看,适合做桌面工具、运维脚本或内部系统的同学直接抄作业。


2. 先理解 FTP 上传的底层模型:控制连接、数据连接与进度来源

2.1 FTP 是双通道协议,进度条要挂在数据连接上

FTP 与 HTTP 最大的不同是它使用两个独立的连接:控制连接(默认 21 端口)负责发送命令,数据连接负责传输文件内容。上传时客户端发送 STOR 命令,服务端会通过数据连接接收字节流。这个过程中,控制连接上只能收到服务端返回的状态码,比如 150(准备传输)、226(传输完成),拿不到字节级别的进度。

这就解释了一个现象:很多初学实现者在 STOR 命令执行期间去读控制连接的返回数据,结果线程卡死不更新;或者有人用socket.recv去读文件流,把 FTP 的数据连接当普通 Socket 用,导致进度和文件内容串了。进度条要实现,必须有一个独立的字节计数点,常见的做法是把本地文件对象包装一层,每次read()后累加已读字节数,再触发回调函数刷新进度。这个方案不依赖服务端任何扩展命令,兼容性最好。

2.2 主动模式与被动模式:进度感知迥异

模式数据连接建立方向常见问题适用场景
主动 PORT服务端主动连接客户端随机端口客户端在防火墙/NAT 后时连接失败服务端与客户端同内网
被动 PASV客户端连接服务端指定端口服务端防火墙未放行数据端口客户端跨网络、有防火墙

我一般默认开启被动模式(set_pasv(True)),因为绝大多数跨机房传输场景下,客户端出网比服务端回连更顺畅。有一点要注意:被动模式下服务端返回的 IP 可能是内网地址,如果你的客户端也在内网没问题;如果是公网传输,要在服务端配置主动对外 IP。遇到连接超时先检查的是这个,不是代码。

2.3 进度条实现思路:本地文件对比与事件回调

一个可靠的进度计算方法是:已上传大小 / 本地文件总大小。本地文件总大小用os.path.getsize()获取,已上传大小统计数据连接上成功发送的字节数。这里有一个隐蔽问题:ftplib的storbinary()内部会调用transfercmd(),后者返回数据连接的 socket 对象。你可以直接用这个 socket 发送数据,也可以传入一个文件对象让storbinary内部读取。无论哪种方式,包装文件对象都是一个干净且跨版本的方案。

class ProgressFile: """包装文件对象,统计已读取字节数""" def __init__(self, file_obj, total_size, callback): self.file_obj = file_obj self.total_size = total_size self.callback = callback self.read_bytes = 0 def read(self, size=-1): chunk = self.file_obj.read(size) self.read_bytes += len(chunk) self.callback(self.read_bytes, self.total_size) return chunk def seek(self, offset, whence=0): self.file_obj.seek(offset, whence) self.read_bytes = offset # seek 后重置计数基准

上面这段代码里,read()方法是进度计数的核心。每读一块数据,read_bytes就增加对应字节数,然后回调把当前值和总量传出去。seek()方法是为断点续传准备的:续传时先定位到本地文件的偏移位置,再把read_bytes重置,否则进度会从 0 重新算,显示就不准了。


3. 落地一个带进度条的上传器:核心代码与参数说明

3.1 建立连接:超时、UTF-8 与主动/被动开关

写上传器第一步是把连接建稳。ftplib.FTP()支持传 host、user、passwd,但我建议用空构造后手动连接,方便控制异常分支。

import ftplib import os def connect_ftp(host, port, user, password, timeout=30): ftp = ftplib.FTP() ftp.connect(host, port, timeout=timeout) ftp.login(user, password) ftp.set_pasv(True) ftp.encoding = 'utf-8' return ftp

这里timeout=30是控制连接的超时时间,不是数据传输的超时。很多人在上传大文件时发现连接被断开,就是以为这个超时覆盖了整个传输过程。实际操作中,ftplib底层 socket 在传输期间会自己处理阻塞,但如果你在上传前设置了过短的 socket 超时,传输半途可能被异常掐断。我一般做法:连接时用 30 秒,上传前把 socket 超时改成None(阻塞模式)。

ftp.sock.settimeout(None)

这一句很重要。如果不做这一步,遇到网络抖动时recv()可能抛超时异常,导致上传中断。

3.2 上传方法与进度回调:storbinary 和 transfercmd 的差异

storbinary(cmd, fp, blocksize=8192)是ftplib提供的高层上传接口,传入文件对象即可。由于我们传入的是ProgressFile包装对象,storbinary内部每次调用fp.read(blocksize),自然触发进度回调。这里的blocksize参数直接影响进度条的刷新频率和传输效率。8192 是默认值,但在高速局域网中我习惯调到 65536,减少 read 调用次数,提高吞吐。

def upload_file(ftp, local_path, remote_path, blocksize=8192): total_size = os.path.getsize(local_path) with open(local_path, 'rb') as f: progress_file = ProgressFile(f, total_size, progress_callback) ftp.storbinary(f'STOR {remote_path}', progress_file, blocksize=blocksize)

progress_callback是一个函数引用,签名是progress_callback(read_bytes, total_bytes)。在回调里可以计算百分比、刷新终端、写日志。

如果你的需求是在上传过程中同时做别的事情,比如暂停、取消,高层接口storbinary就不够灵活,需要用transfercmd()拿到底层 socket 自己控制传输循环。

def upload_socket(ftp, local_path, remote_path, blocksize=65536): total_size = os.path.getsize(local_path) with open(local_path, 'rb') as f: ftp.voidcmd('TYPE I') conn = ftp.transfercmd(f'STOR {remote_path}') while True: chunk = f.read(blocksize) if not chunk: break conn.sendall(chunk) # 这里可以加自定义逻辑,比如检查暂停标志 conn.close() ftp.voidresp()

3.3 完整脚本示例:控制台进度条与日志

把上面的部分组合起来,就是一个完整可运行的实例。这个脚本支持上传单个文件,终端实时显示百分比。

import ftplib import os import sys def progress_callback(read_bytes, total_bytes): if total_bytes == 0: return percent = read_bytes * 100 / total_bytes bar_len = 40 filled = int(bar_len * read_bytes / total_bytes) bar = '=' * filled + '-' * (bar_len - filled) sys.stdout.write(f'\r[{bar}] {percent:.1f}% ({read_bytes}/{total_bytes})') sys.stdout.flush() def upload_with_progress(host, port, user, password, local_path, remote_path): ftp = ftplib.FTP() ftp.connect(host, port, timeout=30) ftp.login(user, password) ftp.set_pasv(True) ftp.encoding = 'utf-8' ftp.sock.settimeout(None) total_size = os.path.getsize(local_path) print(f'本地文件大小: {total_size} 字节') with open(local_path, 'rb') as f: progress_file = ProgressFile(f, total_size, progress_callback) ftp.storbinary(f'STOR {remote_path}', progress_file, blocksize=65536) print(f'\n上传完成: {remote_path}') ftp.quit() if __name__ == '__main__': upload_with_progress('192.168.1.10', 21, 'ftpuser', 'ftppass', './data.bin', '/backup/data.bin')

代码逻辑不复杂:连接并登录、包装文件对象、调用storbinary。参数上唯一要重点关注的是encoding='utf-8',很多 FTP 服务器默认是 Latin-1 编码,中文文件名会乱码。这个设置在 Python 3.9+ 的ftplib里是默认值,但如果你用的是旧版本,必须显式指定。


4. 让上传可中断可恢复:断点续传与校验

4.1 REST 命令与断点续传的实现

大文件传到一半网络断了,重新传一遍成本很高。FTP 协议里有 REST 命令,可以指定从文件的哪个偏移量开始继续传输。实现思路是:先查本地文件大小,再通知服务端REST offset,然后从剩余部分开始STOR。

def upload_resume(ftp, local_path, remote_path): total_size = os.path.getsize(local_path) # 尝试获取远端文件大小,用于判断是否已传过一部分 try: remote_size = ftp.size(remote_path) except ftplib.error_perm: remote_size = 0 if remote_size >= total_size: print('远端文件已存在且完整,跳过上传') return with open(local_path, 'rb') as f: if remote_size > 0: f.seek(remote_size) ftp.sendcmd(f'REST {remote_size}') print(f'从偏移量 {remote_size} 续传') progress_file = ProgressFile(f, total_size, progress_callback) ftp.storbinary(f'STOR {remote_path}', progress_file, blocksize=65536)

这段代码里最关键的是REST命令必须在STOR之前发送,且只对STOR生效。有些服务器对REST的偏移量有块对齐要求,比如要求必须是 512 的倍数,此时续传后会多出几个字节或缺失几个字节。遇到这种情况,需要先做一次探测:用REST 0和REST 1看服务器是否正常返回。如果偏移量导致服务器返回 504 或 350 的变体,只能放弃续传,改为全量重传。这是 FTP 协议本身的兼容性限制,不是你代码的 bug。

4.2 MD5 校验脚本:确认字节真正一致

上传完成后,服务端返回 226 只能代表数据接收完毕,不能代表字节完全一致。传输过程中如果出现网络设备缓存错误、磁盘写入异常,文件可能已损坏。常见的校验方式是比对 MD5。

import hashlib def local_md5(file_path, chunk_size=8192): md5 = hashlib.md5() with open(file_path, 'rb') as f: while True: chunk = f.read(chunk_size) if not chunk: break md5.update(chunk) return md5.hexdigest()

远端 MD5 的获取分两种情况。第一种是服务器支持MD5命令,直接发送MD5 <remote_path>;第二种是通过下载方式比对,开销较大。如果你的 FTP 服务器是 IoT 设备或嵌入式系统,很可能不支持 MD5 命令,此时我会将local_md5的值连同文件大小一起写入一个 .md5 文件,随上传一起传输,作为业务层的校验依据。这种方式不依赖服务器能力,适合内网工具链。


5. 实战避坑:五个必须记住的问题

5.1 现象:传到 99% 卡住,随后报错连接超时

原因:storbinary的blocksize设置过大,加上网络带宽不足,单次send()到 socket 缓冲区阻塞,而底层 socket 超时被设置成了较短的固定值(比如 10 秒)。大文件传输本身需要很长时间,socket 在空闲等待响应时触发了超时。

解决:在storbinary调用前执行ftp.sock.settimeout(None)。续传场景下还要确保REST命令发送后,socket 超时同样被重置。

5.2 现象:进度条跳变,从 40% 突然变成 100%

原因:ProgressFile.seek()被storbinary内部调用(例如重试机制),但read_bytes没有同步重置,导致计数错乱。或者使用了storbinary的rest参数,但本地文件对象没有先 seek 到对应偏移。

解决:重写seek()方法,把read_bytes设置为传入的offset:

def seek(self, offset, whence=0): self.file_obj.seek(offset, whence) if whence == 0: self.read_bytes = offset

这样无论哪种方式调用 seek,进度计数始终与文件偏移保持一致。

5.3 现象:中文文件名上传后变成乱码或问号

原因:ftplib默认编码为 Latin-1(Python 3.9 前),中文无法正确编码。部分服务器不支持 UTF-8,即使客户端发送 UTF-8 文件名也无法存储。

解决:客户端设置ftp.encoding = 'utf-8'。如果服务器不支持 UTF-8,需要与服务器管理员确认字符集,无法调整的话,在文件名中使用拼音或英文缩写是最省事的解法。我在某图像处理 Demo 里就遇到过这种问题,最后是运维在服务端开了 UTF-8 支持才解决。

5.4 现象:上传的 zip 包解压失败,文件字节数一致但内容损坏

原因:FTP 有 ASCII 和二进制两种传输模式。ASCII 模式会在传输时转换换行符(\r\n与\n互转),对二进制文件造成不可逆破坏。storbinary内部会自动设置二进制模式,但如果你的代码使用了retrlines或stor字符串方式,可能误入 ASCII 模式。

解决:统一使用storbinary和retrbinary,不要用storlines处理非文本文件。检查传输模式可以发送TYPE I命令确认。

5.5 现象:上传大文件到一半,服务器返回 450 或 550

原因:多数情况是磁盘空间不足或目标目录权限不够。但也有一种情况被忽视:服务端的传输超时设置较短,数据连接空闲超过阈值后服务端主动断开。默认timeout参数只覆盖控制连接,数据连接超时由服务器决定。

解决:在上传前检查远端剩余空间(SITE STAT命令或文件系统脚本)。大文件传输过程中,代码层面没有保活机制,唯一的降险策略是分块上传(把大文件切成多个片段,逐个上传),而不是尝试一次STOR到底。这个思路是通用的,与具体脚本语言无关。


6. 上传后的验证习惯:用传输时间与字节数双重核对

上传完成后不能直接收工。我习惯写一个验证脚本,同时输出两个指标:实际传输时间、远端文件大小与本地文件大小的比对结果。这个习惯救过我多次。

import time start_time = time.time() upload_with_progress(host, port, user, password, local_path, remote_path) elapsed = time.time() - start_time remote_size = ftp.size(remote_path) local_size = os.path.getsize(local_path) print(f'传输耗时: {elapsed:.2f} 秒') print(f'平均速度: {local_size / elapsed / 1024:.2f} KB/s') print(f'大小比对: 本地 {local_size} / 远端 {remote_size}')

传输时间和平均速度写进日志,不只是给人看,还能用于建立基线。同一份文件后续再传时,如果耗时显著偏离基线,我会先检查链路质量,而不是盲目信任状态码。从那次 FTP 黑匣子事件之后,我每次写完上传工具都会强制走一遍“清单核对”:连接超时、编码、二进制模式、进度计数、续传偏移、大小比对。整个过程不复杂,但每一步都能拦住一类线上事故。希望这篇实例也能帮你把这些弯路一次走完。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 3:59:37

AI报告审核实战:逐个击破汽车零部件与整车检测报告难题

我需要直接开始创作&#xff0c;基于提供的标题生成完整博文&#xff0c;不要附加任何元说明。我注意到项目正文、关键词、摘要描述为空&#xff0c;因此我将完全围绕标题"IA-check AI报告文档审核&#xff1a;逐个击破汽车零部件与整车检测报告审核难题"展开合理演绎…

作者头像 李华
网站建设 2026/10/20 3:59:23

从大模型到数字员工:实在Agent如何用视觉感知替代传统RPA

最近我一直在折腾各种号称“数字员工”的AI产品&#xff0c;说实话&#xff0c;大多数时候期待有多大&#xff0c;失望就有多大。你兴冲冲地打开演示视频&#xff0c;看着大模型熟练地操作各种软件、自动把活干完&#xff0c;觉得这回总算靠谱了。结果自己一上手&#xff0c;要…

作者头像 李华
网站建设 2026/10/20 3:57:20

REA模型详解:从资源事件代理到交易系统落地

1. 为什么是REA模型&#xff1a;从一次业务对齐事故说起大概两年前&#xff0c;我参与某公司的一个系统重构项目&#xff0c;核心目标是把一套老掉牙的进销存系统换成符合数字时代节奏的新架构。需求评审会上&#xff0c;业务方和开发团队因为“销售出库单是不是库存减少的唯一…

作者头像 李华
网站建设 2026/10/20 3:55:47

TCP Socket带宽上限为什么上不去?窗口、BDP与内核参数调优完全指南

如果你维护过一台带宽不小、但单连接吞吐特别难看的服务器&#xff0c;大概体会过这种诡异现象&#xff1a;服务器网卡明明是千兆&#xff0c;iperf3单线程压测却只能跑到两三百 Mb/s&#xff0c;换了更快的 CPU、更大的内存也毫无起色&#xff1b;或者公网下载大文件时速度永远…

作者头像 李华
网站建设 2026/10/20 3:55:33

半监督故障诊断方法:LPS标签传播与DGAT动态图注意力的工业实践

机械设备故障诊断这个领域&#xff0c;做久了都会碰到同一个头疼事&#xff1a;现场采集的数据量倒是不少&#xff0c;但真正带标签的数据少得可怜。每次项目进场&#xff0c;光是筛出有效标签就得耗掉大量时间&#xff0c;而轴承、齿轮箱这类旋转机械一旦出问题&#xff0c;往…

作者头像 李华