news 2026/10/13 1:10:16

JSON文件读写全流程解析:编码、序列化与大文件处理实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSON文件读写全流程解析:编码、序列化与大文件处理实战

JSON文件读写操作详解

JSON文件读写,几乎是每个开发者每天都会碰到的基础操作。不管你是做前端、后端、脚本自动化,还是数据清洗,只要涉及数据交换、配置存储、日志导出,JSON就是绕不开的格式。这个格式简单到看一眼就会,但实际写起来坑一点也不少——中文乱码、浮点精度丢失、大文件加载卡死、编码不统一,随便遇到一个就够你折腾半天。

这篇内容就围绕JSON文件读写的全流程,从格式特性、选型对比,到Python/JavaScript/Java/Go里的实际读写写法,再加上我这些年踩过的坑和排查思路,一次性讲透。适合刚接触JSON的初学者,也适合想查漏补缺的资深开发。

1. 内容整体设计与思路拆解

1.1 为什么是JSON,以及它解决了什么问题

JSON全称是JavaScript Object Notation,但今天它早已不是JavaScript的专属,而是一种跨语言的数据交换格式。它的核心设计目标就两个:对人可读,对机器易解析。

对比一下常见的几种数据格式。XML是早期的数据交换主力,但它的标签嵌套太冗余,一段很简单的数据写出来动辄上百字节,解析起来也重。YAML可读性很好,靠缩进表达层级,但格式敏感度高,一个空格不对就解析失败,而且在跨语言实现上各有差异,容易踩坑。INI适合简单键值对,一旦数据嵌套层级深就力不从心。而JSON用花括号、中括号、冒号、逗号四种符号解决了嵌套和数组的问题,语法规则简单到可以用几行正则做粗略解析,完整解析器在各语言里都是标准库自带,这种"零依赖、零成本"的特性是它普及的最大原因。

我见过不少团队在做配置系统选型时纠结,最后都会回到JSON上。原因很朴素:JSON的容错性比YAML好,表达力比INI强,体积比XML小,心智负担在所有格式里最低。它不完美,但综合成本最优。

1.2 读写方案选型背后的核心思路

JSON读写的本质,是"文本 ↔ 内存对象"的互相转换。读文件就是按某种字符编码把文本读进来,解析成当前语言里的字典、数组、字符串、数字、布尔、null。写文件的过程恰好相反,把一个内存对象序列化成JSON文本,再按编码写进文件。

这里有一个关键认知:JSON文本必须符合UTF-8编码,但JSON标准本身只规定了字符编码,并没有规定文本换行风格。实际项目中,Linux和macOS下文件用\n换行,Windows下用\r\n换行,大部分解析器都能自动处理。真正容易出问题的是文件开头有BOM头,或者文件里混入了不可见字符,导致首字符解析失败。这一点我在后面的常见问题里会详细说。

选型上,Python开发者直接用标准库json即可,不要轻易引入第三方解析库,标准库在性能和兼容性上已经够用。JavaScript用JSON.parse和JSON.stringify,Node环境则用fs模块配合。Java用Jackson或Gson,Go则用encoding/json标准库。你只需要记住一个原则:优先用语言自带的标准库,只有当标准库满足不了性能或定制需求时,再考虑重量级方案。

2. 核心细节解析与实操要点

2.1 JSON数据结构与类型映射关系

JSON一共有六种值类型:对象、数组、字符串、数字、布尔、null。任何一种编程语言内置类型和JSON类型之间,都有一套映射规则,掌握这套规则是读写不出错的前提。

以Python为例,下面这个表格是标准库json模块的映射关系:

JSON类型Python类型说明
objectdict键必须是字符串
arraylist有序序列
stringstr字符串,必须是双引号包裹
number (int)int整数
number (real)float浮点数
true / falsebool布尔值
nullNone空值

有一个常见误区:JSON对象的键必须是字符串,并且必须用双引号,不能用单引号。很多新手拿Python的字典直接str()转字符串往里写,结果写出去的是Python语法,不是JSON语法,再读回来就报错。

JavaScript这边的映射更加自然:对象字面量几乎就是JSON本身,但有两处区别容易被忽略:JSON字符串必须是双引号,而JavaScript对象允许单引号和无引号属性名;JSON里不允许有注释、尾逗号,但JavaScript对象里都可以有。

