Python 中的 JSON:读写文件、CSV、pathlib 与 UTF-8 编码
处理 JSON 的 Python 标准模块是 json:json.load 从打开的文件读取数据,json.dump 把数据写入文件,json.loads 与 json.dumps 对字符串做同样的操作。文件通过 with open(path, encoding="utf-8") 打开,路径用 pathlib.Path 构建,表格数据用 csv 模块读取。
在 Python 中处理 JSON 不需要任何第三方库:模块 json 属于标准库,能把字典和列表转换为文本,也能反向转换。通常还会用到三样东西:带 with 的 open 函数、处理路径的 pathlib 模块,以及处理表格的 csv 模块。下面按顺序讲解,附有可运行的代码,并指出最常踩的坑。
Python 的 open 函数:模式与编码
任何文本文件都用内置函数 open(path, mode, encoding=...) 打开。它返回一个文件对象,有 read、readline、write 等方法,并且可以按行遍历。
| 模式 | 作用 |
|---|---|
"r" | 读取(默认)。文件不存在时抛出 FileNotFoundError |
"w" | 写入。文件会被创建,或者在打开时立即清空 |
"a" | 追加到末尾,原有内容保留 |
"x" | 创建新文件;如果文件已存在,抛出 FileExistsError |
"b" | 二进制模式的修饰符:处理字节("rb"、"wb") |
为什么要用 with
文件必须关闭:否则数据可能没有写入磁盘,而系统的文件描述符也会一直被占用。with 语句会自动关闭文件——即使代码块中出现错误也一样。
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("第一行\n")
f.write("第二行\n")
with open("notes.txt", encoding="utf-8") as f:
for line in f:
print(line.rstrip("\n"))
按行遍历文件时,每次只读取一行,不会把整个文件载入内存,因此即使是几 GB 的日志也能处理。f.read() 会把全部内容作为一个字符串返回,f.readlines() 则返回行的列表。
如何写入一行而不覆盖原有内容
新手最常犯的错误是用 "w" 模式打开文件,想“追加”一行。"w" 模式在打开文件的那一刻就会清空它,还没等到 write。要追加到末尾,使用 "a";要确保不会覆盖别人的文件,使用 "x"。
with open("log.txt", "a", encoding="utf-8") as f:
f.write("新记录\n")
注意,write 不会自动添加换行符,"\n" 需要明确写出。也可以使用 print(..., file=f),它会自动加上换行。
UTF-8 编码:为什么会出现乱码
在 Python 3.15 之前,如果 open() 没有 encoding 参数,就使用系统的编码。在 Linux 和 macOS 上是 UTF-8,而在某些区域设置的 Windows 上是 GBK 或 cp1251。一个在服务器上运行正常的脚本,可能在同事的笔记本上输出“Еда”而不是“餐饮”,或者直接抛出 UnicodeDecodeError。
在 Python 3.15 中,根据 PEP 686,UTF-8 模式默认开启:不带 encoding 的 open() 在所有操作系统上都以 UTF-8 读写。要恢复旧的行为,可以设置环境变量 PYTHONUTF8=0 或使用参数 -X utf8=0;如果想明确使用系统编码,写 encoding="locale"。
import sys
print(sys.flags.utf8_mode) # 在 Python 3.15 中为 1
open("legacy.txt", encoding="locale") # 系统编码
open("old_excel.csv", encoding="cp1251") # 以 cp1251 保存的文件
反面影响是:由 Excel 或 Windows 脚本保存的 cp1251 格式旧 CSV 文件,现在会被当作 UTF-8 读取,从而引发 UnicodeDecodeError。解决办法是:用 encoding="cp1251" 读取,然后以 UTF-8 重新保存。良好的习惯依然不变——始终显式指定 encoding,这样代码在任何版本上的行为都一致。
Python 的 pathlib:把路径当作对象
用字符串拼接路径("data" + "/" + name)既不方便,又容易在不同操作系统上出错。pathlib 模块提供了 Path 类:路径用 / 运算符连接,对象上还有清晰的属性。
from pathlib import Path
data = Path("data")
csv_path = data / "expenses.csv"
print(csv_path.name) # expenses.csv
print(csv_path.stem) # expenses
print(csv_path.suffix) # .csv
print(csv_path.parent) # data
print(csv_path.exists())
data.mkdir(parents=True, exist_ok=True)
(data / "hello.txt").write_text("你好", encoding="utf-8")
print((data / "hello.txt").read_text(encoding="utf-8"))
for p in data.glob("*.csv"):
print(p)
对于小文件,read_text 和 write_text 可以代替 open 加 with 的组合。Path 对象可以直接传给 open、json 和 csv——它们都能接受。要获取相对于脚本本身(而不是当前工作目录)的路径,使用 Path(__file__).parent / "data"。
在 Python 3.14 中,Path 新增了 copy() 和 move() 方法,因此简单的复制已经不再需要 shutil。同一版本还在标准库中加入了 compression.zstd 模块,用于 Zstandard 压缩(PEP 784)。
Python 中的 JSON:load、dump、loads、dumps
JSON 是一种文本格式的数据交换方式:API 用它返回数据,配置和报告也常以它保存。json 模块提供两种操作:序列化(Python 对象 → JSON 文本)和反序列化(文本 → 对象)。函数一共有四个,人们常常把它们搞混。
| 函数 | 作用 |
|---|---|
json.dumps(obj) | 对象 → JSON 字符串 |
json.loads(s) | JSON 字符串 → 对象 |
json.dump(obj, f) | 对象 → 写入已打开的文件 |
json.load(f) | 从已打开的文件读取 JSON |
记住这个规律:结尾的 s 表示 string,也就是处理字符串;没有 s 的版本处理的是文件。
类型对应关系
| Python | JSON |
|---|---|
dict | 对象 {} |
list、tuple | 数组 [] |
str | 字符串 |
int、float | 数字 |
True / False | true / false |
None | null |
元组在读回时会变成列表;而集合、日期和 Decimal 模块无法直接处理,会抛出 TypeError: Object of type ... is not JSON serializable。
如何把 JSON 保存到文件
import json
from pathlib import Path
report = {
"month": "2026-10",
"total": 15230.5,
"categories": {"餐饮": 8200, "交通": 2100},
"tags": ["家庭", "工作"],
}
path = Path("data/report.json")
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
参数 ensure_ascii=False 会让中文原样保存。如果不加这个参数,文件里“餐饮”会变成 "餐饮"——这仍然是合法的 JSON,但人眼无法阅读。indent=2 则会生成带缩进的美观格式。
如何从文件读取 JSON
with open("data/report.json", encoding="utf-8") as f:
data = json.load(f)
print(data["total"]) # 15230.5
print(data["categories"]["餐饮"]) # 8200
结果是普通的字典或列表,可以用常规方法处理。如果文件不是 JSON 或者被截断了,会抛出 json.JSONDecodeError,并给出行号和位置——当数据来自外部时,应当捕获这个异常。
解析 JSON 字符串与美化输出
import json
raw = '{"name": "安娜", "skills": ["python", "sql"], "active": true}'
user = json.loads(raw)
print(user["skills"][0]) # python
print(json.dumps(user, ensure_ascii=False, indent=2, sort_keys=True))
sort_keys=True 在输出时对键排序——便于在 git 中比较文件。要在终端中快速格式化文件,可以使用内置工具:python -m json.tool data/report.json。
HTTP API 的响应也是 JSON 字符串。库 requests 通过 response.json() 解析它,而内部使用的仍然是 json.loads。
陷阱:JSON 的键总是字符串
d = {1: "一月", 2: "二月"}
back = json.loads(json.dumps(d))
print(back == d) # False
print(back) # {'1': '一月', '2': '二月'}
# back[1] -> KeyError,需要写 back["1"]
fixed = {int(k): v for k, v in back.items()}
在 JSON 中,对象的键只能是字符串,因此数字键在一次序列化往返之后会变成字符串。读取时要把它们转换回来。
自定义类型:日期与 Decimal
import json
from datetime import date
def default(obj):
if isinstance(obj, date):
return obj.isoformat()
raise TypeError(f"无法序列化 {type(obj).__name__}")
print(json.dumps({"day": date(2026, 10, 1)}, default=default))
# {"day": "2026-10-01"}
传给参数 default 的函数会对每个无法识别的对象调用一次。对于更复杂的情况,可以继承 json.JSONEncoder 并重写其 default 方法。
如何向 JSON 数组中追加元素
与文本日志不同,JSON 不能直接在末尾追加:需要先读取文件,修改数据,然后整体写回。
path = Path("data/items.json")
items = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
items.append({"id": len(items) + 1, "title": "新的"})
path.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")
如果记录很多并且不断增加,更适合使用 JSON Lines 格式:每行一个 JSON 对象,文件以 "a" 模式打开。
Python 中的 CSV:csv 模块
CSV 是以文本保存的表格:每行是一条记录,值之间用逗号分隔。csv 模块能正确处理引号、值中的逗号和换行,而手动使用 split(",") 做不到这些。
import csv
with open("data/expenses.csv", newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
total = 0.0
for row in reader:
total += float(row["amount"])
print(row["date"], row["category"], row["amount"])
print("合计:", total)
csv.DictReader 会根据文件的表头,把每一行转换为字典。所有值都是字符串:数字和日期需要自己转换(float、int、date.fromisoformat)。用分号 “;” 分隔的文件(俄语版 Excel 就是这样保存的)需要传入参数 delimiter=";"。
rows = [
{"date": "2026-10-01", "category": "餐饮", "amount": 450},
{"date": "2026-10-02", "category": "交通", "amount": 60},
]
with open("data/out.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["date", "category", "amount"])
writer.writeheader()
writer.writerows(rows)
写入时必须加上 newline="":否则在 Windows 上行与行之间会出现空行。如果文件要用 Excel 打开,使用 encoding="utf-8-sig"——BOM 标记能帮助 Excel 识别 UTF-8。
从 CSV 到 JSON:完整示例
import csv
import json
from collections import defaultdict
from pathlib import Path
src = Path("data/expenses.csv")
dst = Path("data/report.json")
by_category = defaultdict(float)
with src.open(newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
by_category[row["category"]] += float(row["amount"])
report = {"total": sum(by_category.values()), "categories": dict(by_category)}
dst.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(dst.read_text(encoding="utf-8"))
这就是任何报表脚本的核心:读入 CSV,在字典中聚合,输出为 JSON。
常见错误
FileNotFoundError— 相对路径是从运行命令时的当前目录计算的,而不是从脚本所在目录。使用Path(__file__).parent。- 文件被清空了 — 用
"w"而不是"a"打开。 UnicodeDecodeError— 文件使用了其他编码;显式指定(cp1251、utf-8-sig)。TypeError: Object of type set is not JSON serializable— 先转换为列表,或者传入default参数。- 加载 JSON 后出现
KeyError— 数字键变成了字符串。 - 自己的文件命名为
json.py或csv.py— 它会覆盖标准模块,import json导入的就是它。
小结
Python 处理 JSON 只需要标准库:文件用 json.load/json.dump,字符串用 loads/dumps;为了可读性,使用 ensure_ascii=False 和 indent。文件要通过 with 打开并显式指定编码,路径用 pathlib 构建,表格用 csv.DictReader 读取。这一主题在课程中同样结合实践讲解:支出记录器从 CSV 中读取消费记录,并把报告保存为 JSON。
常见问题
json.load 和 json.loads 有什么区别?
json.load 从已打开的文件对象读取 JSON,json.loads 则解析字符串。同理,json.dump 写入文件,json.dumps 返回字符串。
如何在 JSON 中保存中文而不出现 \u 转义?
在 json.dump 或 json.dumps 中传入 ensure_ascii=False,并且用 encoding="utf-8" 打开文件。
处理 JSON 需要安装 Python 库吗?
不需要,json 模块已经包含在标准库中,直接写 import json 即可。
能否在 JSON 中保存日期或集合?
不能直接保存:需要先转换——日期用 isoformat 转为 ISO 字符串,集合转为列表;也可以把转换函数传给 default 参数。
如何读取无法全部装入内存的大 JSON 文件?
标准的 json.load 会一次读取整个文件。处理大数据时,应使用 JSON Lines 格式,并通过 json.loads 逐行解析。