Python 文件截断 JSONDecodeError 速查:写入中断、原子写与校验和
更新时间:2026-10-24。FAQ 速查页,面向定时任务跑完后
json.load抛Expecting value、或者读出来的配置文件只剩一半的同学。Python 文件写一半的本质是「写路径上任何一环断掉,读者拿到的就是残缺字节」:进程被杀时缓冲区还没落盘、多进程同时往一个文件写互相踩踏、磁盘满了 write 只成功了一半、以及直接覆盖原文件导致读者撞上「正在写」的窗口。病因收敛在六根轴上:写入被中断截断、open后忘记 close 导致数据还躺在缓冲区、并发写同一文件、磁盘或配额写满、文本/字节模式混用造成编码截断、以及缺乏原子写让半成品直接暴露。顺一遍基本必中。如果是路径压根不对而不是文件内容残缺,转 Python 文件路径不存在速查。
本文要回答的问题
json.decoder.JSONDecodeError: Expecting value: line 1 column 1是文件空了吗?- 程序明明
write了,为什么文件是 0 字节或只有一半? - 多进程写同一个结果文件,内容互相覆盖怎么治?
- 怎么确认磁盘写满是不是凶手?
- 原子写到底怎么写才真的原子?
一、报错原文与典型现象
截断类报错集中在读者一侧,写的时候往往悄无声息(路径一律以 /proj/demo 占位):
# 读到空文件或空白
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
# 读到半个 JSON
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 12 column 8 (char 341)# 写盘时直接抛错(磁盘满)
OSError: [Errno 28] No space left on device
# 进程被信号杀掉前的最后痕迹
KeyboardInterrupt 或 killed,文件停在 341 字节读现象先分两类:Expecting value: line 1 column 1 (char 0) 表示文件是空的或开头就是空白——写入压根没落盘;Expecting ',' delimiter 出现在文件中段,表示写了开头没写结尾——典型的中断截断。ls -l 看一眼文件大小是不是比平时小,基本就能定性。
二、病因处置速查表
| 报错现象 / 场景 | 可能原因 | 快速验证 | 处置 |
|---|---|---|---|
| char 0 报错、文件 0 字节 | write 后未 close,缓冲区没落盘 | 代码里搜 open 是否配 with | 用 with open 包裹,或显式 flush |
| 文件中段截断 | 进程写一半被杀/崩溃 | 看日志最后一条停在何处 | 重跑并加超时看护;改用原子写 |
| 间歇性内容错乱、两半拼接 | 多进程并发写同一文件 | 查是否有多个 worker 写同路径 | 各写临时文件再汇总,或加锁 |
| OSError Errno 28 | 磁盘或 inode 写满 | df -h 与 df -i 看挂载点 | 清理空间,写前检查剩余量 |
| 中文部分变乱码后截断 | 文本/字节模式混用或编码不一致 | file 命令看实际编码 | 统一 encoding=utf-8 读写 |
| 读者偶发读到半个文件 | 直接覆盖原文件,读写撞窗口 | 写端是否直接 open 目标路径 | 临时文件 + os.replace 原子换名 |
三、排查决策图
下面这张图是标准排查路线,颜色按角色区分:红是报错入口与命中的根因,蓝是排查动作,绿是通过分支,橙是修复动作,紫是防复发沉淀。
读这张图抓分水岭:报错位置直接给方向——char 0 走「没写进去」一支,重点查缓冲区与异常路径;中段截断走「写了一半」一支,重点查进程退出原因、并发与磁盘水位。两条支路最终都汇到同一个修复动作:原子写。
四、逐条排查展开
char 0 空文件,九成是缓冲区没落盘。 Python 的文件对象默认带缓冲区,f.write(data) 只是把数据塞进内存缓冲,真正落盘发生在 close() 或 flush() 时。如果 write 之后进程被 kill -9、机器断电、或者代码路径上抛异常绕过了 close,文件留在磁盘上的就是 0 字节。修法一句话:with open(path, "w") as f: 包起来,缩进结束自动 close;长任务中途想让数据可见,显式 f.flush(),极端场景再补 os.fsync(f.fileno()) 把数据推到内核之外。
中段截断,先问进程是怎么死的。 文件写到一半断掉,最常见三种死法:被超时看护杀掉(cron 配的超时、CI 的 step timeout)、自己抛异常退出但异常被上层吞了、内存打满被 OOM killer 干掉。排查顺序:看任务日志最后一行停在哪、系统日志里有没有 OOM 记录、再确认写文件的代码是不是在 try 块里被 except 吞掉。死法找到之前不要改写入逻辑,否则治标。
并发写同一文件,内容必然错乱。 两个进程同时 open(path, "w"),各自维护自己的文件偏移量,写出来的就是两半拼接甚至互相覆盖。典型场景:多个 pytest worker 或 celery worker 往同一个结果文件追加。处置:每个进程写自己的临时文件(文件名带 pid 或 uuid),最后由单点汇总;必须共享时上 fcntl.flock 文件锁,跨机器共享就用数据库或对象存储,别用共享文件当消息队列。
Errno 28 不只看容量,还要看 inode。 df -h 显示还有空间却仍报 No space left,多半是 inode 耗尽(海量小文件)或目录配额到顶,df -i 一看便知。写关键结果文件前,可以用 shutil.disk_usage 预估剩余量,不够就先归档旧文件。
原子写是根治,不是加分项。 所有「读者读到半成品」的问题,根源都是直接对目标路径做覆盖写。标准姿势:写到同目录的临时文件,写完 flush 并 close,然后 os.replace(tmp, target)——同文件系统内 rename 是原子操作,读者要么看到旧文件要么看到新文件,永远看不到中间态。注意临时文件必须与目标在同一挂载点,跨文件系统的 rename 会退化成拷贝。
校验和给截断兜底。 对关键产物文件,写盘时在旁边落一个 .sha256 文件(内容即哈希值),读者先校验再解析;校验不过就按文件不存在处理并重跑。这条对网络盘与对象存储尤其重要——那些环境下 write 成功不代表数据真的完整落盘。
五、规范做法与预防
- 写文件一律 with open:禁止裸 open 后靠自觉 close,代码评审把这条当硬规则。
- 关键产物原子写:公共函数封装「临时文件 + flush + os.replace」,全项目只准走这一个入口。
- 并发各写各的:多进程产物文件名带 pid/时间戳,汇总阶段单线程合并。
- 写前查水位:
shutil.disk_usage预估,磁盘占用过 85% 告警。 - 校验和随行:重要文件配
.sha256,读者先验后用。 - 异常路径也落盘:长任务在 except 里补一次 flush,把已算出的中间结果留住。
这一篇在知识体系里的位置
本篇是 Python 排错速查簇的文件完整性入口篇:文件压根找不到看 Python 文件路径不存在速查;写入直接报权限错看 Python 权限不足速查;磁盘写满的系统侧排查看 Python 磁盘写满速查;编码不一致导致的乱码看 Python 编码与 locale 速查。「读者侧报错 → 定空还是截半 → 缓冲/中断/并发/磁盘四选一 → 原子写根治」这条主线对一切文件残缺类报错通用。
一句话总结
文件写一半的排查从报错位置入手:char 0 空文件查缓冲区没落盘,一律改成 with open 管生命周期;中段截断查进程死法(超时、异常吞掉、OOM),死法没找到别动写入逻辑;内容错乱查并发写,各写临时文件再汇总;Errno 28 查容量加 inode 双水位。根治只有一招——同目录临时文件加 os.replace 的原子写,读者从此只见完整文件;关键产物再配 sha256 校验和兜底。把原子写封装成公共函数入库,这类报错基本绝迹。