应采用流式解析:NDJSON用逐行读取+json.loads(),大数组用ijson.parse()配合异步IO和状态机;禁用json.load()和ijson.items()以防内存溢出与阻塞。

大型JSON文件无法用json.load()直接读取怎么办
内存爆掉、程序卡死、抛出MemoryError——这是用json.load()硬读几个GB JSON的典型结果。根本原因在于它必须把整个文件加载进内存再解析成Python对象,而真实场景中日志、ETL导出、API响应归档等数据动辄上G,结构却往往只是大量同构对象(如每行一个{...},或顶层是大数组)。别硬扛,换流式解析。
关键判断:如果JSON是“行分隔格式”(NDJSON/JSONL),直接按行读+json.loads();如果是单一大数组([{...},{...},...]),就得用支持增量解析的库,比如ijson。
- NDJSON场景:用
asyncio.open()逐行异步读,每行调json.loads(),不占额外内存 - 标准大数组场景:
ijson.parse()返回生成器,配合asyncio.to_thread()把阻塞解析挪到线程池,避免阻塞事件循环 - 绝对不要在协程里直接调
json.load()或ijson.items()(后者内部会缓冲)
为什么ijson的parse()比items()更适合异步流式处理
ijson.items()看着简洁(比如ijson.items(f, 'item')),但它内部会预读、缓存、回溯,实际仍可能吃光内存,且是纯同步阻塞调用,扔进asyncio.to_thread()也救不回吞吐量。而ijson.parse()暴露底层事件流((prefix, event, value)三元组),你能精确控制什么时候推进、什么时候暂停,和asyncio.StreamReader或自定义异步迭代器天然契合。
- 示例:解析
users.item.name字段时,只监听start_map→map_key→string这一小段事件链,跳过无关字段 - 用
async for包装ijson.parse()需配合asyncio.to_thread()读文件块,不能直接await解析器 -
ijson.parse()本身不读文件,你得自己分块传入字节流,这对控制背压(backpressure)很关键
asyncio下如何安全地把ijson.parse()接入事件循环
核心矛盾:文件IO和ijson.parse()都是同步的,但你要不阻塞主线程。解法不是“把ijson变async”,而是分层解耦——IO走asyncio.to_thread(),解析逻辑用状态机驱动,事件到达后立即处理,不攒批。
async def stream_json_objects(file_path: str):
def _parse_chunk(chunk: bytes):
parser = ijson.parse(io.BytesIO(chunk))
# 手动状态机提取对象,不依赖ijson.items
for prefix, event, value in parser:
if (prefix, event) == ('', 'start_array'):
continue
if (prefix, event) == ('', 'end_array'):
break
# ... 状态流转逻辑,yield dict对象
async with aiofiles.open(file_path, 'rb') as f:
while True:
chunk = await f.read(65536) # 每次读64KB
if not chunk:
break
yield await asyncio.to_thread(_parse_chunk, chunk)
- 每次
read()大小要权衡:太小导致系统调用频繁,太大又失去流式意义 -
_parse_chunk里不能直接yield,必须收集完本块内所有完整对象再返回列表,否则异步迭代器无法保证原子性 - 若JSON含嵌套数组(如
users.item.orders.item),状态机复杂度指数上升,此时应考虑先用jq命令行预处理切分
NDJSON场景下用asyncio逐行解析的实际性能瓶颈在哪
看起来最简单的NDJSON(每行一个JSON对象),异步读行+json.loads()却常比同步版本还慢。问题不在json.loads(),而在默认的aiofiles或asyncio.StreamReader行读取方式引入了大量小buffer拷贝和协程调度开销。
- 实测:用
asyncio.StreamReader.readline()读10万行NDJSON,比同步for line in file慢2–3倍 - 优化路径:改用
aiofiles的read(65536)批量读,再用bytes.split(b'\n')手动切行,避免逐行等待 - 更狠一招:用
uvloop+ujson替换json.loads(),解析速度可提升3–5倍,但ujson不支持object_hook等高级特性 - 注意:行末可能缺换行符(最后一行),
split后需检查残留字节并拼接到下次读取
真正复杂的不是语法解析,而是如何让“流”不变成“堵车流”——背压控制、缓冲区大小、解析与消费速率匹配,这些细节没调好,异步反而成负优化。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











