尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Python FTP上传下载进度条与断点续传实战:ftplib+tkinter
简介面向需要实现 FTP 文件上传与可视化进度反馈的开发者这套示例工程以带进度条的 FTP 上传功能为核心演示建立连接、登录验证、切换目录、文件传输及资源释放等关键环节适合学习 C#/WinForms 网络编程与文件传输交互设计。压缩包共 35 个文件约 78KB主要包含 cs 源码、resx 资源、config 配置、exe 程序及 sln/csproj 工程文件并附带 pdb、cache 等编译运行产物便于直接打开工程查看结构与调试。已有 869 人学习下载。工程将 FTP 帮助类与上传进度计算封装在示例项目中读者可参考其连接管理、分块发送与百分比更新思路并在此基础上补充重试、异常处理和权限校验提升上传功能的可用性与复用性。1. 从「进度条停在 0%」说起这个 FTP 上传实例能干什么第一次把 FTP 上传和进度条拼到一起是在一个每天要往文件服务器推几百兆日志的脚本里界面一动不动进度条死死钉在 0%日志也刷不出来只能靠ls -l猜到底传了多少。这个实例就是把那次事故里拆出来的东西整理成一份能直接跑的代码——一份只依赖 Python 标准库的 FTP 文件上传下载 实现传输层用ftplib进度展示用tkinter的进度条上传、下载、断点续传、整个目录批量同步都有对应入口不引入任何第三方包。适合正在写运维工具、客户端自动更新、定时备份脚本的开发者直接照着改如果你还停留在「storbinary一调界面就冻住」的阶段这份代码基本能对上你的问题。2. 架构与选型三层拆分让上传和界面互不干扰这个实例最容易写错的地方不是 FTP 协议本身而是把网络 IO 和界面刷新塞进同一个线程。storbinary是同步阻塞调用一个 500MB 的文件在百兆带宽下要跑几十秒只要它占着主线程tkinter的窗口就会进入「未响应」进度条自然也不会动。所以整个项目从第一行代码起就按三层切开连接层只管建连和参数传输层只管字节搬运和进度回报界面层只管把数字画成进度条。三层之间只靠回调和队列通信谁都不认识谁。2.1 三个模块的职责边界按这个划分仓库里常见的是四个文件文件职责对外暴露ftp_conn.py建立连接、设置被动模式/编码/超时connect()transfer.py上传、下载、断点续传、目录同步upload_with_progress()等ui_app.py窗口、按钮、进度条、事件循环UploadApp类main.py组装依赖、启动主循环程序入口这么切的直接好处是transfer.py完全不依赖tkinter你可以把它原封不动搬到命令行工具、Flask 接口或者定时任务里进度回调换成写日志、写数据库都行。我一般会把传输层设计成「只接收一个on_progress(sent, total)回调」至于这个回调最后是更新进度条还是打日志由调用方决定这样二次开发时改动面最小。2.2 目录结构与依赖清单目录按上面的模块平铺不搞多层包嵌套示例代码层级越浅越好读ftp_progress_demo/ ├── ftp_conn.py # 连接层 ├── transfer.py # 传输层上传 / 下载 / 续传 / 目录同步 ├── ui_app.py # 界面层tkinter 队列 ├── main.py # 入口 └── config.example.ini # 服务器地址、账号、块大小等外置配置依赖是零。Python 3.8 以上自带ftplib、tkinter、queue、concurrent.futures、os、time不用pip install任何东西。唯一要确认的是tkinter有没有装部分精简版 Linux 发行版把它拆成了独立包跑python -c import tkinter报ModuleNotFoundError的话补装系统包即可这不是pip能解决的事。2.3 连接参数被动模式、编码、超时怎么定连接层看着简单坑几乎全在这十几行里# ftp_conn.py 连接层只负责建连和参数不做任何数据传输 import ftplib def connect(host, port21, useranonymous, password, timeout30, encodingutf-8): # timeout 必须给默认是全局 socket 超时卡死时无法自愈 ftp ftplib.FTP(timeouttimeout) # encoding 要在 connect 之前设定它决定路径和响应文本怎么解码 ftp.encoding encoding ftp.connect(host, port) ftp.login(user, password) # 主动模式在 NAT / 防火墙后基本连不上数据端口一律走被动 ftp.set_pasv(True) # 显式声明二进制模式传文件绝对不能落到 TYPE A ftp.voidcmd(TYPE I) return ftpset_pasv(True)是默认值但我不喜欢依赖默认值——现场排查时你会感谢自己写明了。timeout30是单次 socket 操作的超时不是整个传输的总时长大文件传十分钟也不会被它掐断但它能保证网络静默 30 秒后抛异常而不是永远挂着。encoding是最容易被忽略的参数ftplib用它来编解码命令和路径服务端是 Windows IIS 搭的 FTP 时经常是 GBK用默认的 UTF-8 去传中文文件名就会变成一堆问号症状是550 The filename, directory name, or volume label syntax is incorrect。3. 上传主链路把字节数和进度条对上ftplib.FTP.storbinary(cmd, fp, blocksize8192, callbackNone, restNone)这个签名里有三个东西决定成败fp必须是二进制文件对象、blocksize决定进度回报的粒度、callback决定你能不能拿到进度。很多人卡在第一步用open(path, r)打开文件fp.read()返回strsendall直接抛TypeError也有人以为callback会给「累计已发送字节数」实际上它每次收到的是「刚刚发出去的那一块数据」累计值得自己加。3.1 storbinary 的 callback 到底给了什么先看传输层最核心的一段# transfer.py 上传累计 callback 收到的字节块换算成进度 import os, ftplib def upload_with_progress(ftp, local_path, remote_path, on_progress, blocksize64 * 1024): total os.path.getsize(local_path) # 总字节数进度条的分母 sent 0 def _cb(buf): # ftplib 每发完一块就回调一次buf 是这一块的 bytes nonlocal sent sent len(buf) on_progress(sent, total) # 二进制打开避免 Windows 下把 \n 转成 \r\n 导致文件损坏 with open(local_path, rb) as fp: ftp.storbinary(fSTOR {remote_path}, fp, blocksizeblocksize, callback_cb) return sentnonlocal sent是这里的关键闭包里改外层变量忘了声明就会UnboundLocalError。blocksize默认 8192我调到 64KB 是因为进度回调太密反而拖慢传输——64KB 一块在百兆内网里意味着大约每秒几十次回调界面完全够顺滑如果是几十 KB 的小文件可以降到 8KB进度条开始动得更早。STOR {remote_path}里的路径必须是服务器上已存在的目录ftplib不会替你建父目录路径写错会直接抛error_perm: 550。回调拿到的sent是「已交给内核 socket 缓冲区的字节数」不是「服务器已确认落盘的字节数」。这个差别就是后面排查章节里「进度条 100% 但界面不结束」的根源先记住这一点。3.2 从工作线程到 tkinter 主线程的进度传递tkinter不是线程安全的工作线程里直接碰progress[value]在 Windows 上表现为随机崩溃在 macOS 上可能只是偶尔不刷新属于典型的玄学问题。稳妥做法是工作线程只往queue.Queue里塞元组主线程用after轮询消费# ui_app.py 工作线程只写队列主线程用 after 轮询消费 import queue, threading, tkinter as tk from tkinter import ttk class UploadApp: def __init__(self, root, uploader): self.root root self.uploader uploader self.events queue.Queue() # 唯一的跨线程通道 self.bar ttk.Progressbar(root, length360, maximum100) self.bar.pack(padx12, pady12) self.root.after(80, self._drain) # 80ms 一次肉眼够顺滑 def start(self, local_path, remote_path): def _work(): def _cb(sent, total): # 只放数据不做任何 UI 操作 self.events.put((progress, sent, total)) try: self.uploader(local_path, remote_path, _cb) self.events.put((done, 0, 0)) except Exception as exc: self.events.put((error, str(exc), 0)) threading.Thread(target_work, daemonTrue).start() def _drain(self): try: while True: kind, a, b self.events.get_nowait() if kind progress and b: self.bar[value] a * 100 / b elif kind done: self.bar[value] 100 elif kind error: self.bar[value] 0 print(上传失败:, a) except queue.Empty: pass self.root.after(80, self._drain) # 自己续约循环轮询轮询间隔 80ms 是权衡后的结果再快主线程被无谓唤醒再慢进度条会有明显跳跃感。daemonTrue保证关窗口时工作线程不会拖住进程退出代价是传输可能被硬中断服务端留下半截文件——所以正式场合我会在WM_DELETE_WINDOW里先置一个self.cancelled标志让工作线程在回调里检查后主动ftp.abort()。3.3 断点续传SIZE REST 的两步走传 2GB 的镜像传到 1.8GB 断网从头再来一遍谁都会崩溃。FTP 的续传靠两条命令先SIZE问服务器上已有多少字节再在STOR之前发REST offset让服务端把写指针挪过去。storbinary的rest参数就是干这个的# transfer.py 续传上传先问大小再定位最后带 rest 发 STOR def resume_upload(ftp, local_path, remote_path, on_progress, blocksize64 * 1024): total os.path.getsize(local_path) try: offset ftp.size(remote_path) or 0 # 服务器不支持 SIZE 会抛异常 except ftplib.error_perm: offset 0 if offset total: on_progress(total, total) # 已经是完整文件直接收工 return total with open(local_path, rb) as fp: fp.seek(offset) # 本地也跳到同一位置 sent offset def _cb(buf): nonlocal sent sent len(buf) on_progress(sent, total) ftp.storbinary(fSTOR {remote_path}, fp, blocksizeblocksize, callback_cb, restoffset) return sentsent初始化为offset而不是 0进度条才不会从零重新爬。fp.seek(offset)和restoffset必须一致只做一边的后果是文件内容错位传上去的文件能打开但内容全是乱的这种问题肉眼极难发现。并非所有服务端都支持对STOR使用REST有些只允许REST配合RETR遇到502 REST not implemented就得退化成APPE追加写或者干脆把文件切块用临时文件名传完再RNFR/RNTO重命名——重命名这一步顺带解决了「传一半被别处读到」的问题我后来做生产工具时都默认这么干。4. 下载、目录与批量任务把单文件示例扩成能用的工具只会传单个文件示例就永远只是个示例。真实需求往往是「把本地某个目录整个推上去」或者「把服务器上昨天的备份拉下来」这就涉及下载链路、递归建目录和并发。这三件事各有各的坑尤其并发ftplib.FTP实例内部维护着控制连接的状态机多线程共用一个实例会出现命令和响应错位的诡异现象表现为莫名拿到上一个命令的返回码。4.1 retrbinary 与下载进度下载用retrbinary回调里拿到的是刚从数据连接读到的字节块写文件也在回调里做# transfer.py 下载回调里同时落盘和回报进度 def download_with_progress(ftp, remote_path, local_path, on_progress, blocksize64 * 1024): total ftp.size(remote_path) or 0 # 拿不到大小时给 0前端做不确定态 done 0 with open(local_path, wb) as fp: def _cb(buf): nonlocal done fp.write(buf) # 边收边写避免大文件全塞内存 done len(buf) on_progress(done, total) ftp.retrbinary(fRETR {remote_path}, _cb, blocksizeblocksize) return doneftp.size()在 ASCII 模式下返回的是转换后的大小和实际字节数对不上所以连接层那句voidcmd(TYPE I)不只是为了上传。total为 0 时部分服务端对某些文件返回550 SIZE not allowed in ASCII mode或干脆不支持进度条改走不确定态别除零。下载续传同理retrbinary也有rest参数本地以ab打开先os.path.getsize得到已有长度# transfer.py 续传下载本地已有多少就从哪里继续 def resume_download(ftp, remote_path, local_path, on_progress, blocksize64 * 1024): total ftp.size(remote_path) or 0 offset os.path.getsize(local_path) if os.path.exists(local_path) else 0 done offset with open(local_path, ab) as fp: # 追加模式别覆盖已下载部分 def _cb(buf): nonlocal done fp.write(buf) done len(buf) on_progress(done, total) ftp.retrbinary(fRETR {remote_path}, _cb, blocksizeblocksize, restoffset) return done追加模式ab一旦写错成wb续传就变成了「从断点开始覆盖」最终文件前面一段全丢。4.2 目录遍历与递归上传批量上传要先解决目录FTP 没有mkdir -pMKD一次只能建一级。常见做法是遍历时逐级建目录已存在会抛error_perm: 550直接吞掉即可# transfer.py 递归上传逐级建目录550 当已存在处理 import os, ftplib def sync_dir(ftp, local_root, remote_root, on_progressNone): for dirpath, _dirnames, filenames in os.walk(local_root): rel os.path.relpath(dirpath, local_root) remote_dir remote_root if rel . else \ remote_root.rstrip(/) / rel.replace(os.sep, /) try: ftp.mkd(remote_dir) # 逐级建不支持递归创建 except ftplib.error_perm: pass # 已存在正常情况不中断 for name in filenames: local_file os.path.join(dirpath, name) remote_file remote_dir / name upload_with_progress(ftp, local_file, remote_file, on_progress or (lambda s, t: None))os.sep那一步不能省Windows 上os.walk给出的是反斜杠路径直接拼进 FTP 命令会得到一个服务器看不懂的目录名。另外遍历时会被.git、__pycache__、.DS_Store这类目录和文件污染实际项目里我会传一个exclude集合在dirnames[:] [...]上做原地过滤这样os.walk连进都不会进去。4.3 多线程并发与连接池的边界单连接传一百个小文件握手开销比数据本身还大。加并发是自然想法但必须是「一线程一连接」# transfer.py 并发上传每个任务独立建连绝不共用 FTP 实例 from concurrent.futures import ThreadPoolExecutor def upload_many(conn_kwargs, jobs, on_progress, max_workers4): def _one(job): local_file, remote_file job ftp connect(**conn_kwargs) # 每个线程自己的连接 try: return upload_with_progress(ftp, local_file, remote_file, on_progress) finally: try: ftp.quit() # 正常退出让服务端释放会话 except Exception: ftp.close() # quit 失败也要关 socket with ThreadPoolExecutor(max_workersmax_workers) as pool: return list(pool.map(_one, jobs))max_workers不要贪多很多服务端有单 IP 会话数上限开 16 个线程大概率撞上421 Too many connections而且被动模式下每个连接都要占一个数据端口服务端没配置端口范围的话会直接被防火墙掐。我一般取 3 到 4配合每线程独立连接的方案实测比单线程快四倍左右再加就只剩失败率上升了。5. 排查手册五类让上传卡住或失败的现场这一节是按「现象 → 原因 → 解决」整理的全是现场遇到过的。FTP 的错误码本身信息量很低425、426、550具体指什么要结合上下文所以排查时先定位是控制连接、数据连接还是应用层的问题比死背错误码有用得多。5.1 进度条跑到 100% 但界面一直不结束现象进度条已经拉满按钮还是灰的过几秒甚至几十秒才提示成功网络差的时候直接超时。原因storbinary的callback统计的是「已经写进内核 socket 缓冲区的字节」此时数据可能还堆在发送缓冲区里没到对端函数末尾还有个self.voidresp()要等服务器回226 Transfer complete才算真正完成。带宽小、延迟高的时候最后这一段等待非常明显。解决把 100% 拆成两个状态。回调里到sent total时先把进度条设成 99% 并显示「等待服务器确认」storbinary正常返回之后再置 100%。这样用户看到的是「传输完成正在校验」而不是「卡死在 100%」。5.2 服务器上的文件大小是 0或者是原始文件的一半现象命令没报错RETR下来发现文件损坏文本文件末尾少几行二进制文件直接打不开。原因用open(path, r)打开了文件fp.read()返回strsendall要bytes要么直接TypeError要么在某些封装里被隐式编码Windows 下\n被转成\r\n字节数膨胀另一种情况是自己发了TYPE A命令服务端按 ASCII 模式做换行转换。解决传输层一律open(local_path, rb)连接建立后立刻voidcmd(TYPE I)并且不要在业务代码里手动发TYPE命令去覆盖它。5.3 中文文件名变成问号或者 550 找不到路径现象本地叫报表2024.xlsx传上去在客户端里显示成一串乱码或者STOR直接报550。原因ftplib.FTP.encoding默认是 UTF-8服务端如果是老版本 IIS 或者某些嵌入式 FTP用的是 GBK/CP936双方对同一串字节的解释不一致。解决连接时把encoding传成gbk试一次新一些的服务端可以发OPTS UTF8 ON显式开启 UTF-8。两边都不确定的话做一层「先按 UTF-8 试收到error_perm就切 GBK 重连」的降级虽然丑但有效。5.4 小文件正常大文件传一会儿就 425 或超时断开现象几百 KB 的文件随便传几十 MB 传到一半抛TimeoutError或425 Cant open data connection。原因被动模式下服务端返回的是数据端口如果服务端在 NAT 后面又没配置外网地址客户端拿到的是内网 IP根本连不上另一种是防火墙只放行了 21 端口被动模式随机端口全被拦。解决set_pasv(True)确认已开让服务端把被动端口范围固定成一小段比如 50000-50010并在防火墙上放行客户端侧把timeout从 30 提到 60 到 120同时开socket的SO_KEEPALIVE避免中间设备把空闲连接踢掉。5.5 并发上传时报 421 或者文件内容互相串现象单线程一切正常开了线程池之后偶发421 Too many connections或者某个文件传上去内容明显是另一个文件的。原因多个线程共用了同一个ftplib.FTP实例。FTP 是严格的一问一答协议控制连接上的状态机被并发写就会错位STOR的响应可能被另一个线程读走。解决一线程一连接用完quit()连接数控制在服务端允许的会话上限内。如果确实需要复用就上连接池并保证同一时刻一个连接只被一个任务持有加锁串行化别指望ftplib自己处理并发。6. 二次开发把 demo 改成能配置的传输组件真要把这套代码用在项目里第一件该做的事是把host、user、password、blocksize、timeout从代码里挪出去。我在示例里留了config.example.ini用configparser读进来打包成一个dataclass好处是单元测试可以塞假参数、部署时改配置不改代码、密码还能从环境变量覆盖# config.py 配置外置密码优先读环境变量 import os, configparser from dataclasses import dataclass dataclass class FtpConf: host: str port: int 21 user: str anonymous password: str timeout: int 60 blocksize: int 64 * 1024 encoding: str utf-8 def load_conf(pathconfig.ini): cp configparser.ConfigParser() cp.read(path, encodingutf-8) sec cp[ftp] return FtpConf( hostsec[host], portsec.getint(port, 21), usersec[user], passwordos.environ.get(FTP_PASSWORD, sec.get(password, )), timeoutsec.getint(timeout, 60), blocksizesec.getint(blocksize, 64 * 1024), encodingsec.get(encoding, utf-8), )第二件事是给传输层留可替换的进度回调。示例里回调签名是(sent, total)二次开发时你可以包一层把同一个回调复用成「更新进度条 写审计日志 上报监控」三个下游传输层代码一行都不用动。第三件事是验证别拿生产服务器当测试环境。我一般会在本地起一个临时 FTP 服务把STOR、RETR、REST、MKD四条路径各跑一遍重点看三个断言上传后的size和本地一致、续传后的文件 MD5 和源文件一致、550场景父目录不存在能正确抛异常而不是静默失败。还有一个值得提前做的改造是把「传输」包成可取消的任务对象。界面上的取消按钮不能直接ftp.close()那会让服务端留下半截文件正确做法是在回调里检查取消标志抛出自己定义的TransferCancelled在except里调ftp.abort()再关连接服务端会把这次传输标记为失败并回收。这个细节我第一次做的时候完全没考虑结果测试同事点了几次取消服务器上堆了一堆残缺文件被运维找上门。从那以后我每次动传输相关的代码都强制走一遍「小文件 / 大文件 / 断网重连续传 / 中文名 / 取消」这五条固定用例哪怕改动只有一行。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

