深入理解python的异常处理与文件读写
- 一、异常处理与文件读写
-
- [1. 认识异常](#1. 认识异常)
-
- [1.1 什么是异常](#1.1 什么是异常)
- [1.2 常见异常类型](#1.2 常见异常类型)
- [1.3 如何阅读 Traceback](#1.3 如何阅读 Traceback)
- [2. 使用 try / except 处理异常](#2. 使用 try / except 处理异常)
-
- [2.1 捕获指定异常](#2.1 捕获指定异常)
- [2.2 获取异常信息](#2.2 获取异常信息)
- [2.3 分别处理不同异常](#2.3 分别处理不同异常)
- [2.4 多种异常使用相同处理方式](#2.4 多种异常使用相同处理方式)
- [2.5 异常匹配存在继承关系](#2.5 异常匹配存在继承关系)
- [3. else、finally 与异常传播](#3. else、finally 与异常传播)
-
- [3.1 try 中的 else](#3.1 try 中的 else)
- [3.2 finally:执行收尾操作](#3.2 finally:执行收尾操作)
- [3.3 未匹配的异常会继续传播](#3.3 未匹配的异常会继续传播)
- [3.4 不要随意吞掉异常](#3.4 不要随意吞掉异常)
- [4. 使用 raise 主动抛出异常](#4. 使用 raise 主动抛出异常)
-
- [4.1 对不合法参数明确报错](#4.1 对不合法参数明确报错)
- [4.2 返回 None 还是抛出异常](#4.2 返回 None 还是抛出异常)
- [4.3 重新抛出异常](#4.3 重新抛出异常)
- [4.4 转换异常并保留原因](#4.4 转换异常并保留原因)
- [5. 文件路径与 pathlib](#5. 文件路径与 pathlib)
-
- [5.1 绝对路径与相对路径](#5.1 绝对路径与相对路径)
- [5.2 使用 Path 表示路径](#5.2 使用 Path 表示路径)
- [5.3 以脚本目录为基准](#5.3 以脚本目录为基准)
- [5.4 创建目录](#5.4 创建目录)
- [6. 文本文件读写](#6. 文本文件读写)
-
- [6.1 使用 with 打开文件](#6.1 使用 with 打开文件)
- [6.2 常见打开模式](#6.2 常见打开模式)
- [6.3 一次读取全部内容](#6.3 一次读取全部内容)
- [6.4 逐行读取](#6.4 逐行读取)
- [6.5 写入与追加](#6.5 写入与追加)
- [6.6 文件有当前位置](#6.6 文件有当前位置)
- [6.7 Path 的快捷方法](#6.7 Path 的快捷方法)
- [7. 编码与二进制文件](#7. 编码与二进制文件)
-
- [7.1 为什么需要指定编码](#7.1 为什么需要指定编码)
- [7.2 不要把忽略解码错误当作通用修复](#7.2 不要把忽略解码错误当作通用修复)
- [7.3 二进制模式](#7.3 二进制模式)
- [7.4 分块复制文件](#7.4 分块复制文件)
- [8. 使用 JSON 保存结构化数据](#8. 使用 JSON 保存结构化数据)
-
- [8.1 JSON 适合保存什么](#8.1 JSON 适合保存什么)
- [8.2 dumps 与 loads:处理字符串](#8.2 dumps 与 loads:处理字符串)
- [8.3 dump 与 load:处理文件](#8.3 dump 与 load:处理文件)
- [8.4 类型转换与限制](#8.4 类型转换与限制)
- [8.5 捕获 JSON 格式错误](#8.5 捕获 JSON 格式错误)
- [8.6 不要直接向 JSON 数组末尾追加对象](#8.6 不要直接向 JSON 数组末尾追加对象)
- [9. 使用 CSV 读写表格数据](#9. 使用 CSV 读写表格数据)
-
- [9.1 CSV 的特点](#9.1 CSV 的特点)
- [9.2 写入 CSV](#9.2 写入 CSV)
- [9.3 读取 CSV](#9.3 读取 CSV)
一、异常处理与文件读写
上一篇,我们学习了函数、参数和作用域,已经能够把查询、筛选、统计等操作封装为可复用函数。但实际运行程序时,输入和环境不一定符合预期:
- 需要输入数字,用户却输入了文字。
- 准备读取的文件不存在。
- 文件内容不符合约定格式。
- 程序没有权限访问某个目录。
- 数据保存在变量中,程序结束后就丢失了。
本篇围绕两个问题展开:发生错误时,程序应该怎样处理?需要保留数据时,应该怎样读写文件?
我们将依次学习异常处理、路径操作、文本与二进制文件,以及 JSON、CSV 的读写,最后完成一个能够保存商品数据的小程序。
本文综合示例使用 Python 3.10 及以上版本。示例涉及文件写入,运行后会在指定目录生成文件;使用
"w"模式时,会覆盖已有文件内容。
1. 认识异常
1.1 什么是异常
异常表示程序执行过程中出现了无法按当前逻辑继续处理的情况。
python
number = int("abc")
"abc" 无法转换为整数,因此会抛出:
ValueError: invalid literal for int() with base 10: 'abc'
再例如:
python
result = 10 / 0
会抛出:
ZeroDivisionError: division by zero
如果异常没有被处理,它会沿调用关系向外传播。在普通脚本中,如果最终仍未被处理,程序通常会输出错误信息并终止执行。
1.2 常见异常类型
| 异常类型 | 常见原因 |
|---|---|
ValueError |
参数类型可以接受,但值不合法,例如 int("abc") |
TypeError |
类型不符合操作要求,例如 1 + "2" |
IndexError |
列表、元组等序列的索引越界 |
KeyError |
访问字典中不存在的键 |
NameError |
使用尚未定义的名称 |
ZeroDivisionError |
除数为零 |
FileNotFoundError |
文件或路径不存在 |
PermissionError |
没有操作文件或目录的权限 |
UnicodeDecodeError |
无法按照指定编码解码文件内容 |
异常类型说明了问题的类别,后面的消息通常会提供具体原因。
1.3 如何阅读 Traceback
假设运行:
python
def parse_age(text):
return int(text)
if __name__ == '__main__':
age = parse_age("abc")
错误信息通常包括:
Traceback (most recent call last):
...
ValueError: invalid literal for int() with base 10: 'abc'
阅读时可以先看最后一行,确定异常类型和原因,再向上查看调用位置,找到自己的代码。不要只看到报错就添加 try / except。有些异常来自用户输入,有些则说明代码本身写错了,需要先分清原因。
2. 使用 try / except 处理异常
2.1 捕获指定异常
基本语法:
try:
可能产生异常的代码
except 异常类型:
对应的处理代码
例如:
python
if __name__ == '__main__':
text = "abc"
try:
age = int(text)
print(f"年龄:{age}")
except ValueError:
print("年龄必须是整数")
print("程序继续执行")
运行结果:
年龄必须是整数
程序继续执行
执行过程:
- 进入
try。 int(text)抛出ValueError。- 跳过
try中剩余语句。 - 执行匹配的
except。 - 继续执行整个结构后面的代码。
如果没有异常,则跳过 except。
2.2 获取异常信息
可以通过 as 获取异常对象:
python
if __name__ == '__main__':
try:
number = int("abc")
except ValueError as error:
print(f"转换失败:{error}")
运行结果:
转换失败:invalid literal for int() with base 10: 'abc'
error 是异常对象,不只是普通字符串。放入格式化字符串时,会显示它的文本说明。面向用户时,可以输出容易理解的提示;调试或记录日志时,再保留具体异常信息。
2.3 分别处理不同异常
python
def divide_text(left, right):
try:
first = int(left)
second = int(right)
result = first / second
except ValueError:
print("请输入合法整数")
except ZeroDivisionError:
print("除数不能为零")
else:
print(f"结果:{result}")
if __name__ == '__main__':
divide_text("10", "2")
divide_text("abc", "2")
divide_text("10", "0")
运行结果:
结果:5.0
请输入合法整数
除数不能为零
这里使用的 else 表示:try 正常执行完毕、没有异常时,再执行成功后的操作。
2.4 多种异常使用相同处理方式
如果处理逻辑相同,可以使用元组指定多个异常类型:
python
if __name__ == '__main__':
data = {"age": "abc"}
try:
age = int(data["age"])
except (KeyError, ValueError):
print("年龄缺失或格式不正确")
运行结果:
年龄缺失或格式不正确
但如果需要给出不同提示,就应拆成多个 except。
2.5 异常匹配存在继承关系
许多异常类型之间具有继承关系,例如:
FileNotFoundError 是 OSError 的子类
PermissionError 是 OSError 的子类
因此,具体异常应该写在较宽泛的异常之前:
python
try:
...
except FileNotFoundError:
print("文件不存在")
except PermissionError:
print("没有访问权限")
except OSError as error:
print(f"其他文件操作错误:{error}")
如果先捕获 OSError,后面的两个具体分支就没有机会处理这些异常。
3. else、finally 与异常传播
3.1 try 中的 else
完整结构可以写成:
try:
可能失败的操作
except 某种异常:
失败时的处理
else:
操作成功后的处理
else 适合放依赖成功结果、但不应该被前面这些 except 一并捕获的代码。需要注意:发生在 else 中的异常,不会被同一组前面的 except **捕获。**因此,应该让 try 尽量只包住明确需要处理异常的操作,避免把整段程序都包进去。
3.2 finally:执行收尾操作
finally 用于执行收尾代码。在通常的异常、返回和控制流退出过程中,执行离开 try 结构前都会先运行它。
python
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
print("除数不能为零")
return None
finally:
print("本次计算结束")
if __name__ == '__main__':
print(divide(10, 2))
print(divide(10, 0))
运行结果:
本次计算结束
5.0
除数不能为零
本次计算结束
None
即使 try 或 except 中准备执行 return,也会先执行 finally``finally 适合释放资源、恢复状态,但不建议在其中编写 return。因为它可能覆盖原本的返回值,甚至掩盖正在传播的异常。
3.3 未匹配的异常会继续传播
python
def parse_number(text):
return int(text)
def calculate(text):
number = parse_number(text)
return number * 2
if __name__ == '__main__':
try:
print(calculate("abc"))
except ValueError:
print("输入无法转换为整数")
运行结果:
输入无法转换为整数
异常先从 parse_number() 产生,再经过 calculate() 传播,最终由调用方处理。不需要在每一层都捕获异常。通常在能够决定"应该重试、提示用户还是结束操作"的位置处理更合适。
3.4 不要随意吞掉异常
下面这种写法不利于排查问题:
try:
...
except Exception:
pass
它会让很多错误悄悄消失,使程序表面上继续执行,实际结果却可能已经不正确。同样,应避免不加区分地使用:
except:
...
裸 except 还会捕获 KeyboardInterrupt、SystemExit 等异常,可能影响用户中断和程序退出。优先捕获能够明确处理的异常。需要统一记录普通应用异常时,可以捕获 Exception,但应保留错误信息,并决定是否继续抛出。
4. 使用 raise 主动抛出异常
4.1 对不合法参数明确报错
异常不仅由 Python 自动产生,也可以主动抛出:
python
def calculate_average(scores):
if not scores:
raise ValueError("成绩列表不能为空")
return sum(scores) / len(scores)
if __name__ == '__main__':
try:
average = calculate_average([])
except ValueError as error:
print(f"计算失败:{error}")
运行结果:
计算失败:成绩列表不能为空
raise 可以让函数明确表达:当前输入无法满足执行要求。
4.2 返回 None 还是抛出异常
两者没有统一答案,应根据函数约定区分。
| 情况 | 常见选择 |
|---|---|
| 查询商品,未找到是正常情况 | 返回 None |
| 筛选后没有符合条件的数据 | 返回空列表 |
| 必须读取的文件格式损坏 | 抛出异常 |
| 参数违反函数明确要求 | 抛出异常 |
例如,查询不到商品可以返回 None;文件解析失败却返回空列表,可能让调用方误以为"文件中没有商品",从而掩盖问题。
4.3 重新抛出异常
如果只是补充记录,再交给上层处理,可以在 except 中使用不带参数的 raise:
python
def parse_quantity(text):
try:
return int(text)
except ValueError:
print(f"无法解析数量:{text!r}")
raise
if __name__ == '__main__':
try:
parse_quantity("abc")
except ValueError:
print("请重新输入数量")
运行结果:
无法解析数量:'abc'
请重新输入数量
!r 表示使用对象的表示形式,因此字符串会显示引号。
4.4 转换异常并保留原因
有时需要把底层异常转换为更贴近业务的说明:
python
def parse_quantity(text):
try:
quantity = int(text)
except ValueError as error:
raise ValueError("商品数量必须是整数") from error
if quantity <= 0:
raise ValueError("商品数量必须大于 0")
return quantity
raise ... from error 保留了原始异常作为原因。排查时既能看到业务说明,也能追踪最初的错误。
5. 文件路径与 pathlib
5.1 绝对路径与相对路径
绝对路径从文件系统的根开始定位,例如:
python
D:\python-study\data\products.json
/home/user/python-study/data/products.json
相对路径则相对于某个基准位置,例如:
data/products.json
默认情况下,普通相对路径相对于当前工作目录,不一定是脚本所在目录。
这意味着在不同位置启动同一个脚本,读取到的文件位置可能不同。
5.2 使用 Path 表示路径
pathlib 是 Python 标准库,用于处理路径:
python
from pathlib import Path
if __name__ == '__main__':
path = Path("data") / "products.json"
print(path.name)
print(path.stem)
print(path.suffix)
print(path.parent)
运行结果:
products.json
products
.json
data
这里的 / 用于拼接路径,不是数值除法。
| 属性或方法 | 用途 |
|---|---|
path.name |
文件名,包含扩展名 |
path.stem |
不含最后一个扩展名的名称 |
path.suffix |
最后一个扩展名 |
path.parent |
父目录 |
path.exists() |
路径是否存在 |
path.is_file() |
是否为文件 |
path.is_dir() |
是否为目录 |
Path.cwd() |
当前工作目录 |
在 Windows 中直接书写带反斜杠的路径时,可以使用原始字符串:
path = Path(r"D:\python-study\data")
避免将 \n、\t 等误当成转义字符。
5.3 以脚本目录为基准
普通 .py 文件中可以使用:
python
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
其中:
__file__表示当前模块文件的路径。resolve()解析为绝对路径,并处理符号链接等路径关系。parent取得父目录。
在某些交互式环境或 Notebook 中,__file__ 不存在,可以根据需要使用 Path.cwd()。
5.4 创建目录
python
from pathlib import Path
if __name__ == '__main__':
data_dir = Path("data")
data_dir.mkdir(parents=True, exist_ok=True)
print(data_dir.is_dir())
运行结果:
True
参数含义:
parents=True:必要时创建缺失的父目录。exist_ok=True:目标已经是目录时,不因此报错。
这不代表所有情况都不会报错。如果同名路径是文件,或没有创建权限,操作仍然会失败。
6. 文本文件读写
6.1 使用 with 打开文件
推荐使用:
with open(路径, 模式, encoding="utf-8") as file:
操作文件
例如:
python
from pathlib import Path
if __name__ == '__main__':
path = Path("notes.txt")
with path.open("w", encoding="utf-8") as file:
file.write("正在学习 Python\n")
file.write("今天学习文件读写\n")
运行后,notes.txt 内容为:
正在学习 Python
今天学习文件读写
with 用于管理资源。正常结束代码块或因异常离开时,文件都会被关闭。
它能帮助释放资源,但不会自动撤销已经写入的内容,也不会自动吞掉异常。
6.2 常见打开模式
| 模式 | 用途 | 文件不存在 | 文件已存在 |
|---|---|---|---|
"r" |
读取文本 | 报错 | 从开头读取 |
"w" |
写入文本 | 创建 | 打开时截断原内容 |
"a" |
追加文本 | 创建 | 追加到末尾 |
"x" |
独占创建并写入 | 创建 | 抛出 FileExistsError |
"rb" |
读取二进制 | 报错 | 读取原始字节 |
"wb" |
写入二进制 | 创建 | 打开时截断原内容 |
还可以使用 + 开启读写,例如 "r+"、"w+",但初学时分别使用明确的读模式或写模式通常更容易理解。
需要特别注意:"w" 在成功打开文件时就会清空原有内容,不是等到调用 write() 才清空。
6.3 一次读取全部内容
python
from pathlib import Path
if __name__ == '__main__':
path = Path("notes.txt")
with path.open("w", encoding="utf-8") as file:
file.write("第一行\n第二行\n")
with path.open("r", encoding="utf-8") as file:
content = file.read()
print(repr(content))
运行结果:
'第一行\n第二行\n'
read() 在文本模式下返回字符串。
不指定读取长度时,会读取当前位置之后的全部内容。因此,这种方式适合能够合理放入内存的文件。
6.4 逐行读取
对于较大的文本文件,可以直接遍历文件对象:
python
from pathlib import Path
if __name__ == '__main__':
path = Path("notes.txt")
path.write_text("第一行\n第二行\n", encoding="utf-8")
with path.open("r", encoding="utf-8") as file:
for line_number, line in enumerate(file, start=1):
text = line.rstrip("\n")
print(f"{line_number}:{text}")
运行结果:
1:第一行
2:第二行
普通文本读取默认会将常见换行形式统一转换为 \n。这里使用 rstrip("\n") 去掉行末换行,不直接使用 strip(),是为了保留正文中可能有意义的首尾空格。
常见读取方式:
| 方式 | 返回结果 |
|---|---|
read() |
剩余全部内容组成的字符串 |
readline() |
一行字符串,通常保留行末换行 |
readlines() |
剩余各行组成的列表 |
for line in file |
逐行迭代 |
readlines() 会把剩余各行放入列表,不应将它当成节省大文件内存的方式。
6.5 写入与追加
python
from pathlib import Path
if __name__ == '__main__':
path = Path("study.txt")
with path.open("w", encoding="utf-8") as file:
file.write("第一天:变量与字符串\n")
with path.open("a", encoding="utf-8") as file:
file.write("第二天:条件判断与循环\n")
print(path.read_text(encoding="utf-8"), end="")
运行结果:
第一天:变量与字符串
第二天:条件判断与循环
追加模式不会自动插入换行,因此需要根据文件格式自己添加 \n。
writelines() 也不会自动添加换行:
with open("study.txt", "w", encoding="utf-8") as file:
file.writelines([
"第一行\n",
"第二行\n"
])
6.6 文件有当前位置
连续读取时,第二次读取会从当前位置继续:
python
from pathlib import Path
if __name__ == '__main__':
path = Path("letters.txt")
path.write_text("abcdef", encoding="utf-8")
with path.open("r", encoding="utf-8") as file:
print(file.read(2))
print(file.read())
print(repr(file.read()))
file.seek(0)
print(file.read(3))
运行结果:
ab
cdef
''
abc
读到末尾后,再读取会得到空字符串。seek(0) 将位置移回文件开头。
文本模式下,read(n) 按字符数读取;涉及多字节编码时,不要把任意字符位置直接当成可以自由跳转的字节偏移。需要精确处理原始字节时,应使用二进制模式。
6.7 Path 的快捷方法
对于小型文本文件,可以直接使用:
python
path.write_text("内容", encoding="utf-8")
content = path.read_text(encoding="utf-8")
它们会自行打开并关闭文件。其中,write_text() 同样会覆盖已有内容。需要追加或逐行处理时,使用 open() 更合适。
7. 编码与二进制文件
7.1 为什么需要指定编码
文本在程序中是字符串,保存到文件时需要转换为字节;读取时再把字节转换为字符串。
这个转换规则就是编码。
python
with open("notes.txt", "r", encoding="utf-8") as file:
content = file.read()
明确指定编码,可以避免程序在不同系统环境下依赖不同的默认值。如果文件实际使用 GBK 保存,却使用 UTF-8 读取,可能产生 UnicodeDecodeError,也可能读出错误内容。应根据文件真实编码读取,例如:
python
with open("legacy.txt", "r", encoding="gbk") as file:
content = file.read()
7.2 不要把忽略解码错误当作通用修复
下面的写法会跳过无法解码的数据:
python
open("notes.txt", encoding="utf-8", errors="ignore")
虽然可能不再报错,但内容也可能被悄悄丢弃。如果数据需要准确保留,应先确认实际编码或修复文件,而不是直接忽略错误。
7.3 二进制模式
图片、压缩包等文件应按字节处理:
python
from pathlib import Path
if __name__ == '__main__':
path = Path("sample.bin")
with path.open("wb") as file:
file.write(b"\x00\x01\x02ABC")
with path.open("rb") as file:
content = file.read()
print(content)
print(type(content))
运行结果:
b'\x00\x01\x02ABC'
<class 'bytes'>
二进制模式返回和接收的是 bytes,不需要、也不能同时指定文本编码参数 encoding。
7.4 分块复制文件
python
from pathlib import Path
def copy_binary_file(source: Path, destination: Path) -> None:
"""分块复制到新文件;目标已存在时拒绝覆盖。"""
with source.open("rb") as reader:
with destination.open("xb") as writer:
while True:
chunk = reader.read(64 * 1024)
if not chunk:
break
writer.write(chunk)
if __name__ == '__main__':
source = Path("source.bin")
destination = Path("copied.bin")
source.write_bytes(b"Python file example")
try:
copy_binary_file(source, destination)
except FileExistsError:
print("目标文件已存在")
else:
print("复制完成")
每次最多读取 64 KiB,避免一次将整个文件读入内存。
这里使用 "xb",防止误覆盖已有目标文件。复制过程中如果发生错误,仍可能留下不完整的目标文件;更完整的保存策略会在后面的综合示例中介绍。
8. 使用 JSON 保存结构化数据
8.1 JSON 适合保存什么
普通文本适合笔记和日志,但如果要保存列表、字典等结构化数据,JSON 更方便。
例如:
{
"name": "矿泉水",
"price": 2,
"stock": 100,
"enabled": true
}
Python 标准库的 json 模块可以完成转换。
8.2 dumps 与 loads:处理字符串
python
import json
if __name__ == '__main__':
product = {
"name": "矿泉水",
"price": 2,
"enabled": True,
"remark": None
}
text = json.dumps(product, ensure_ascii=False)
print(text)
restored = json.loads(text)
print(restored["name"])
print(type(restored))
运行结果:
{"name": "矿泉水", "price": 2, "enabled": true, "remark": null}
矿泉水
<class 'dict'>
其中:
dumps():Python 对象转换为 JSON 字符串。loads():JSON 字符串转换为 Python 对象。ensure_ascii=False:让中文直接显示,而不是写成 Unicode 转义形式。
它不负责指定文件编码,文件编码仍由 open() 等文件操作设置。
8.3 dump 与 load:处理文件
python
import json
from pathlib import Path
if __name__ == '__main__':
path = Path("products.json")
products = [
{"id": 1001, "name": "矿泉水", "stock": 100},
{"id": 1002, "name": "可乐", "stock": 20}
]
with path.open("w", encoding="utf-8") as file:
json.dump(products, file, ensure_ascii=False, indent=2)
with path.open("r", encoding="utf-8") as file:
restored = json.load(file)
print(restored)
运行结果:
[{'id': 1001, 'name': '矿泉水', 'stock': 100}, {'id': 1002, 'name': '可乐', 'stock': 20}]
indent=2 表示使用两格缩进排版,方便人工阅读。
| 函数 | 输入或输出 |
|---|---|
json.dumps() |
生成 JSON 字符串 |
json.loads() |
解析 JSON 字符串 |
json.dump() |
将 JSON 写入文件对象 |
json.load() |
从文件对象读取并解析 JSON |
8.4 类型转换与限制
| Python 类型 | JSON 类型 | 读取后的常见 Python 类型 |
|---|---|---|
dict |
object | dict |
list、tuple |
array | list |
str |
string | str |
int |
number | int |
float |
number | float |
True、False |
true、false | bool |
None |
null | None |
需要注意:
- 元组保存后再读取,通常变为列表。
- JSON 对象的键是字符串,避免依赖整数键原样往返。
- 集合、
Path、日期对象等不能直接通过默认编码器保存。 - JSON 解析成功,不代表字段和业务数据一定合法。
例如:
json.loads('{"stock": "很多"}')
是合法 JSON,但 "stock" 的值不符合整数库存要求,需要程序进一步检查。
8.5 捕获 JSON 格式错误
python
import json
if __name__ == '__main__':
text = '{"name": "矿泉水",}'
try:
data = json.loads(text)
except json.JSONDecodeError as error:
print(f"JSON 格式错误,位于第 {error.lineno} 行")
运行结果:
JSON 格式错误,位于第 1 行
JSON 不允许在最后一个成员后保留多余逗号,字符串和键也需要使用双引号。不要通过 eval() 解析 JSON 或其他外部文本。eval() 会执行 Python 表达式,不是安全的数据解析方式。
8.6 不要直接向 JSON 数组末尾追加对象
如果文件原本是:
[{"id": 1001}]
直接使用追加模式写入另一个对象,会得到类似:
[{"id": 1001}]{"id": 1002}
这不是一个有效的完整 JSON 文档。
对于小型 JSON 列表,通常需要:
- 读取原列表。
- 修改内存中的列表。
- 将整个列表重新保存。
大规模数据或多进程共享写入,不应继续简单依赖这个方式,可以根据需求使用数据库或专门的存储格式。
9. 使用 CSV 读写表格数据
9.1 CSV 的特点
CSV 常用于保存行列结构的数据,例如:
id,name,stock
1001,矿泉水,100
1002,饼干,20
它适合简单表格,不适合直接表达复杂嵌套结构。
应使用标准库 csv,而不是简单地按逗号切割。字段内部可能包含逗号、双引号甚至换行,需要正确处理引用规则。
9.2 写入 CSV
python
import csv
from pathlib import Path
if __name__ == '__main__':
path = Path("products.csv")
products = [
{"id": 1001, "name": "矿泉水", "stock": 100},
{"id": 1002, "name": "饼干,家庭装", "stock": 20}
]
with path.open("w", encoding="utf-8", newline="") as file:
writer = csv.DictWriter(
file,
fieldnames=["id", "name", "stock"]
)
writer.writeheader()
writer.writerows(products)
文件内容:
id,name,stock
1001,矿泉水,100
1002,"饼干,家庭装",20
因为名称中包含逗号,csv 模块会自动添加必要的引号。newline="" 让 CSV 模块负责处理换行,避免平台换行转换造成额外空行等问题。
9.3 读取 CSV
python
import csv
from pathlib import Path
if __name__ == '__main__':
path = Path("products.csv")
with path.open("r", encoding="utf-8", newline="") as file:
reader = csv.DictReader(file)
for row in reader:
product_id = int(row["id"])
stock = int(row["stock"])
print(product_id, row["name"], stock)
运行结果:
1001 矿泉水 100
1002 饼干,家庭装 20
DictReader 默认按字符串读取这些字段,需要数值时应明确转换。对于外部 CSV,还需要根据情况处理缺少列、空值和非法数字,不能只因为文件能够打开就认为数据正确。