JSON 数据处理与操作
一. 认识JSON与Python类型的映射关系
JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式。
在 Python 中,我们不需要安装第三方库,直接导入内置的 json 模块即可:
python
import json
核心概念:JSON 本质上只是一串"带有特定格式的文本字符串"。在代码中操作时,我们需要将其与 Python 内存中的数据结构(字典、列表)相互转换。
| JSON 数据类型 | 对应的 Python 数据类型 |
|---|---|
object ( {} ) |
dict (字典) |
array ( [] ) |
list (列表) |
string ( " " ) |
str (字符串,JSON 强制要求双引号) |
number |
int 或 float (整型或浮点型) |
true / false |
True / False (布尔值,Python首字母大写) |
null |
None (空值) |
二. 前置知识:Python 原生文件操作 (open & with...as)
在将数据写入磁盘或从磁盘读取之前,我们必须先通过操作系统"打开"目标文件,建立数据传输管道。在 Python 中,这通常由内置的 open() 函数配合 with ... as 语法来完成。
1. open - 打开文件对象
作用:在内存和本地磁盘文件之间建立一条连接,返回一个可操作的文件对象。注:使用纯 open() 打开文件后,程序使用完毕时必须显式调用 .close() 关闭文件,否则会占用系统资源甚至导致文件损坏!
python
open(file, mode='r', encoding=None)
参数:
- 文件路径 (file): 需要打开的目标文件路径(str 或 Path 对象)。
- 操作模式 (mode): 指定打开文件的目的(str)。深度学习中最常用的有:
'r': 只读模式(默认)。文件必须存在,用于读取数据。'w': 覆盖写入模式。文件不存在则创建,文件若存在则完全清空原内容再写!'a': 追加写入模式。文件不存在则创建,存在则在文件最末尾继续追加内容。
- 字符编码 (encoding): 指定文本的编解码方式。处理含中文或特殊符号的数据集时,强烈推荐固定设为
'utf-8'(str)。
返回值:
- 成功: 返回一个可供读写操作的文件指针对象(通常为 TextIOWrapper)。
示例:
python
# 传统写法(极不推荐,极其容易忘记 close 导致文件被占用)
f = open("test.txt", "w", encoding="utf-8")
f.write("Hello World!")
f.close() # 必须手动执行这一句!
2. with ... as - 优雅的上下文管理器
作用:with 语句用于包装带有生命周期管理的对象(如文件读写、数据库连接)。它的核心魅力在于:无论缩进的代码块内部是否发生报错崩溃,只要代码脱离了 with 的缩进层级,Python 都会百分之百自动帮你执行 .close() 关闭并保存文件!
python
with open(file, mode, encoding) as f:
# 在这里的缩进块内对文件 f 进行读写操作
pass
# 缩进结束,f 自动关闭
参数:
- 无特定函数参数,这是 Python 的语法关键字。
as后面紧跟的是给这个文件对象起的变量名(科研代码中习惯简写为f、file或fin/fout)。
返回值:
- 无直接返回值。
示例:
python
# 现代标准写法(极度推荐,安全且优雅)
with open("test.txt", "w", encoding="utf-8") as f:
f.write("Hello World!")
# 不需要写 f.close(),缩进结束时系统自动安全关闭文件
三. 内存中:字符串与对象的相互转换 (dumps & loads)
带有 s (代表 string) 的方法,用于在内存中将"Python 对象"和"JSON 格式的字符串"相互转换。
1. json.dumps - 将对象序列化为 JSON 字符串
作用:将 Python 对象(如字典、列表)转化为 JSON 格式的字符串。常用于打印输出或准备写入文件。
python
json.dumps(obj, ensure_ascii=True, indent=None)
参数:
- 对象 (obj): 需要被转换的 Python 对象(通常是
dict或list)。 - 保证ASCII (ensure_ascii): 【极其重要】 如果数据中包含中文,必须设置为
False!否则中文会被转码为\uXXXX格式的乱码!(bool,默认为 True) - 缩进 (indent): 格式化输出。指定缩进的空格数,让打印出来的 JSON 具有美观的层级结构(int 或 None)。
返回值:
- 成功: 返回一个符合 JSON 规范的字符串(str)。
示例:
python
data = {"instruction": "你好", "output": "世界"}
# 默认转换 (中文会变乱码,且没有换行)
res1 = json.dumps(data)
# '{"instruction": "\\u4f60\\u597d", "output": "\\u4e16\\u754c"}'
# 深度学习常用标准参数:不转译中文,且缩进 4 格对齐
res2 = json.dumps(data, ensure_ascii=False, indent=4)
# {
# "instruction": "你好",
# "output": "世界"
# }
print(type(res2)) # <class 'str'>
2. json.loads - 将 JSON 字符串反序列化为对象
作用:将符合 JSON 语法的文本字符串,解析为 Python 内存中的真实对象(如字典)。
python
json.loads(s)
参数:
- 字符串 (s): 包含合法 JSON 数据的文本字符串(str)。
返回值:
- 成功: 返回解析后的 Python 对象(通常是 dict 或 list)。
示例:
python
json_str = '{"model": "llama-3", "parameters": 70}'
# 解析回 Python 字典
parsed_data = json.loads(json_str)
print(parsed_data["model"]) # 输出: llama-3
print(type(parsed_data)) # <class 'dict'>
四. 磁盘上:文件的读取与写入 (dump & load)
不带 s 的方法,必须配合上文提到的 open() 文件指针使用,直接将对象塞入文件,省去了手动读写字符串的步骤。
1. json.dump - 将对象直接写入 JSON 文件
作用:将 Python 对象直接序列化并写入到本地 .json 文件中。
python
json.dump(obj, fp, ensure_ascii=True, indent=None)
参数:
- 对象 (obj): 需要保存的 Python 数据对象(Any)。
- 文件指针 (fp): 通过
with open(..., 'w')打开的文件对象(TextIOWrapper)。 - 保证ASCII (ensure_ascii) / 缩进 (indent): 用法与
dumps完全一致。
返回值:
- 成功: 无返回值(NoneType),数据被安全保存在本地。
示例:
python
config = {"learning_rate": 2e-5, "batch_size": 32, "model_name": "Qwen-7B"}
# 使用 with open 语法安全地打开文件写入
with open("training_config.json", "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=4)
2. json.load - 直接从 JSON 文件读取对象
作用:读取本地 .json 文件的内容,并直接解析为 Python 对象。
python
json.load(fp)
参数:
- 文件指针 (fp): 通过
with open(..., 'r')打开的文件对象(TextIOWrapper)。
返回值:
- 成功: 返回解析后的 Python 对象(通常是 dict 或 list)。
示例:
python
# 读取刚才保存的配置文件
with open("training_config.json", "r", encoding="utf-8") as f:
loaded_config = json.load(f)
print(loaded_config["learning_rate"]) # 2e-05
五. 深度学习科研特供:JSONL 文件处理
在大模型训练(如 SFT 微调)时,极少使用标准的单个大 .json 文件。 因为当数据集包含百万条对话时,整体读取一个 JSON 文件会撑爆内存。
行业标准是使用 .jsonl(JSON Lines)格式:每一行都是一个独立的、合法的 JSON 字典对象。
1. 读取 JSONL 数据集
思路:利用标准的 open() 逐行遍历文件,然后对取出的每一行字符串使用 json.loads() 进行解析。
python
dataset = []
# 逐行读取 JSONL 文件
with open("sft_data.jsonl", "r", encoding="utf-8") as f:
for line in f:
# 去除行末换行符,并解析该行为字典
record = json.loads(line.strip())
dataset.append(record)
print(f"总共加载了 {len(dataset)} 条训练数据!")
2. 写入 JSONL 数据集
思路:遍历 Python 列表中的每一个字典,用 json.dumps() 转化为单行字符串后写入,并手动加上换行符 \n。
python
results = [
{"prompt": "1+1=?", "response": "2"},
{"prompt": "Python的作者是谁?", "response": "Guido van Rossum"}
]
# 追加写入 JSONL 文件 (这里演示用 'a' 模式追加)
with open("output_results.jsonl", "a", encoding="utf-8") as f:
for item in results:
# 注意:写入 JSONL 时,indent 必须保持默认的 None,确保每个对象严格挤在一行!
json_string = json.dumps(item, ensure_ascii=False)
f.write(json_string + "\n")
六. 高频踩坑避雷指南 (TypeError 报错)
在科研中,你会经常把模型的输出(Tensor)或 NumPy 数组保存到 JSON 里,此时极易遇到以下报错:
TypeError: Object of type ndarray / Tensor / float32 is not JSON serializable
原因: json 库非常基础,它只认识 Python 原生的 dict, list, int, float, str 等。它完全不认识 NumPy 数组或 PyTorch 的张量!
终极解决方案:在传给 json 之前,强制降维降级为 Python 原生类型。
python
import numpy as np
import torch
vector_np = np.array([1.1, 2.2, 3.3])
tensor_pt = torch.tensor([4.4, 5.5])
# 错误示范:
# json.dumps({"data": vector_np}) # 报错!
# 正确处理:
# 1. 标量类型使用 .item() 转化为原生 float/int
# 2. 数组/张量使用 .tolist() 转化为原生嵌套 list
safe_data = {
"np_vector": vector_np.tolist(),
"pt_tensor": tensor_pt.tolist(),
"loss_value": tensor_pt[0].item()
}
# 此时可以完美保存
json.dumps(safe_data)