带进度条的FTP上传下载实例:断点续传与失败重试封装

带进度条的FTP上传下载实例:断点续传与失败重试封装

简介:面向需要为文件传输加入可视化反馈的开发者,这份 FTP上传实例(带进度条)以 C# 窗体项目形式提供可运行的参考实现,重点解决连接服务器、登录验证、切换目录、分块上传以及按已传字节数实时刷新进度条等常见需求。…

📅 2026/11/2 3:37:59
动态规划实战:摆动序列与最大子数组和算法解析

动态规划实战:摆动序列与最大子数组和算法解析

1. 摆动序列与最大子数组和问题概述在算法领域,摆动序列和最大子数组和是两个经典问题,经常出现在编程面试和算法竞赛中。2026年2月2日这个日期可能暗示着某个特定算法竞赛或编程挑战的举办时间。这两个问题虽然看似独立,但实际上都涉及数组处…

📅 2026/11/2 3:32:59
2026科研检索工具排行榜:10个文献检索工具与AI自动化工作流实战

2026科研检索工具排行榜:10个文献检索工具与AI自动化工作流实战

1. 科研检索这件事,为什么2026年值得重新聊一遍我做了十多年科研工具相关的工作,从最早在图书馆里翻纸质索引,到后来用各种数据库的高级检索语法,再到现在看着一批批年轻研究者用AI Agent自动跑文献综述,这个领域的变化…