2.2 编码规则与ensure_ascii的深度理解

读JSON文件时,文件内容会被按指定的encoding解码成字符串,再交给解析器。写JSON文件时,序列化后的字符串再按encoding编码写入文件。这里的编码问题,是中文开发者踩坑最多的地方。

Python的json.dumps()默认ensure_ascii=True,意思是会把所有非ASCII字符转义成\uXXXX形式。这就导致一个诡异现象:内存里的字符串明明是"用户",写进文件里却变成"\u7528\u6237"。文件内容是合法的JSON,工具也能解析,但人眼完全看不懂。很多人在第一次遇到的都很困惑,其实解法很简单,设置ensure_ascii=False即可。

我建议团队内统一规范:所有JSON读写操作,要么统一ensure_ascii=False保证文件可读,要么统一默认转义保证文件纯ASCII。最怕的是项目里两种混用,同一个配置文件,某天被某个脚本处理过后,里面原本明文的中文全部变成转义序列,diff看起来像整个文件被改了一遍。

2.3 缩进、分隔符与sort_keys的实战意义

格式化参数看似无关紧要,实际上直接影响文件的可用性。json.dumps()里的indent参数控制缩进,生产环境建议统一为2或4空格,不要用制表符。原因有两点:一是在Git等版本控制工具里,tab和空格混用会导致diff混乱;二是不同编辑器显示tab宽度不一致,代码Review时对齐错位很头痛。

separators参数控制分隔符,紧凑模式写成separators=(',', ':'),可读性差但体积最小,适合网络传输。格式化模式写成separators=(',', ': '),注意冒号后多了一个空格,可读性好很多。

sort_keys=True是容易被忽视的实用参数。对字典的键排序后输出,效果是同一份数据、无论谁来序列化,只要数据内容相同,输出的字节就是一致的。这在做缓存哈希、文件对比、接口签名验证时有奇效。我维护的一个配置生成系统就强制开启了sort_keys,极大降低了配置文件变更的噪音。

3. 实操过程与核心环节实现

3.1 Python读写JSON的标准姿势

直接上代码,这是我在项目里最常用的写法。读文件部分:

import json # 最简读取方式 with open('config.json', 'r', encoding='utf-8') as f: data = json.load(f) # 如果你需要拿到原始文本做额外处理 with open('config.json', 'r', encoding='utf-8') as f: raw_text = f.read() data = json.loads(raw_text)

这里必须强调encoding='utf-8'不是可有可无的。虽然Python 3里文本模式默认编码跟平台有关,Windows下默认是gbk,不显式指定就会出现UnicodeDecodeError。养成显式写编码的习惯,能让代码在Linux、macOS、Windows上表现一致。

写文件部分:

with open('output.json', 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2, sort_keys=True)

或者先序列化成字符串再手动写入:

text = json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True) with open('output.json', 'w', encoding='utf-8') as f: f.write(text)

两种方式效果一样。区别在于,先dumps再写入,你可以对序列化结果做进一步处理,比如加密、压缩、加前缀,灵活度更高。一次性dump则代码更简洁,但无法拿到中间产物。

3.2 大JSON文件不卡内存的流式读取写法

遇到几百MB甚至GB级别的JSON文件时,直接json.load()会把整个文件加载进内存,轻则卡顿,重则直接把进程杀死。实测一个1.5GB的JSON文件,直接用json.load()读取,内存峰值轻松超过4GB。

解决思路有两种。如果你能控制数据格式,最好把整体数据拆分成更小的JSON对象,每行一个,即所谓JSON Lines格式。然后流式逐行读取:

import json with open('data.jsonl', 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line: continue record = json.loads(line) # 逐条处理,内心毫无压力 do_something(record)

如果数据结构不能改,就必须用流式解析方案。Python标准库的json.JSONDecoder暴露了一个raw_decode方法,可以从文本中解析一个JSON对象并返回剩余部分,基于它可以做流式处理而不必全量加载:

import json decoder = json.JSONDecoder() with open('huge_data.json', 'r', encoding='utf-8') as f: chunk = f.read(8192) buf = '' while chunk: buf += chunk try: # 从缓冲开头解析一个完整JSON对象 obj, end = decoder.raw_decode(buf.lstrip()) do_something(obj) buf = buf[end:].lstrip() except json.JSONDecodeError: # 缓冲数据不够,继续读 pass chunk = f.read(8192)

精度上会有一点瑕疵,如果字符串里的引号跨了缓冲区边界,可能会出现解析失败的抖动。但在绝大多数场景下这样已经能应付。实际项目里我更推荐前者,JSON Lines格式不管在写入、读取、日志清洗上都更友好。

3.3 其他主流语言的JSON读写速查

JavaScript在浏览器环境:

// 读取(需要配合fetch请求) const resp = await fetch('./data.json'); const data = await resp.json();

Node.js环境更直接:

const fs = require('fs'); // 读取 const raw = fs.readFileSync('./data.json', 'utf-8'); const data = JSON.parse(raw); // 写入 fs.writeFileSync('./output.json', JSON.stringify(data, null, 2), 'utf-8');

浏览器环境里JSON.stringify(data, null, 2)中的第三个参数2就是缩进空格数,写JSON文件时非常实用。

Java使用Jackson库:

ObjectMapper mapper = new ObjectMapper(); // 读取 MyClass obj = mapper.readValue(new File("data.json"), MyClass.class); // 写入 mapper.writerWithDefaultPrettyPrinter().writeValue(new File("output.json"), obj);

如果只是通用Map类型,可以用mapper.readValue(new File("data.json"), new TypeReference<Map<String, Object>>() {})。

Go语言使用标准库:

import ( "encoding/json" "os" ) // 读取 var data map[string]interface{} raw, err := os.ReadFile("data.json") if err != nil { panic(err) } json.Unmarshal(raw, &data) // 写入 out, err := json.MarshalIndent(data, "", " ") if err != nil { panic(err) } os.WriteFile("output.json", out, 0644)

Go写JSON时要留意:MarshalIndent的第三个参数是缩进字符串,我习惯用两个空格,也就是" "。结构体字段需要导出(大写开头)才能被json包序列化,这是个经典的入门坑。

3.4 自定义对象序列化的高级用法

日常业务里,你经常会遇到语言内置类型无法直接表达的场景。比如Python的datetime对象,直接json.dumps()会报TypeError。这时候需要自定义编码器。

import json from datetime import datetime class CustomEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, datetime): return obj.isoformat() if isinstance(obj, bytes): return obj.decode('utf-8') # 兜底交给父类 return super().default(obj) data = { 'name': '某跨平台应用', 'version': '1.2.3', 'release_time': datetime.now(), } with open('meta.json', 'w', encoding='utf-8') as f: json.dump(data, f, cls=CustomEncoder, ensure_ascii=False, indent=2)

反过来,读取时要把ISO格式字符串还原成datetime,就需要object_hook参数:

def custom_decoder(dct): if 'release_time' in dct: dct['release_time'] = datetime.fromisoformat(dct['release_time']) return dct with open('meta.json', 'r', encoding='utf-8') as f: data = json.load(f, object_hook=custom_decoder)

这种方式在处理嵌套结构时非常丝滑,能自动递归调用每一层字典。团队里一旦有自定义对象序列化的需求,应该第一时间封装成公共工具类,否则每个模块各写各的,到后面对不齐格式,几小时都排查不过来。

4. 常见问题与排查技巧实录

4.1 中文乱码与\u转义序列问题

这个坑出现频率最高。打开写好的JSON文件,看到一堆\u7528\u6237,第一反应是文件坏了,其实文件是好的,只是被默认的ensure_ascii转义了。

排查思路很简单:先用文本编辑器打开看原始内容,如果看到\u开头的一串符号,说明写入时用了ensure_ascii=True;如果看到乱码,说明读取或写入时的encoding不一致。

解决方案按场景区分:配置文件、日志文件、数据交换文件,建议都用ensure_ascii=False并显式指定utf-8;如果文件要传输到只支持ASCII的系统里,那就保留默认转义,因为解析后内容还是正确的中文。最忌讳的是写的时候ensure_ascii=False,读的时候不指定encoding,在Windows上就会因为gbk解码造成UnicodeDecodeError。

4.2 浮点精度问题与NaN/Infinity处理

JSON里浮点数的序列化,在不同语言里有细微差异。Python的json.dumps()默认会把float序列化成尽量短的字符串,但某些小数在二进制浮点体系里本身就是近似值。例如0.1 + 0.2,在内存里是0.30000000000000004,序列化出去就带着这串尾巴。

