登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python shutil.copyfileobj 怎样控制大文件复制缓冲区

来源:17golang原创

时间:2026-10-23 08:56:16 206浏览 收藏

用 shutil.copyfileobj() 复制大文件时,真正控制内存占用的是第三个参数 length。它表示每一轮从源对象读取的缓冲区大小:省略时由 Python 分块复制,传入正数时使用指定大小,传入负数则可能一次把剩余内容读入内存。生产代码通常应优先使用明确的正数,并根据输入流的性质调整。

官方文档地址:https://docs.python.org/3/library/shutil.html#shutil.copyfileobj

如果目标是稳定复制大内容,先把 length 当作“单次读取上限”来设计,而不是把它当作文件总大小。默认分块适合不确定的输入,正数适合需要控制内存的任务,负数只适用于你明确接受一次性读取的短数据。

先分清 length 的真实含义

copyfileobj(fsrc, fdst, length) 面向的是两个 file-like object,不要求它们一定来自普通文件。文件、BytesIO、HTTP 响应流和临时文件都可以提供类似的读取或写入接口。

写法内存语义适用判断
省略 length按内部块分段读取,避免无界的一次性加载输入规模不确定时的默认选择
length=1024 * 1024每次最多读取约 1 MiB希望明确控制单次缓冲区
length=-1不再按块循环,可能一次读取全部剩余内容只有短小且内存可控的输入才考虑

官方文档还特别说明:如果源对象当前的位置不是 0,复制只会从当前位置到末尾。这个细节很容易让“目标文件少了一截”被误判成缓冲区问题。

用正数缓冲区复制大文件

对于本地文件,建议以二进制模式打开,并把缓冲区值写成有名字的常量。下面的示例只表达复制边界,不把某个数值宣称为所有磁盘和网络环境的最佳值。

from pathlib import Path
from shutil import copyfileobj

CHUNK_SIZE = 1024 * 1024  # 每轮最多读取 1 MiB,限制复制阶段的临时内存
source = Path("backup.tar")
target = Path("backup-copy.tar")

with source.open("rb") as src, target.open("wb") as dst:
    copyfileobj(src, dst, length=CHUNK_SIZE)  # 让源流和目标流保持二进制类型
    dst.flush()  # 后续若要立即读取目标文件,先刷新写入缓冲区

这里的关键不是“1 MiB 一定最快”,而是把单次读取的边界显式化。缓冲区太小会增加循环和系统调用次数,太大则会抬高单个任务的瞬时内存;多任务并发复制时,应把每个任务的缓冲区一起估算。

Python copyfileobj 源流、正数缓冲区和目标流之间的结构关系说明图
图1:复制流边界说明图,查看源流、length 缓冲区和目标流的关系;这是静态结构图,不是运行截图。

按流类型做取舍,而不是盲目放大缓冲区

在高并发任务中,旧的“每次读取全部剩余内容”做法会把输入大小直接转化为单任务内存压力。更稳妥的设计是把源流、缓冲区和目标流分开考虑:

  • 本地大文件:使用固定正数,让内存占用与文件总大小脱钩。
  • 网络输入:用中等块大小换取吞吐和延迟的平衡,不因为网络慢就扩大到整个响应。
  • 临时文件或内存流:仍要关注源对象当前位置,目标对象若要马上读取则显式 flush() 或关闭。

这是一种架构取舍:用更多循环次数换取可预测的内存曲线。真正的“上线结果”应由目标环境中的吞吐、并发数、磁盘和网络条件共同决定,不能只凭一个示例值下结论。若复制任务由队列并发执行,可先按“任务数 × 单任务缓冲区”估算上界,再决定是否需要更小的块或更低的并发。

Python copyfileobj 默认分块、正数缓冲区和负数一次性读取的边界对比说明图
图2:缓冲区选择说明图,比较默认分块、正数 length 与负数 length 的内存边界;这是静态结构图,不是运行截图。

四个容易误判的边界

源流不是从开头开始。如果上游已经读过请求头、文件头或一部分数据,copyfileobj() 会从当前位置继续复制。需要从开头复制时,应由调用方先明确 seek(0),而不是修改缓冲区参数。

文本流和二进制流不能混用。文件内容是字节时使用 rb 与 wb;若把二进制读取结果写入文本流,可能触发类型错误。编码转换应放在明确的文本处理层,不要交给复制函数猜测。

复制完成不等于目标已经刷新。copyfileobj() 不保证目标流在函数返回时已经 flush。复制到可读的临时文件或内存流后,先执行 flush() 或关闭,再进行后续读取。

不要把负数当成“更大的缓冲区”。负数改变的是复制策略:它取消按块循环,可能一次读取所有剩余数据。只有短数据、内存边界明确的场景才适合使用。

相关问题

为什么复制后的文件为空?先检查目标流是否仍处于写入状态,以及读取前是否执行了 flush();如果是内存流,还要确认读取位置是否需要重新定位。

length 应该设置多大?没有脱离环境的固定答案。先根据并发数和可用内存设上界,再用实际吞吐观察调整;大文件场景优先保证可预测,而不是追求一次调用完成。

copyfileobj 会复制文件权限和时间吗?不会。它只复制 file-like object 的内容;如果还需要文件元数据,应另行选择 shutil.copy()、copy2() 或专门的元数据处理逻辑。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>