📅 2026/11/2 3:32:59
MORE NEWS

更多资讯

📰

逆向拆解六款开源RAG:从架构共性到自研蓝图与避坑指南

RAG 这个词从 2023 年火到现在,热度一直没降过。但我在实际项目里观察到一个很普遍的现象:很多团队一上来就自己造轮子,从文档解析、切片、向量化到检索排序全部手写一遍,结果做出来的东西召回率惨不忍睹,用户问三句答…

📰

RAG答错别急着换模型:文档预处理才是效果分水岭

1. 为什么模型背了锅:RAG 答错的真实归因RAG 答错的时候,绝大多数人的第一反应是换模型。从 7B 换到 14B,从 14B 换到 70B,再不行就上闭源 API,账单翻了几倍,效果可能只提升了几个百分点。我见过太多团队在…

📰

Formula One 9.0 工业级表格控件深度解析与COM集成实战

简介:Formula One 9.0 是一款面向VTS(视频标题系统或车辆追踪系统)内容编辑场景的免费桌面工具,适用于媒体制作、交通监控系统运维及Java GUI开发学习者,解决模板化内容快速生成与定制化编辑需求。资源包为完整安装版&…

📰

Python写模型,Java扛服务:AI落地的真实分工

前阵子有个写Java后端的朋友问我:AI这波热潮里,Python是不是已经把Java的活全抢了?他焦虑的原因很现实——打开招聘软件,算法岗几乎都写着"熟悉Python",连不少Java岗位的JD里都开始加一句"了解Python优…

📰

RAGFlow企业知识库选型与部署实战:DeepDoc解析、GraphRAG与Dify对比

1. 企业知识库选型的核心矛盾与 RAGFlow 的切入点企业知识库这个赛道,这两年我最大的感受就是:Demo 满天飞,落地一地鸡毛。几乎每一家声称要做“企业大脑”的团队,第一周就能搭出一个能问答的机器人,但真正扔进业务部门…

📰

35岁转行网络安全的挑战与可行路径

1. 大龄转行网安的行业现状剖析35岁转行网络安全这个看似充满诱惑的选择,背后隐藏着许多行业真相。作为一个在安全行业摸爬滚打多年的从业者,我见过太多满怀希望转行而来,最终却黯然离场的案例。网络安全行业确实存在人才缺口,但这…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