Python mmap 怎样分段处理超过内存的大文件
用 Python mmap 处理超过物理内存的大文件时,不要把“映射整个文件”和“把整个文件读入 Python 堆”混为一谈。映射主要占用虚拟地址空间,文件页由操作系统按需调入;但在 32 位进程、地址空间紧张或文件特别大时,整文件映射仍可能失败。更稳妥的做法是:每次只映射一个固定大小的窗口,映射起点按 mmap.ALLOCATIONGRANULARITY 对齐,再把逻辑窗口交给解析器。
如果记录可能跨窗口,还要保留窗口末尾的未完成字节,并与下一段开头拼接。下面给出的实现以换行分隔记录为例,每轮只保留当前映射和一条未完成记录,不会把完整文件复制到内存。
官方文档:https://docs.python.org/3/library/mmap.html
- 先取得文件真实大小,空文件直接结束。
- 用逻辑偏移计算对齐偏移和页内差值。
- 映射“页内差值 + 当前窗口长度”,但只解析逻辑窗口。
- 用
find在限定范围内找分隔符,避免复制整个窗口。 - 用 carry 保存跨窗尾部,并设置最大记录长度。
- 每段使用上下文管理器关闭,提前停止时显式关闭生成器。
为什么不能只写 mmap(fileno, 0)
在 Unix 上,length=0 表示映射调用时的当前文件大小;Windows 也支持用 0 表示当前大小,但空文件不能建立映射。对 64 位进程中的普通大文件,整文件映射往往可行,而且不等于立刻占用同等大小的物理内存。
问题在于,映射仍要保留连续虚拟地址范围。几十 GB、数百 GB 文件,或同时存在多个大映射时,整文件方案可能碰到地址空间、平台限制或资源管理难题。分段映射把最大映射范围固定在一个可控窗口内,适合批量日志、CSV、JSON Lines 和变长二进制记录。
最小配方:先对齐,再限定逻辑窗口
mmap 的 offset 不能随意取值。Python 官方文档要求它必须是 ALLOCATIONGRANULARITY 的倍数;Unix 上这个值等于页大小,Windows 上通常是系统分配粒度。业务窗口的逻辑起点不一定对齐,因此需要向下取整:
import mmap logical_offset = 70_000_000 window_size = 64 * 1024 * 1024 granularity = mmap.ALLOCATIONGRANULARITY # 映射起点向下对齐到系统要求的粒度 aligned_offset = logical_offset - (logical_offset % granularity) # delta 表示逻辑窗口在本次映射中的起始位置 delta = logical_offset - aligned_offset mapping_length = delta + window_size
真正需要处理的是 [delta, delta + window_size),不是整个映射区。最后一个窗口还要用剩余文件长度收窄,保证 aligned_offset + mapping_length 不超过文件末尾。

