Python 数据处理(十三):JSON、CSV 与数据序列化

Hello,大家好,我是 Ivan。

前面我已经讲过怎么读取和写入文件。不过,当时保存的大多是普通字符串。如果要记录一批商品,每件商品都有名称、价格、库存和分类,继续自己拼接文本,后面读取时还要重新拆分,字段稍微多一点就容易乱。

这时候可以换成 JSON 或 CSV。它们都能把数据按照固定格式保存下来,程序再次读取时,也知道哪一部分对应哪个字段。这一章我们就用一组商品数据,把 JSON、CSV 和数据序列化的常见操作写一遍。

一、为什么需要结构化数据

先看一组 Python 商品数据:

python 复制代码
products = [
    {"id": "P001", "name": "键盘", "price": 299, "stock": 12},
    {"id": "P002", "name": "鼠标", "price": 129, "stock": 0},
    {"id": "P003", "name": "显示器", "price": 1599, "stock": 5},
]

如果直接把它写成普通文本,可能会变成:

复制代码
P001 键盘 299 12
P002 鼠标 129 0
P003 显示器 1599 5

人能大概看懂,但是程序还要约定每一列的顺序,并处理商品名称中可能出现的空格。以后再增加分类、品牌或者备注,原来的解析代码也要跟着改。

JSON 会保留字段名称:

json 复制代码
{
  "id": "P001",
  "name": "键盘",
  "price": 299,
  "stock": 12
}

CSV 更接近表格:

复制代码
id,name,price,stock
P001,键盘,299,12
P002,鼠标,129,0
P003,显示器,1599,5

两种格式的用途不完全相同,后面会分别讲。

二、什么是序列化和反序列化

列表和字典存在于 Python 程序的内存中,文件里保存的则是文本或字节。把 Python 对象转换成可以保存、传输的数据格式,这个过程叫作序列化。

程序重新读取文件,再把其中的内容还原成列表或字典,就是反序列化。

复制代码
Python 对象 → JSON 文本 → 写入文件
读取文件 → JSON 文本 → Python 对象

序列化以后,数据可以保存到磁盘,也可以通过网络发送给其他程序。很多接口返回的内容就是 JSON,因为浏览器、Python、Java 和其他语言都能处理它。

三、JSON 的基本结构

JSON 中常见的是对象和数组。对象使用花括号,数组使用方括号:

json 复制代码
{
  "name": "Ivan",
  "age": 20,
  "skills": ["Python", "Java"],
  "active": true
}

对象中的数据按照键值对保存,键必须使用双引号包起来。数组中可以继续放字符串、数字、对象或者其他数组。

JSON 看起来和 Python 的字典、列表很像,不过它们不是同一种语法。下面几个地方要注意:

复制代码
Python       JSON
True         true
False        false
None         null

Python 可以使用单引号表示字符串,JSON 字符串必须使用双引号。

四、Python 类型与 JSON 类型

使用 json 模块转换数据时,常见类型会按照下面的关系处理:

Python 类型 JSON 类型
dict object
listtuple array
str string
intfloat number
TrueFalse true、false
None null

需要留意的是,元组保存到 JSON 后会变成数组,再读取回来时得到的是列表:

python 复制代码
import json

data = (10, 20, 30)

json_text = json.dumps(data)
result = json.loads(json_text)

print(json_text)
print(result)
print(type(result))

输出结果是:

复制代码
[10, 20, 30]
[10, 20, 30]
<class 'list'>

JSON 本身没有元组类型,所以读取时无法恢复成原来的元组。

五、使用 dumps 把对象转换成字符串

Python 标准库中的 json 模块可以处理 JSON:

python 复制代码
import json

product = {
    "id": "P001",
    "name": "键盘",
    "price": 299,
    "stock": 12,
}

json_text = json.dumps(product)

print(json_text)
print(type(json_text))

json.dumps() 接收一个 Python 对象,返回 JSON 字符串。这里的结果类型是 str,还没有写入文件。

默认情况下,中文会被转成 Unicode 转义字符:

复制代码
{"name": "\u952e\u76d8"}

想直接看到中文,可以设置 ensure_ascii=False

python 复制代码
json_text = json.dumps(
    product,
    ensure_ascii=False,
)