如果你的业务对精度要求高,比如金额计算,不要用浮点数,直接存整数分;或者用字符串表达,读取时再转Decimal。这个决策要在设计阶段就定下来,不要等数据写坏了再回头修。

非标准JSON还有个隐蔽问题:Python的json.dumps()默认允许输出NaN、Infinity、-Infinity这些字面量。但JSON标准根本不含这三个值。写出来的文件用Python自己读没问题,换到JavaScript的JSON.parse、Java的Jackson、Go的encoding/json,直接就解析失败。跨语言交换时一定要设置allow_nan=False,宁可让它抛异常,也绝不让非标准内容进文件。

4.3 BOM头与不可见字符导致解析失败

有时候你从Windows工具导出的JSON文件,看起来完全正常,但一用json.load()就报Expecting value: line 1 column 1。最常见的原因是文件开头有UTF-8 BOM(\ufeff字符)。

用Python处理很简单:

with open('config.json', 'r', encoding='utf-8-sig') as f: data = json.load(f)

utf-8-sig编码会自动剥掉BOM头。如果你必须用utf-8模式读取,那就手动清理首字符:

with open('config.json', 'r', encoding='utf-8') as f: text = f.read() if text.startswith('\ufeff'): text = text[1:] data = json.loads(text)

另外,用curl或脚本从HTTP接口拉取JSON数据后,偶尔会混入控制字符、前后空格、甚至多个JSON对象粘连的情况。遇到这类脏数据,raw_decode比loads更从容。

4.4 超大JSON的加载性能瓶颈

前面提到过大文件内存问题,这里补充一个容易被忽略的性能场景:即使文件只有几十MB,如果嵌套层级特别深,json.loads的递归解析也可能触发Python递归深度限制,抛RecursionError。默认递归深度上限是1000,深层嵌套的配置文件在极端情况下会撞上。

遇到这种情况,要么调整递归上限(sys.setrecursionlimit,但要小心栈溢出),要么改造数据结构拍平嵌套。后者才是正路。JSON本身就不适合表达深度嵌套的数据模型,一旦超过十层,就该认真反思设计了。

4.5 JSON Schema校验与脏数据兜底

配置类JSON文件,最怕的就是结构变了而旧文件还在线上。某个键从字符串改成了数组,代码里直接按下标取,运行时就崩了。

我建议对所有配置类JSON文件加上Schema校验。Python有现成的jsonschema库,定义一套约定结构:

import jsonschema schema = { "type": "object", "properties": { "server": { "type": "object", "properties": { "host": {"type": "string"}, "port": {"type": "integer", "minimum": 0, "maximum": 65535} }, "required": ["host", "port"] } }, "required": ["server"] } with open('config.json', 'r', encoding='utf-8') as f: config = json.load(f) jsonschema.validate(config, schema)

加上校验之后,配置缺失或者类型错误会在启动阶段就暴露,而不是运行到某个边缘逻辑才炸出来。代价是每次启动多几十毫秒的校验时间,换来的是稳定的运行期,这笔账非常划算。

5. 工具链与实战效率提升

5.1 jq命令让你在终端直接处理JSON

排查JSON问题,不一定都要写脚本。命令行工具jq是处理JSON的瑞士军刀,几乎所有Linux发行版的软件源里都有,macOS可以用包管理器装,Windows也有Win64版本。

常用操作,比如格式化一个压缩的JSON文件:

jq '.' minified.json > pretty.json

提取某个字段:

jq '.server.port' config.json

过滤数组成员:

jq '.rows[] | select(.status == "error")' data.json

统计数组长度:

jq '.items | length' data.json

调试时对着一个JSON文件反复jq和grep,效率远高于打开编辑器逐段看。而且jq天然容忍BOM头,是快速判断文件是否合法的好工具。

5.2 标准库之外的序列化策略

有个常见误解:JSON读写就是dump和load两个函数的事。实际上很多项目里JSON被滥用——日志用JSON、缓存用JSON、消息队列也用JSON,每种场景的读写偏好不一样。

日志场景,应该用JSON Lines,每条日志一行,方便grep和按行切割。缓存场景,要关注序列化速度,可以用带separators=(',', ':')的紧凑模式,把体积压到最小。消息队列场景,必须保证跨语言兼容,坚决不用NaN之类非标准内容,也不用依赖特定键排序。

