Python開發(fā)中使用read()讀取大文件導(dǎo)致內(nèi)存溢出問(wèn)題解決辦法
前言
在 Python 中,文件讀取是最基本的操作之一。許多開發(fā)者習(xí)慣用 f.read() 一次性將整個(gè)文件加載到內(nèi)存中,這在處理小文件時(shí)完全無(wú)礙,代碼也最為簡(jiǎn)潔。但當(dāng)文件體積增長(zhǎng)到幾百 MB 甚至幾 GB 時(shí),read() 就會(huì)成為“內(nèi)存殺手”,輕則導(dǎo)致程序運(yùn)行緩慢、系統(tǒng)卡頓,重則直接拋出 MemoryError 并使進(jìn)程被 操作系統(tǒng)強(qiáng)制終止。理解這一問(wèn)題的本質(zhì)并掌握流式讀取技術(shù),是編寫健壯數(shù)據(jù)處理程序的基本功。
一、問(wèn)題復(fù)現(xiàn):一行read()引發(fā)的崩潰
with open('huge_log.txt', 'r') as f:
content = f.read()
# 如果 huge_log.txt 大小為 10 GB,服務(wù)器內(nèi)存只有 8 GB,
# 程序會(huì)卡死或拋出 MemoryError
執(zhí)行上述代碼后,你可能看到:
MemoryError
或者操作系統(tǒng)直接殺死 Python 進(jìn)程(尤其在 Linux 的 OOM Killer 介入時(shí)),不會(huì)留下任何 Python 異常。即便文件恰巧小于可用內(nèi)存,read() 也會(huì)瞬間將大量數(shù)據(jù)寫入 RAM,導(dǎo)致系統(tǒng)其他進(jìn)程被迫換出,嚴(yán)重拖慢整個(gè)環(huán)境。
二、底層原理:read()是“一口吞”
文件對(duì)象的 read(size=-1) 方法的行為是:
- 若不傳
size或傳入負(fù)值,讀取文件的全部剩余內(nèi)容直到 EOF。 - 返回一個(gè)字符串(文本模式)或字節(jié)串(二進(jìn)制模式),這個(gè)對(duì)象在內(nèi)存中是連續(xù)的,其大小約等于文件的原始字節(jié)數(shù)(或稍大,因 Python 字符串內(nèi)部表示可能有額外開銷)。
對(duì)于大文件,這顯然要求 可用內(nèi)存 ≥ 文件大小。而實(shí)際場(chǎng)景中,處理數(shù)據(jù)往往只需要一次查看一行或一個(gè)數(shù)據(jù)塊,根本無(wú)需將全文件同時(shí)駐留內(nèi)存。
三、常見誤區(qū)與陷阱場(chǎng)景
1. 使用readlines()同樣危險(xiǎn)
with open('huge.csv', 'r') as f:
lines = f.readlines() # 同樣將所有行讀入一個(gè)列表
readlines() 一次性返回包含所有行的列表,每一行作為一個(gè)字符串,內(nèi)存占用較 read() 只有更大的份(因列表本身還有開銷)。
2. 對(duì)大文件使用splitlines()前先read()
content = f.read()
for line in content.splitlines():
process(line)
這仍然要求整個(gè)文件進(jìn)入內(nèi)存,毫無(wú)改觀。
3. Pandas / JSON 等庫(kù)隱含的全量讀取
import pandas as pd
df = pd.read_csv('big_data.csv') # 默認(rèn)一次性加載全部數(shù)據(jù)
雖然這些庫(kù)提供了分塊讀取參數(shù),但若忘記設(shè)置,同樣會(huì)遭遇內(nèi)存耗盡。
四、解決方案:流式讀取,按需加載
幸運(yùn)的是,Python 提供了多種優(yōu)雅的流式處理方式,使得內(nèi)存占用僅與單次處理的數(shù)據(jù)塊大小相關(guān),而不隨文件大小線性增長(zhǎng)。
方案一:固定大小分塊讀取(二進(jìn)制模式最可靠)
chunk_size = 4096 # 或 8192, 64*1024 等
with open('large_file.bin', 'rb') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
# 處理 chunk (bytes)
- 適用場(chǎng)景:處理二進(jìn)制文件,或進(jìn)行簡(jiǎn)單文本處理(但需自行處理行邊界)。
- 優(yōu)點(diǎn):內(nèi)存占用恒定,對(duì)任何大小文件都穩(wěn)定。
- 注意:文本模式下按字符讀取,需注意多字節(jié)字符可能被截?cái)啵灰话憬ㄗh在二進(jìn)制模式下處理非文本或自行解碼。
方案二:將文件對(duì)象作為迭代器(按行讀?。?/h3>
文本模式打開的文件對(duì)象是一個(gè)可迭代對(duì)象,逐行產(chǎn)出 str。
with open('huge_log.txt', 'r', encoding='utf-8') as f:
for line in f:
process(line)
- 底層:文件對(duì)象內(nèi)部有一個(gè)緩沖區(qū),按需從磁盤讀取數(shù)據(jù)并按換行符分割,每次只返回一行。內(nèi)存中僅保留當(dāng)前行及少量緩沖,非常高效。
- 適用:日志分析、CSV 初步過(guò)濾、任何基于行的文本處理。
- 額外好處:代碼極簡(jiǎn),無(wú)需顯式循環(huán)
readline()。
注意:如果行特別長(zhǎng)(例如單個(gè) JSON 對(duì)象占用一整行且高達(dá)數(shù)百 MB),逐行迭代仍可能因?yàn)閱涡羞^(guò)大而內(nèi)存暴漲。此時(shí)需切換為分塊讀取,并自行解析。
方案三:使用fileinput模塊處理多個(gè)大文件
import fileinput
with fileinput.input(files=['log1.txt', 'log2.txt'], mode='r',
openhook=fileinput.hook_encoded('utf-8')) as f:
for line in f:
process(line)
fileinput 支持同時(shí)串聯(lián)多個(gè)文件,逐行遍歷,并自動(dòng)關(guān)閉打開的文件。適合需要按行處理一系列大文件的場(chǎng)景,內(nèi)存開銷低。
方案四:內(nèi)存映射文件——mmap
對(duì)于需要隨機(jī)訪問(wèn)或部分讀取的大文件,使用 mmap 模塊將文件映射到虛擬內(nèi)存空間,操作系統(tǒng)會(huì)按需加載頁(yè)。
import mmap
with open('huge.dat', 'r+b') as f:
with mmap.mmap(f.fileno(), 0) as m:
# m 是一個(gè)類似字節(jié)數(shù)組的對(duì)象,支持切片
first_kb = m[:1024]
# 可以通過(guò) find 等方法高效搜索
pos = m.find(b'ERROR')
mmap并不將整個(gè)文件讀入物理內(nèi)存,而是利用操作系統(tǒng)的內(nèi)存頁(yè)管理,只有實(shí)際訪問(wèn)的區(qū)域才被加載。這使得處理超大文件成為可能,且隨機(jī)訪問(wèn)性能極佳。- 缺點(diǎn):API 是字節(jié)級(jí)操作,對(duì)文本處理需手動(dòng)解碼;不是所有文件(如管道或網(wǎng)絡(luò)流)都支持
mmap。
方案五:利用高級(jí)庫(kù)的分塊參數(shù)
許多數(shù)據(jù)處理庫(kù)原生支持流式讀?。?/p>
- Pandas:
pd.read_csv('data.csv', chunksize=10000)返回一個(gè)迭代器,每次產(chǎn)出一個(gè)包含chunksize行的 DataFrame。for chunk in pd.read_csv('big.csv', chunksize=50000): # 對(duì) chunk 進(jìn)行操作 - JSON:使用
ijson庫(kù)進(jìn)行增量解析,避免將整個(gè) JSON 對(duì)象加載到內(nèi)存。 - XML:
xml.etree.ElementTree的iterparse支持流式處理。 - SQLAlchemy:可使用
yield_per()分批獲取查詢結(jié)果。
五、內(nèi)存與性能對(duì)比
| 方法 | 內(nèi)存占用 | 適用場(chǎng)景 | 復(fù)雜度 |
|---|---|---|---|
f.read() | 等于文件大小 | 小文件(< 幾 MB) | 極簡(jiǎn) |
f.readlines() | 大于文件大小(+ 列表開銷) | 小文件讀所有行 | 簡(jiǎn)單 |
for line in f | 約等于最長(zhǎng)行的字節(jié)數(shù) | 基于行的文本處理 | 極簡(jiǎn) |
分塊 f.read(chunk_size) | 固定為 chunk_size | 任意二進(jìn)制或需定界解析的文本 | 簡(jiǎn)單 |
mmap | 恒常低內(nèi)存(頁(yè)緩存) | 需要隨機(jī)訪問(wèn)的巨大文件 | 中等 |
pandas chunksize | 單塊大小 | 表格數(shù)據(jù)分析 | 簡(jiǎn)單 |
必須強(qiáng)調(diào)的是,流式讀取雖然在內(nèi)存上占據(jù)絕對(duì)優(yōu)勢(shì),但若處理邏輯需要同時(shí)知道所有數(shù)據(jù)(如全局排序、全數(shù)據(jù)集統(tǒng)計(jì)分析),則可能需要其他技術(shù)(外部排序、數(shù)據(jù)庫(kù)、采樣等),單純靠流式讀取無(wú)法解決。
六、調(diào)試與監(jiān)控
1. 使用memory_profiler分析內(nèi)存
# pip install memory_profiler
from memory_profiler import profile
@profile
def read_large():
with open('big.txt') as f:
return f.read()
運(yùn)行該腳本會(huì)輸出每行代碼的內(nèi)存增量,清晰地暴露 read() 的暴漲。
2. 估算文件大小
用 os.path.getsize() 獲取文件大小,判定是否采用流式讀?。?/p>
import os
def smart_open(path, chunk_threshold=50*1024*1024): # 50 MB
size = os.path.getsize(path)
if size < chunk_threshold:
with open(path) as f:
return f.read()
else:
# 返回一個(gè)生成器,按行讀取
with open(path) as f:
for line in f:
yield line
3. 操作系統(tǒng)工具
- Linux:
htop/free -m觀察進(jìn)程內(nèi)存。 - Windows:任務(wù)管理器。
psutil庫(kù)在代碼中監(jiān)控進(jìn)程內(nèi)存:import psutil, os process = psutil.Process(os.getpid()) print(process.memory_info().rss) # 字節(jié)
七、最佳實(shí)踐總結(jié)
- 永遠(yuǎn)不要假設(shè)文件“足夠小”。用戶輸入、生產(chǎn)環(huán)境日志可能隨時(shí)膨脹。
- 默認(rèn)使用
for line in file處理文本,這是 Pythonic 且安全的習(xí)慣。 - 處理二進(jìn)制文件,使用固定大小的
while chunk := f.read(CHUNK):循環(huán)。 - 面對(duì)結(jié)構(gòu)化數(shù)據(jù),首先查閱相關(guān)庫(kù)是否提供了流式 API(如
chunksize、iterparse)。 - 如果函數(shù)封裝了文件讀取邏輯,應(yīng)避免返回整個(gè)文件內(nèi)容,而是返回一個(gè)生成器或文件對(duì)象本身,由調(diào)用方迭代。
- **編寫單元測(cè)試時(shí),用大文件(例如生成幾百 MB 的臨時(shí)文件)驗(yàn)證流式邏輯,防止重構(gòu)后退化為全量讀取。
- 注意關(guān)閉文件和上下文管理,流式讀取時(shí)應(yīng)仍然使用
with語(yǔ)句,確保文件描述符不泄漏。
八、結(jié)語(yǔ)
“使用 read() 讀取大文件導(dǎo)致內(nèi)存溢出”是一個(gè)從初學(xué)者到有經(jīng)驗(yàn)工程師都可能重復(fù)踩入的陷阱。它的危險(xiǎn)性在于:在測(cè)試環(huán)境和少量數(shù)據(jù)下一切正常,而在真實(shí)環(huán)境和數(shù)據(jù)量激增后瞬間崩盤。流式讀取不是高深技巧,而是 Python 文件處理的基本素養(yǎng)。將其內(nèi)化為肌肉記憶——看到 f.read() 時(shí),先問(wèn)自己:“這個(gè)文件可能有多大?” 你將因此避開無(wú)數(shù)的痛苦故障,寫出穩(wěn)健、可擴(kuò)展的數(shù)據(jù)處理程序。
到此這篇關(guān)于Python開發(fā)中使用read()讀取大文件導(dǎo)致內(nèi)存溢出問(wèn)題解決的文章就介紹到這了,更多相關(guān)Python read()讀取大文件內(nèi)存溢出內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python?哈希表的實(shí)現(xiàn)——字典詳解
這篇文章主要介紹了Python?哈希表的實(shí)現(xiàn)——字典,那么今天我們就來(lái)看看哈希表的原理以及如何實(shí)現(xiàn)一個(gè)簡(jiǎn)易版的?Python?哈希表,需要的朋友可以參考下2023-11-11
Python控制鍵盤鼠標(biāo)pynput的詳細(xì)用法
這篇文章主要介紹了Python控制鍵盤鼠標(biāo)pynput的詳細(xì)用法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-01-01
python實(shí)現(xiàn)微信遠(yuǎn)程控制電腦
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)微信遠(yuǎn)程控制電腦的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02
python 如何執(zhí)行控制臺(tái)命令與操作剪切板
這篇文章主要介紹了python 如何執(zhí)行控制臺(tái)命令與操作剪切板,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-05-05
Python抓取數(shù)據(jù)到可視化全流程的實(shí)現(xiàn)過(guò)程
這篇文章主要介紹了Python抓取數(shù)據(jù)到可視化全流程的實(shí)現(xiàn)過(guò)程,2022-01-01
pycharm使用技巧之自動(dòng)調(diào)整代碼格式總結(jié)
這篇文章主要給大家介紹了關(guān)于pycharm使用技巧之自動(dòng)調(diào)整代碼格式總結(jié)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
Python編程快速上手的15個(gè)場(chǎng)景的示例代碼
本文為初學(xué)者提供15個(gè)簡(jiǎn)單的Python示例代碼,涵蓋了基本語(yǔ)法、數(shù)據(jù)結(jié)構(gòu)、文件操作、面向?qū)ο缶幊?、異常處理和網(wǎng)絡(luò)請(qǐng)求等內(nèi)容,幫助快速入門Python編程2025-11-11

