【Python基础篇-05】深入理解python的异常处理与文件读写

深入理解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("程序继续执行")

运行结果:

复制代码
年龄必须是整数
程序继续执行

执行过程:

  1. 进入 try。
  2. int(text) 抛出 ValueError。
  3. 跳过 try 中剩余语句。
  4. 执行匹配的 except。
  5. 继续执行整个结构后面的代码。

如果没有异常,则跳过 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 列表,通常需要:

  1. 读取原列表。
  2. 修改内存中的列表。
  3. 将整个列表重新保存。

大规模数据或多进程共享写入,不应继续简单依赖这个方式,可以根据需求使用数据库或专门的存储格式。

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,还需要根据情况处理缺少列、空值和非法数字,不能只因为文件能够打开就认为数据正确。

相关推荐
Zhou1411361 小时前
SpringMVC_02_注解开发实战
开发语言·windows·python
禹凕2 小时前
机器学习之数据清洗(Machine Learning about Data Cleaning)
人工智能·爬虫·python·机器学习·数据挖掘
浩瀚地学2 小时前
deepagents学习打卡day07
经验分享·笔记·python·学习·agent
霸道流氓气质2 小时前
Mermaid 图表完全指南:从文本语法到LangGraph4j工作流可视化实战
开发语言·python
SamChan902 小时前
PDF翻译时页眉页脚总在捣乱?跨页重复文本块的检测与过滤实测
人工智能·python·ai·pdf·wpf
晴空蓝天2 小时前
MDC traceId 全链路日志追踪:Spring Boot 3.5 里把日志串成一条线
java·spring boot·后端·python
L@ncor2 小时前
第六章可能出现的问题:依赖冲突与成本乘法
python·依赖管理·langgraph·agentscope·避坑
AC赳赳老秦2 小时前
财报附注表格精准提取:OpenClaw 从 PDF 年报附注挖掘隐藏明细,补齐财务分析维度
java·汇编·c++·python·青少年编程·deepseek·openclaw
小小张说故事2 小时前
CatBoost 入门指南:类别特征为什么不用 One-Hot?Python 实战与 5 个坑
python·机器学习