文件、异常及常用标准库
前置知识:函数、作用域与参数
本章目录
本章学习目标
- 使用 pathlib、csv 和 json 保存数据
- 在边界处处理可预期异常
- 用日期与计数工具完成小型清洗
一、文件路径与编码属于输入契约
相对路径默认相对于当前工作目录,不相对于脚本文件。终端从不同目录启动时,open('data.csv') 可能找不到文件或读取了同名错误文件。课程示例使用 Path(__file__).resolve().parents[1] 定位课程根目录,再拼接 outputs;所有写入限制在该目录中。Path 的斜杠运算拼接路径,不需要手动混合 Windows 反斜杠与 Linux 正斜杠。
文本文件需要指定编码,课程统一使用 UTF-8;读取某些 Windows 工具导出的带 BOM 文件可以用 utf-8-sig。编码错误和内容格式错误是两个问题,不能用 errors='ignore' 长期吞掉坏字符,否则用户编号或中文类别可能被无声修改。with path.open(...) as f 在离开代码块时关闭资源,即使发生异常也能完成清理;读写模式决定是否创建、截断或追加文件,w 会覆盖已有内容,使用前必须确认目标。
二、CSV 与 JSON 不是普通拼字符串
CSV 的字段可能包含逗号、双引号甚至换行,因此不能只调用 line.split(',')。csv.DictReader 用表头映射到字符串字段,DictWriter 可按固定字段写出;Windows 读写 CSV 时用 newline='' 让模块处理换行,减少空行问题。读取后金额还是字符串,必须显式转换并处理空值,不能因为表头写 amount 就认为已经是数字。
JSON 保存结构化对象,适合进度或实验配置;只支持有限的数据类型,不会自动保存任意 Python 对象。json.dump(...,ensure_ascii=False) 便于阅读中文,日期对象应先转换成约定格式。保存配置后重新读取并比较是最基本的往返验证。若字段需要严格保证类型,读取后再验证,不要信任 JSON 文件中的计数字段一定是整数。
三、日期、计数与精确金额
标准库 datetime 把日期字符串转换成可计算对象,例如 date.fromisoformat('2026-01-31') 加一天会进入二月,不必自己处理月份长度。日期表示日,datetime 表示日期时间;分析时必须说明时区和按天切分的口径。课程业务数据采用统一约定的合成时间,真实日志则不能把不同时区字符串直接按字面分组。
Counter 可以统计类别出现次数,defaultdict 可以在首次访问时创建默认累计结构。Decimal 从字符串构造能表达精确十进制金额,避免把已有 float 的误差带进去;例如 Decimal('0.1')+Decimal('0.2') 正好等于 Decimal('0.3')。这些工具解决不同问题:Counter 负责次数,日期工具负责时间规则,Decimal 负责精度,不能仅为了展示标准库而让每一步变得更复杂。
四、异常处理要保留可诊断信息
try/except 适用于已知会失败的操作,如文件不存在、数值解析失败。捕获具体异常比裸 except 更清楚;若捕获后仅 pass,数据质量问题会从输出消失,严重时还会让统计分母错误。清洗 CSV 时应记录行号、字段和值,并区分可跳过的坏记录与必须停止的结构错误,例如缺少主键表头。
finally 用于始终执行清理,但大多数文件操作用 with 更容易正确。异常可以通过 raise ValueError(...) from exc 关联原始解析原因,使 traceback 保留上下文。不要把所有问题转换成“文件读取失败”,那会掩盖权限、编码、缺列和业务约束等不同原因。日志应解释发生了什么,而不是只打印“错误”;教学脚本的预期异常捕获后要明确输出该反例已被拒绝。
五、建立最小数据往返流水线
配套脚本在 outputs 中写两行 CSV:一名用户买十点五元,另一名用户买二十元;重新读取后使用 Decimal 求和为三十点五,并将人数和总额写入 JSON 再读回。这里把金额作为字符串写入 JSON 是有意保留精确表示,而不是让 Decimal 自动序列化。文件名固定,只覆盖课程自有输出,不访问用户文档。
验证时加入含逗号的类别、中文字段值、空文件、缺少表头和非法金额。空 CSV 与零行合法表不同,必须明确处理;空表的金额合计可定义为零,但活跃用户数和平均额仍要考虑分母。先学会读取、清洗、计算、保存和检查,再转向 pandas 的大规模表操作。读取成功不是完成任务,仍要检查行数、唯一键、数据类型和业务取值范围。
完整示例与实际输出
from pathlib import Path
from decimal import Decimal
from datetime import date, timedelta
from collections import Counter
import csv, json
ROOT=Path(__file__).resolve().parents[1]
out=ROOT/'outputs'; out.mkdir(exist_ok=True)
path=out/'py-files-orders.csv'
rows=[{'user':'1','amount':'10.50','category':'book, study'},
{'user':'2','amount':'20.00','category':'中文类别'}]
with path.open('w',encoding='utf-8',newline='') as f:
writer=csv.DictWriter(f,fieldnames=['user','amount','category'])
writer.writeheader(); writer.writerows(rows)
with path.open(encoding='utf-8',newline='') as f:
loaded=list(csv.DictReader(f))
assert loaded==rows
total=sum((Decimal(r['amount']) for r in loaded),Decimal('0'))
summary={'users':len({r['user'] for r in loaded}),'total':str(total)}
config=out/'py-files-summary.json'
config.write_text(json.dumps(summary,ensure_ascii=False,indent=2),encoding='utf-8')
assert json.loads(config.read_text(encoding='utf-8'))==summary
assert total==Decimal('30.50')
assert date(2026,1,31)+timedelta(days=1)==date(2026,2,1)
print(summary)
print('category counts',dict(Counter(r['category'] for r in loaded)))
print('files written',path.name,config.name)
实际运行通过CPU Python execution.
{'users': 2, 'total': '30.50'}
category counts {'book, study': 1, '中文类别': 1}
files written py-files-orders.csv py-files-summary.json五道自测:先回答,再核对
01 · open('data.csv') 相对哪个目录?
查看提示
启动位置和源码位置可能不同。
查看参考答案
默认相对于当前工作目录。
02 · 含逗号的 CSV 字段能用 split(',') 正确读取吗?
查看提示
引号与转义。
查看参考答案
不能,应使用 csv 模块处理 CSV 规则。
03 · 为什么 Decimal 优先从字符串构造?
查看提示
float 可能已带误差。
查看参考答案
直接从十进制文本构造保留原始精度;从 float 构造会保留该浮点近似。
04 · except Exception: pass 会带来什么风险?
查看提示
记录还能追踪吗?
查看参考答案
会吞掉异常和坏数据信息,让分析结果与分母无声变化。
05 · JSON 能直接保存 datetime 或 Decimal 吗?
查看提示
标准编码器支持的类型有限。
查看参考答案
默认不能;需转换为明确的字符串/数值格式或自定义序列化。
两项动手任务
任务 1 · 写出含逗号和中文的两行 CSV,读回确认字段不被拆坏。
提示
newline=''、UTF-8、csv.writer。
完整参考解答与检查方式
所有写入限制在课程 outputs;重新读回比较完整记录。
from pathlib import Path
import csv
ROOT=Path(__file__).resolve().parents[1]
out=ROOT/'outputs'; out.mkdir(exist_ok=True)
p=out/'task-csv.csv'; rows=[['name','value'],['甲,乙','10'],['丙','20']]
with p.open('w',encoding='utf-8',newline='') as f:
csv.writer(f).writerows(rows)
with p.open(encoding='utf-8',newline='') as f:
loaded=list(csv.reader(f))
assert loaded==rows
print(loaded)任务 2 · 解析金额 ['10.00','bad','0.50'],汇总有效金额并记录坏值所在位置。
提示
只捕获 InvalidOperation。
完整参考解答与检查方式
有效合计十点五;坏值在从一开始计数的第二项。
from decimal import Decimal, InvalidOperation
values=['10.00','bad','0.50']; total=Decimal('0'); errors=[]
for line,value in enumerate(values,1):
try:
amount=Decimal(value)
if not amount.is_finite():
raise InvalidOperation('nonfinite')
except InvalidOperation:
errors.append((line,value))
else:
total+=amount
assert total==Decimal('10.50') and errors==[(2,'bad')]
print(total,errors)官方资料与掌握状态
完成两项任务后,按独立完成程度记录。状态会尽量保存在当前浏览器,可导出备份。
动手任务记录
掌握状态与复习
关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。
记录尽量保存在当前浏览器,建议定期导出备份。