# Python 中的 JSON：读写文件、CSV、pathlib 与 UTF-8 编码

URL: https://skillaria.top/zh/guides/python/json-python

处理 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。

## FAQ

**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 逐行解析。


---
Source: Skillaria.Top (https://skillaria.top)