print(json_text)

如果还想让结构更容易阅读,可以加上缩进:

python 复制代码
json_text = json.dumps(
    product,
    ensure_ascii=False,
    indent=2,
)

print(json_text)

输出会变成:

json 复制代码
{
  "id": "P001",
  "name": "键盘",
  "price": 299,
  "stock": 12
}

六、使用 loads 把字符串转回对象

json.loads() 负责处理反方向的转换:

python 复制代码
import json

json_text = '''
{
  "id": "P001",
  "name": "键盘",
  "price": 299,
  "stock": 12
}
'''

product = json.loads(json_text)

print(product)
print(type(product))
print(product["name"])

运行结果是:

复制代码
{'id': 'P001', 'name': '键盘', 'price': 299, 'stock': 12}
<class 'dict'>
键盘

方法名末尾的 s 可以理解成 string。dumps() 得到字符串,loads() 读取字符串。后面要讲的 dump()load(),直接接收文件对象。

七、把 JSON 写入文件

先准备项目目录:

复制代码
data_demo/
├─ main.py
└─ data/

使用 json.dump() 把商品列表写入文件:

python 复制代码
from pathlib import Path
import json

products = [
    {"id": "P001", "name": "键盘", "price": 299, "stock": 12},
    {"id": "P002", "name": "鼠标", "price": 129, "stock": 0},
    {"id": "P003", "name": "显示器", "price": 1599, "stock": 5},
]

base_dir = Path(__file__).resolve().parent
data_dir = base_dir / "data"
data_dir.mkdir(exist_ok=True)

json_path = data_dir / "products.json"

with json_path.open("w", encoding="utf-8") as file:
    json.dump(
        products,
        file,
        ensure_ascii=False,
        indent=2,
    )

这里的第二个参数是文件对象。json.dump() 会完成对象到 JSON 文本的转换,并把结果写入 products.json

因为打开文件时使用了 "w",重复运行会覆盖原文件。配置和数据快照通常可以这样保存;如果需要保留每次历史记录,就要生成不同文件名,或者改用更合适的存储方式。

八、从 JSON 文件读取数据

读取刚才生成的文件,可以使用 json.load()

python 复制代码
from pathlib import Path
import json

base_dir = Path(__file__).resolve().parent
json_path = base_dir / "data" / "products.json"

with json_path.open("r", encoding="utf-8") as file:
    products = json.load(file)

for product in products:
    print(product["name"], product["price"])

输出结果是:

复制代码
键盘 299
鼠标 129
显示器 1599

json.load() 会读取文件并完成反序列化。JSON 最外层是数组,所以这里得到列表;数组里的每个对象会变成字典。

九、处理嵌套 JSON

JSON 可以保存多层结构,接口返回结果里经常会看到这种数据:

json 复制代码
{
  "code": 200,
  "message": "success",
  "data": {
    "total": 2,
    "items": [
      {
        "id": "P001",
        "name": "键盘"
      },
      {
        "id": "P002",
        "name": "鼠标"
      }
    ]
  }
}

读取以后,按照字典和列表的层级访问:

python 复制代码
result = json.loads(json_text)

items = result["data"]["items"]

for item in items:
    print(item["name"])

层级多的时候,可以把中间结果保存到变量里。全部写成一长串下标虽然能运行,但是后面排查缺少字段的问题会比较麻烦。

十、JSON 不能直接保存哪些对象

JSON 支持的类型有限。集合、日期、Path 对象和自己定义的类,不能直接交给 json.dumps()

python 复制代码
import json

product = {
    "name": "键盘",
    "tags": {"办公", "外设"},
}

json.dumps(product, ensure_ascii=False)

运行时会出现:

复制代码
TypeError: Object of type set is not JSON serializable

保存前需要转换成 JSON 支持的类型:

python 复制代码
product["tags"] = list(product["tags"])

json_text = json.dumps(
    product,
    ensure_ascii=False,
)

日期也可以先转换成字符串:

python 复制代码
from datetime import datetime

created_at = datetime.now()

data = {
    "created_at": created_at.isoformat(),
}

再次读取时,created_at 还是字符串。如果程序需要日期对象,还要使用 datetime.fromisoformat() 转回去。

十一、JSON 常见格式错误

