shutil.copyfileobj需配合手动循环和回调实现进度更新:先用os.path.getsize获取总大小,再以固定length(如8192)分块read/write,每块后调用回调更新tqdm进度条。

shutil.copyfileobj 怎么配合回调函数更新进度
直接用 shutil.copy 或 shutil.copy2 无法传入进度回调,必须降级到更底层的 shutil.copyfileobj——它允许你控制每次读取的块大小,并在每次读写后触发自定义逻辑。
关键点在于:你需要手动打开源文件和目标文件(open(..., 'rb') 和 open(..., 'wb')),再把文件对象传给 copyfileobj,同时用循环模拟“分块搬运”,每搬一块就调用一次回调函数。
-
copyfileobj默认一次性读完全部内容,所以必须显式传入length参数(比如8192字节)来限制单次读取量 - 回调函数接收当前已复制字节数和总大小,适合用来更新
tqdm进度条或打印百分比 - 别忘了在复制前用
os.path.getsize(src)获取源文件总大小,否则无法计算进度
用 tqdm 实现简洁可靠的进度条
比起手写 print('\r{}%'.format(...)),tqdm 自动处理刷新、估算剩余时间、适配不同终端宽度,且支持嵌套和手动更新模式。
使用时注意:不能直接对 copyfileobj 包装,而要自己写循环,在每次 fout.write(data) 后调用 pbar.update(len(data))。
立即学习“Python免费学习笔记(深入)”;
import shutil
import os
from tqdm import tqdm
def copy_with_progress(src, dst):
size = os.path.getsize(src)
with open(src, 'rb') as fin, open(dst, 'wb') as fout:
pbar = tqdm(total=size, unit='B', unit_scale=True, desc=os.path.basename(src))
while True:
data = fin.read(8192)
if not data:
break
fout.write(data)
pbar.update(len(data))
pbar.close()
为什么不用 shutil.copyfile + 回调钩子
shutil.copyfile 内部确实调用了 copyfileobj,但它把整个流程封装死了,不暴露中间读写步骤,也没有预留回调接口。试图通过 monkey patch 或信号中断来注入逻辑,既不可靠又破坏可移植性。
- 某些平台(如 Windows)上,
shutil.copyfile可能调用系统级 API(如CopyFile),根本不会经过 Python 的 read/write 循环 - 即使在 Linux 上,它的实现也可能随 Python 版本变化,比如 3.12 开始部分路径改用
os.sendfile,绕过用户态缓冲区 - 真正可控的只有你自己掌控
read()和write()的节奏
大文件复制时要注意的边界情况
当文件超过几 GB,或目标磁盘空间不足时,进度条容易“卡住”或报错后状态不一致。
- 务必在
open(dst, 'wb')前检查磁盘剩余空间,用shutil.disk_usage(os.path.dirname(dst))避免写到一半因OSError: No space left on device中断 - 不要在回调里做耗时操作(如频繁写日志、网络请求),否则拖慢整体复制速度;进度条更新本身也应设限(例如每 100ms 最多刷新一次)
- 如果目标路径已存在且是目录,
open(dst, 'wb')会报IsADirectoryError,需提前用os.path.isdir(dst)检查
进度条本身不改变复制行为,只是观测层;真正影响成败的是你对文件打开、读写、异常捕获和资源清理的控制粒度。越想“看着安心”,越得把底层细节盯紧。


