如果你需要一个二进制格式做缓存复用,可以考虑MessagePack或ProtoBuf,它们的结构化数据比JSON紧凑得多。但代价是调试困难,所以我的建议很明确:跨服务边界、需要人眼排查的数据,老老实实用JSON;内部性能敏感且不需要人读的数据,再用二进制方案。

5.3 安全性与输入校验的最后一公里

JSON解析还有个安全陷阱:攻击者可以构造一个递归嵌套的JSON字符串,让解析器陷入深度递归,形成另一种形式的拒绝服务。json.loads在Python里对嵌套深度有限制,但限制值偏高,在高安全要求的场景需要主动校验长度和深度。

另一个安全隐患是,用eval()或类似动态执行方式解析JSON,这是绝对不能碰的。JSON解析应该用专门的解析器,在Python里就是json.loads,在JavaScript里就是JSON.parse,任何拼接代码字符串再执行的方案都是给自己挖坑。

对于从不可信来源获取的JSON,建议在解析前做基础卫生处理:限制文件大小、校验整体结构、过滤危险字段、关闭NaN兼容。这一套做完,JSON读写这块才算真正踏实。

6. JSON读写长期维护的经验总结

我做了这么多年的项目,JSON读写代码写得越久,越倾向于把细节收敛成标准工具函数。团队里定义一个load_json_file和dump_json_file工具,统一封装编码、缩进、错误处理,整个项目的JSON行为就稳定了。这套标准一旦跑起来,新加入的同学不用理解底层细节,直接调用工具即可。

一个很典型的实践是初始化时统一注册自定义编码器。某些对象类型从项目第一天就需要序列化,但不同模块的开发者可能各自在自己的代码里添加了定制逻辑,结果同样是datetime对象,有的写成ISO8601字符串,有的写成时间戳,有的写成自定义格式。读取时互相交叉就会出错。与其等出问题再救火,不如把序列化方案直接写在公共工具函数里,想绕都绕不开。

再一个容易忽略的点是:JSON文件的读写不能只依赖单元测试,还要定期做真实文件回放测试。用生产环境导出的文件样本,跑一遍全链路的读写逻辑,专门验证那些历史毛病——超大文件、含BOM、嵌套超深、特殊转义字符、跨平台换行。这种语料式测试方法,比写一百个纯构造数据的测试用例都管用。

最后说一个很多人没意识到的技巧:把JSON文件的Schema版本字段从一开始就设计好。在根节点加一个"schema_version": 1,后续结构升级时,旧文件可以通过版本号触发迁移逻辑。这个字段每个文件只占十几个字节,但它能让JSON文件的向下兼容问题在代码层面彻底解决。很多项目就是没做这一步,发布新版本后老配置全部失效,用户只能手动改文件,体验极差。

JSON读写不是高技术含量的东西,它的价值全在细节的稳定性和一致性上。把上面的问题全部规避掉,你的JSON读写就是项目里最让人放心的那部分代码。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 1:10:07

PN结原理与工程实践:从载流子到反向恢复的深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:10:05

KEA128串口不定长数据接收:接收中断+定时器超时方案详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:09:56

工控现场排障必知:8种自动化控制信号原理、选型与故障排查实战

1. 工控信号入门&#xff1a;为什么这8种信号是现场排障的命根子干工控这行十来年&#xff0c;我越来越觉得&#xff0c;现场排故快不快&#xff0c;很多时候不取决于你PLC编程多溜、上位机做得多花哨&#xff0c;而取决于你对控制信号的理解有多深。设备不动了&#xff0c;到底…

作者头像 李华
网站建设 2026/10/13 1:09:40

Spring纯注解开发全解析:从XML迁移到Bean管理实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:09:07

ACM刷题的本质:工程问题求解能力训练体系

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:08:32

AnyPS5跨平台适配实战:从需求拆解到性能优化的完整方法论

1. 从"AnyPS5"这个名字说起&#xff1a;它到底想解决什么问题第一次看到"AnyPS5"这个标题&#xff0c;我脑子里蹦出来的第一个念头是&#xff1a;这大概率是一个围绕"跨平台运行"或者"通用化改造"的项目。为什么这么说&#xff1f;因为…

作者头像 李华