下面这段内容看起来像字典,但是它不是合法 JSON:

复制代码
{
  'name': '键盘',
  'price': 299,
  'active': True,
}

这里有三个问题:字符串使用了单引号,布尔值写成了 Python 的 True,最后一个字段后面还保留了逗号。

合法 JSON 应该写成:

json 复制代码
{
  "name": "键盘",
  "price": 299,
  "active": true
}

格式有问题时,json.loads()json.load() 会抛出 JSONDecodeError。可以使用 Python 自带的命令检查文件:

bash 复制代码
python -m json.tool data/products.json

文件格式正确时,终端会输出整理后的 JSON;有错误时,会显示大概的行号和列号。

十二、CSV 是怎么保存数据的

CSV 的全称是 Comma-Separated Values,也就是逗号分隔值。它按行保存记录,第一行通常是表头:

复制代码
id,name,price,stock
P001,键盘,299,12
P002,鼠标,129,0
P003,显示器,1599,5

每一行是一条商品记录,每一列对应一个字段。CSV 很适合保存结构相同的批量数据,也能直接用表格软件查看。

CSV 不适合自然表达多层嵌套。商品还有多个标签、不同规格和详细参数时,硬塞进一个单元格会越来越难处理,这类数据通常更适合 JSON。

十三、使用 csv.reader 读取文件

Python 标准库提供了 csv 模块。先准备一个 products.csv

复制代码
id,name,price,stock
P001,键盘,299,12
P002,鼠标,129,0
P003,显示器,1599,5

使用 csv.reader() 读取:

python 复制代码
from pathlib import Path
import csv

base_dir = Path(__file__).resolve().parent
csv_path = base_dir / "data" / "products.csv"

with csv_path.open("r", encoding="utf-8", newline="") as file:
    reader = csv.reader(file)

    for row in reader:
        print(row)

每一行都会变成列表:

复制代码
['id', 'name', 'price', 'stock']
['P001', '键盘', '299', '12']
['P002', '鼠标', '129', '0']
['P003', '显示器', '1599', '5']

CSV 读取出来的内容默认都是字符串。后面要计算价格和库存时,需要自己转换类型:

python 复制代码
price = float(row[2])
stock = int(row[3])

打开 CSV 文件时建议加上 newline="",让 csv 模块自己处理换行。Windows 上写入文件时,这个参数也能避免出现多余空行。

十四、使用 csv.writer 写入文件

csv.writer() 接收一行列表,再把它写成 CSV:

python 复制代码
from pathlib import Path
import csv

products = [
    ["P001", "键盘", 299, 12],
    ["P002", "鼠标", 129, 0],
    ["P003", "显示器", 1599, 5],
]

csv_path = Path("data/products.csv")

with csv_path.open("w", encoding="utf-8", newline="") as file:
    writer = csv.writer(file)

    writer.writerow(["id", "name", "price", "stock"])
    writer.writerows(products)

writerow() 写入一行,writerows() 可以一次写入多行。这里的 "w" 仍然会覆盖原文件。

如果某个字段中本身带有逗号,csv 模块会自动加上引号:

python 复制代码
writer.writerow([
    "P004",
    "键盘,鼠标套装",
    399,
    8,
])

文件中会得到:

复制代码
P004,"键盘,鼠标套装",399,8

这也是不建议自己使用 line.split(",") 解析 CSV 的原因。字段中出现逗号、引号或换行时,手动切分很容易出错。

十五、使用 DictReader 按字段读取

csv.reader() 得到的是列表,访问价格要写 row[2]。列一多,很难记住每个位置对应什么字段。

CSV 带有表头时,可以使用 DictReader

python 复制代码
from pathlib import Path
import csv

csv_path = Path("data/products.csv")

with csv_path.open("r", encoding="utf-8", newline="") as file:
    reader = csv.DictReader(file)

    for product in reader:
        print(product["name"], product["price"])

每一行会变成字典,键来自第一行表头:

python 复制代码
{
    "id": "P001",
    "name": "键盘",
    "price": "299",
    "stock": "12",
}

这里的价格和库存依然是字符串,只是访问方式从位置下标变成了字段名。

十六、使用 DictWriter 写入字典