跨窗口记录怎么拼起来
如果每条记录固定长度,窗口按记录宽度对齐即可。换行文本更常见的情况是:窗口 A 的末尾只包含一条记录的前半部分,窗口 B 才出现换行符。直接逐段调用 splitlines() 会把这条记录错误拆成两条,而且会为整个窗口创建大量新 bytes 对象。
更节省的做法是用 mmap.find(b"\n", start, end) 在逻辑窗口内定位换行符。每找到一条完整记录就立即交给消费者,最后未匹配的尾部存入 carry。进入下个窗口后,只在第一条完整记录前拼接 carry。
carry 的大小必须有上限。否则一个损坏文件里数 GB 都没有换行符,程序仍会不断扩张 Python 堆,失去分段处理的意义。上限应按业务协议设置,例如日志单行允许 8 MiB,超过就报告异常数据。
完整片段:逐段产出字节行
下面的生成器返回不含换行符的 bytes。它使用 ACCESS_READ 保持只读语义,按系统粒度对齐 offset,并用上下文管理器在每个窗口完成后立即关闭映射。
from __future__ import annotations
import mmap
import os
from collections.abc import Iterator
def iter_mmap_lines(
path: str | os.PathLike[str],
*,
window_size: int = 64 * 1024 * 1024,
max_record_size: int = 8 * 1024 * 1024,
) -> Iterator[bytes]:
"""按窗口读取换行分隔记录,返回不含换行符的字节串。"""
if window_size max_record_size:
# 防止异常超长记录持续扩大 Python 堆
raise ValueError("记录长度超过 max_record_size")
yield record
carry = b""
cursor = newline + 1
# 只复制当前窗口最后一条未完成记录
tail = region[cursor:window_end]
if len(carry) + len(tail) > max_record_size:
raise ValueError("跨窗口记录长度超过 max_record_size")
carry += tail
logical_offset += payload_size
if carry:
# 文件末尾没有换行符时,仍产出最后一条记录
yield carry
这里没有使用 memoryview 跨过 with mmap.mmap(...) 的边界。若活动视图仍引用映射,关闭 mmap 可能失败;直接在窗口内切出当前记录的 bytes,生命周期更清晰。代价是每条完整记录会复制一次,这通常正是下游解析所需要的独立对象。
如何消费并在提前停止时释放资源
普通 for 循环读到结尾时,生成器会自然退出,当前 mmap 和文件都会关闭。如果消费者可能提前 break,可用 contextlib.closing 保证生成器收到 close(),从而立即展开内部上下文管理器:
from contextlib import closing
with closing(iter_mmap_lines("events.log")) as records:
for raw_record in records:
# 完整记录形成后再解码,避免拆断 UTF-8 多字节字符
text = raw_record.decode("utf-8")
if "FATAL" in text:
print(text)
# 提前停止时 closing 会关闭生成器及当前映射
break
如果是 CSV 或 JSON Lines,建议先在字节层完成跨窗拼接,再进行解码与结构化解析。这样 UTF-8 多字节字符即使落在窗口边缘,也会随整条记录一起进入解码器。
变体:什么时候映射整个文件更简单
当文件规模可控、运行在 64 位进程、需要大量随机定位,而且文件在映射期间保持稳定时,整文件映射通常更简洁。此时可使用 length=0,通过 find 或 readline 扫描,但仍要先排除空文件,并用上下文管理器关闭。
import mmap
import os
with open("index.bin", "rb") as source:
size = os.fstat(source.fileno()).st_size
if size:
# length=0 表示映射当前完整文件;只读访问禁止误写
with mmap.mmap(source.fileno(), 0, access=mmap.ACCESS_READ) as region:
marker = region.find(b"INDEX-V1")
print(marker)
若任务只是从头到尾读取一次,普通带缓冲文件迭代器也可能更快、更简单。是否使用 mmap 应在真实文件大小、存储设备和访问模式下测量,不要仅凭“零拷贝”字样下结论。
兼容坑:空文件、粒度和生命周期