商品数据本来就是字典时,使用 DictWriter 更方便:

python 复制代码
from pathlib import Path
import csv

products = [
    {"id": "P001", "name": "键盘", "price": 299, "stock": 12},
    {"id": "P002", "name": "鼠标", "price": 129, "stock": 0},
    {"id": "P003", "name": "显示器", "price": 1599, "stock": 5},
]

fieldnames = ["id", "name", "price", "stock"]
csv_path = Path("data/products.csv")

with csv_path.open("w", encoding="utf-8", newline="") as file:
    writer = csv.DictWriter(
        file,
        fieldnames=fieldnames,
    )

    writer.writeheader()
    writer.writerows(products)

fieldnames 决定字段和列的顺序,writeheader() 写入表头,writerows() 写入多条字典数据。

如果字典里有 fieldnames 之外的字段,默认会报错。需要哪些列,最好在写入之前明确整理好。

十七、CSV 中文打开乱码怎么办

使用代码读取 CSV 时,统一写 encoding="utf-8" 就可以了。但是,有些版本的 Excel 直接打开 UTF-8 CSV,可能无法正确识别中文。

需要把文件交给 Excel 使用时,可以写成 utf-8-sig

python 复制代码
with csv_path.open(
    "w",
    encoding="utf-8-sig",
    newline="",
) as file:
    writer = csv.DictWriter(
        file,
        fieldnames=fieldnames,
    )

    writer.writeheader()
    writer.writerows(products)

utf-8-sig 会在文件开头加入编码标记,Excel 通常可以据此识别 UTF-8。后面如果这个 CSV 只交给程序处理,普通的 UTF-8 就够了。

十八、JSON 和 CSV 怎么选

JSON 能保存嵌套对象和数组,字段也会保留类型。配置文件、接口数据以及层级较多的内容,经常使用 JSON。

CSV 适合结构一致的表格数据。每一行字段相同,数据量又比较大时,CSV 查看和处理都很方便。

使用场景 更适合的格式
保存程序配置 JSON
接口请求和返回数据 JSON
保存包含嵌套列表的数据 JSON
导出商品、订单和用户表格 CSV
交给表格软件查看 CSV
和数据分析工具交换批量记录 CSV

格式没有固定的高低之分,主要看数据本身长什么样,以及接下来要交给谁使用。

十九、pickle 可以保存更多 Python 对象

JSON 只能处理有限的数据类型。如果只在 Python 程序内部保存对象,还可以使用 pickle

python 复制代码
from pathlib import Path
import pickle

products = [
    {"name": "键盘", "price": 299},
    {"name": "鼠标", "price": 129},
]

file_path = Path("data/products.pkl")

with file_path.open("wb") as file:
    pickle.dump(products, file)

读取时使用二进制模式:

python 复制代码
with file_path.open("rb") as file:
    products = pickle.load(file)

print(products)

pickle 保存的是 Python 专用二进制格式,其他语言通常不能直接使用,文件内容也不方便人工查看。

更重要的是,不要加载来源不明的 .pkl 文件。恶意构造的 pickle 数据可能在反序列化时执行代码。自己项目内部的临时缓存可以考虑使用,接口传输、公开文件和用户上传内容不要使用它。

二十、综合示例:生成库存报表

现在把 JSON 和 CSV 连起来。程序会从 products.json 读取商品,筛选有库存的记录,计算库存金额,再生成 inventory_report.csvsummary.json

项目结构如下:

复制代码
data_demo/
├─ report.py
├─ data/
│  └─ products.json
└─ output/

products.json 中准备下面的数据:

json 复制代码
[
  {
    "id": "P001",
    "name": "键盘",
    "category": "电脑外设",
    "price": 299,
    "stock": 12
  },
  {
    "id": "P002",
    "name": "鼠标",
    "category": "电脑外设",
    "price": 129,
    "stock": 0
  },
  {
    "id": "P003",
    "name": "显示器",
    "category": "电脑外设",
    "price": 1599,
    "stock": 5
  }
]

report.py 中写入:

python 复制代码
from pathlib import Path
import csv
import json

base_dir = Path(__file__).resolve().parent
data_file = base_dir / "data" / "products.json"
output_dir = base_dir / "output"

output_dir.mkdir(parents=True, exist_ok=True)

with data_file.open("r", encoding="utf-8") as file:
    products = json.load(file)

available_products = []

for product in products:
    if product["stock"] <= 0:
        continue

    product["inventory_value"] = round(
        product["price"] * product["stock"],
        2,
    )
    available_products.append(product)

csv_file = output_dir / "inventory_report.csv"
fieldnames = [
    "id",
    "name",
    "category",
    "price",
    "stock",
    "inventory_value",
]

with csv_file.open(
    "w",
    encoding="utf-8-sig",
    newline="",
) as file:
    writer = csv.DictWriter(
        file,
        fieldnames=fieldnames,
    )
    writer.writeheader()
    writer.writerows(available_products)

summary = {
    "source_count": len(products),
    "available_count": len(available_products),
    "total_stock": sum(
        product["stock"]
        for product in available_products
    ),
    "total_value": round(
        sum(
            product["inventory_value"]
            for product in available_products
        ),
        2,
    ),
}

summary_file = output_dir / "summary.json"

with summary_file.open("w", encoding="utf-8") as file:
    json.dump(
        summary,
        file,
        ensure_ascii=False,
        indent=2,
    )

print(f"CSV 报表:{csv_file}")
print(f"汇总数据:{summary_file}")

products.json 负责保存原始商品数据。程序读取以后,把库存为 0 的商品排除,再为剩余商品增加 inventory_value 字段。

详细记录写进 CSV,方便用表格软件查看;统计结果写进 JSON,后续程序可以继续读取。两种格式放在同一个案例里,用途上的区别会更明显。

二十一、练习

练习一:保存用户信息

创建一个包含姓名、年龄和技能列表的字典,使用 json.dump() 保存到 user.json,再读取回来并打印技能列表。

练习二:处理不支持的类型

在字典中加入一个集合和一个日期对象,观察 json.dumps() 的报错,再把它们转换成 JSON 支持的类型。

练习三:检查 JSON 文件

准备一个包含单引号、末尾逗号和 True 的错误 JSON 文件。使用 python -m json.tool 找到问题,再把它改成合法 JSON。

练习四:统计 CSV 数据

使用 csv.DictReader() 读取商品 CSV,把价格转换成 float,库存转换成 int,统计所有商品的库存总数。

练习五:导出筛选结果

从 CSV 中筛选价格大于 500 的商品,使用 DictWriter 写入 expensive_products.csv,并保留原来的表头。

练习六:完成 JSON 与 CSV 转换

读取一个由字典列表组成的 JSON 文件,把它转换成 CSV。再读取生成的 CSV,把数据重新整理成列表,并写入新的 JSON 文件。

写 JSON 和 CSV 时,代码通常不会很长。容易出错的地方还是数据类型、编码和字段结构。保存之前先确认数据中有没有集合、日期或者自定义对象,读取 CSV 后也要记得把需要计算的字段转成数字。

相关推荐
Patrick在香港2 小时前
Claude 工具调用返回空:8 次失败里只有 1 次状态码不对,其余全带 200
爬虫·python·api·claude·香港
Web3&Basketball2 小时前
CRM Agent 后训练实战:3 倍更少错误
python·架构·大模型·agent·推理
aramae2 小时前
MySQL复合查询(8)
java·c语言·开发语言·后端·算法
AIFQuant3 小时前
ETF行情API接入踩坑记:从报错到跑通的七个问题
python·金融·区块链·etf·基金
GPU实战笔记4 小时前
云端 Python 开发:JupyterLab 还是 VS Code Remote-SSH?
python·vs code·jupyterlab·remote-ssh·远程开发
狗狗狗狗狗乐啊5 小时前
搭一个 AI 对话工作台 AChat:从 0 到可用的完整记录(一)
python·react.js·ai编程
qq_2518364575 小时前
springboot vue3 开发实现 拼豆管理系统
java·开发语言·ai编程
白猫不黑5 小时前
Python实现简易Web弱口令爆破与防护方案
python·web安全·计算机·网络安全·黑客·信息安全·渗透测试
郑州光合科技余经理6 小时前
同城外卖小程序开发:下单成功后,后台导出能不能对上用户端状态
开发语言·前端·git·后端·uni-app·php·ai编程