| 问题 | 安全处理 |
|---|---|
| 空文件 | 先用 fstat 判断大小;Windows 不允许空映射。 |
| offset 未对齐 | 向下对齐到 ALLOCATIONGRANULARITY,用 delta 恢复逻辑起点。 |
| 误修改源文件 | 读取任务显式使用 ACCESS_READ,写入会抛 TypeError。 |
| 映射关闭后继续访问 | 不要把 region 或它的活动视图带出 with;关闭后方法会抛 ValueError。 |
| 处理期间文件被截断 | 让上游先写临时文件再原子替换,或建立外部协调,避免原文件尺寸变化。 |
| WASI 环境 | 官方文档标明 mmap 不可用,改用普通分块读取。 |
| 写映射需要落盘 | 另行使用合适访问模式并调用 flush();范围 offset 也要满足页粒度要求。 |
常见问题
mmap 会不会把整个文件都占进内存?
不会像 read() 那样立即返回同等大小的 bytes。它建立虚拟内存映射,页面按访问情况进入物理内存;但映射范围仍占虚拟地址空间,因此极大文件适合分段。
窗口越大越快吗?
不一定。大窗口减少映射次数,小窗口降低地址空间占用并缩短单段生命周期。应按存储设备、记录分布、并发任务和解析成本实测。
为什么不直接对每段调用 splitlines?
splitlines() 会为整段产生许多对象,而且会把跨窗记录拆错。用 find 限定搜索区间,再只复制完整记录和尾部残片,更容易控制内存。
文件被另一个进程追加可以继续读吗?
当前映射长度不会自动代表一个稳定的增长快照。持续追加日志更适合轮询文件大小并建立新窗口,同时处理轮转和截断;不要让映射期间的尺寸变化处于无协调状态。
Python mmap 分段处理的关键不是把窗口设得多小,而是分清三组边界:系统要求的对齐边界、当前业务窗口的逻辑边界,以及记录本身的分隔边界。对齐 offset、限制解析范围、保留 carry、及时关闭映射,这四点组合起来,才能稳定处理真正超过内存的大文件。
CrossOriginProtection 为什么拒绝没有 Origin 的请求
- 上一篇
- CrossOriginProtection 为什么拒绝没有 Origin 的请求
- 下一篇
- Linux pidfd 如何避免按 PID 操作进程的竞态
-
- 文章 · python教程 | 3小时前 | python · Python 二进制协议 零拷贝 memoryview
- Python memoryview 如何零拷贝切片二进制协议数据
- 225浏览 收藏
-
- 文章 · python教程 | 5小时前 | 并发控制 · Python教程 · asyncio · 虚假唤醒 wait_for Python asyncio asyncio.Condition 异步同步
- Python asyncio.Condition.wait_for 如何处理虚假唤醒
- 478浏览 收藏
-
- 文章 · python教程 | 7小时前 |
- Python ExceptionGroup 派生新组时如何保留异常元数据
- 417浏览 收藏
-
- 文章 · python教程 | 9小时前 | 异常处理 · 并发编程 · Python教程 · asyncio · asyncio 结构化并发 ExceptionGroup except* Python TaskGroup
- Python TaskGroup 如何汇总多个子任务异常
- 208浏览 收藏
-
- 文章 · python教程 | 13小时前 | 并发编程 · 工程实践 · Python教程 · 多进程日志 QueueListener multiprocessing.Queue RotatingFileHandler Python QueueHandler
- Python 日志 QueueHandler 解决多进程写入争用
- 186浏览 收藏
-
- 文章 · python教程 | 15小时前 | 数据校验 · python · Pydantic 部分更新 exclude_unset model_fields_set 显式空值 model_dump
- Pydantic 模型更新时区分未提供字段与显式空值
- 399浏览 收藏
-
- 文章 · python教程 | 17小时前 |
- pytest Fixture 作用域如何影响测试隔离与速度
- 341浏览 收藏
-
- 文章 · python教程 | 19小时前 | Python教程 · pathlib · 路径安全 Python pathlib Path.resolve 目录穿越 relative_to
- Pathlib 安全拼接用户路径:解析后再验证根目录
- 463浏览 收藏
-
- 文章 · python教程 | 21小时前 | 性能优化 · Python教程 · Python 进程间通信 pickle multiprocessing SharedMemory
- multiprocessing 传输大对象为何变慢,如何减少序列化
- 478浏览 收藏
-
- 文章 · python教程 | 1天前 | python · Python import很慢 -X importtime 模块级副作用 延迟导入 Python启动优化
- Python import 很慢怎么分析:模块级副作用与延迟导入
- 292浏览 收藏
-
- 文章 · python教程 | 1天前 | python · 异步编程 · Python asyncio contextvars request_id
- contextvars 在异步请求链中传递追踪信息
- 393浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 386次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 466次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 475次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 415次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 240次使用
-
- go格式“占位符”输入输出 类似python的input
- 2023-01-19 346浏览
-
- Golang如何调用Python代码详解
- 2023-01-07 235浏览
-
- Go weak.Pointer 实战:缓存别越跑越胖,先搞懂弱引用和 AddCleanup
- 2026-06-01 134浏览
-
- Go unique 实战:别再用全局 map 硬做字符串去重
- 2026-06-02 324浏览
-
- Go 服务内存突增怎么处理:pprof 与预算阈值运行手册
- 2026-07-01 399浏览

