引言
第七章(文件与异常处理)到此收官。本篇综合测验严格以第 1 至 8 篇为范围:open 的文件对象、模式与关闭(第 1 篇)、读取与写入 API 全解(第 2 篇)、文件编码与文本/二进制模式(第 3 篇)、pathlib 路径对象(第 4 篇)、IOError 家族与防御式读写(第 5 篇)、os 模块的目录与环境变量(第 6 篇)、CSV 与 JSON 结构化文件(第 7 篇)、配置文件的读取、校验与默认值(第 8 篇)。异常处理本身的语法(try/except/else/finally、异常层次、raise)在第四章第 8 篇已经系统讲述,本章只考察它在文件场景中的应用,题目不重复语法定义,也不引入第八章及以后的任何知识。
全篇分三个部分:10 道选择题按「记忆 → 理解 → 应用 → 分析」的难度梯度排列,每题标注难度,覆盖八篇内容的全部考点,并附逐题解析;3 道编程题给出可独立运行的参考解答与实测输出;最后是一个综合任务——从一份学生成绩 CSV 出发,完成字段校验、汇总报表与 JSON 导出的全链路,考察你把本章知识串起来解决真实问题的能力。建议先独立完成全部题目再对照解析;做错的题要回到对应篇章重新理解原理。文中所有运行结果均基于本机 CPython 3.12.10(Windows)实测,未做任何修饰。
选择题(共 10 题,每题只有一个正确答案)
第 1 题:open 的写入模式语义【记忆】
with open('q1.txt', 'w', encoding='utf-8') as f:
f.write('abc')
with open('q1.txt', 'a', encoding='utf-8') as f:
f.write('def')
with open('q1.txt', 'w', encoding='utf-8') as f:
f.write('xyz')
print(open('q1.txt', encoding='utf-8').read())
输出是什么?
A. abcdefxyz
B. xyz
C. abcdef
D. 第三次 open 抛出异常(文件已存在)
第 2 题:readline 与文件末尾【记忆】
with open('q2.txt', 'w', encoding='utf-8') as f:
f.write('第一行\n第二行')
f = open('q2.txt', encoding='utf-8')
a, b, c = f.readline(), f.readline(), f.readline()
f.close()
print(repr(a), repr(b), repr(c))
输出是什么?(文件内容恰好两行,末尾没有换行符)
A. '第一行\n' '第二行' ''
B. '第一行' '第二行' None
C. '第一行\n' '第二行\n' ''
D. 第三次调用 readline() 抛 EOFError
第 3 题:编码不匹配的后果【理解】
with open('q3.txt', 'w', encoding='utf-8') as f:
f.write('你好')
print(open('q3.txt', encoding='gbk').read())
输出是什么?(本机 Windows 默认编码为 GBK 系)
A. 输出 你好
B. 输出 浣犲ソ(乱码,但不报错)
C. 抛出 UnicodeDecodeError
D. 抛出 UnicodeEncodeError
第 4 题:pathlib 的路径属性【记忆】
from pathlib import Path
p = Path('logs/access.log')
print(p.parent)
print(p.name, p.stem, p.suffix)
输出是什么?
A. 第一行 logs;第二行 access.log access .log
B. 第一行 logs;第二行 logs access log
C. 第一行 logs/access.log;第二行 access.log access .log
D. 第一行 Path('logs');第二行 access .log access.log
第 5 题:except 分支的顺序与继承关系【理解】
try:
with open('no_such_file.tmp', encoding='utf-8') as f:
print(f.read())
except FileNotFoundError:
print('文件不存在:走 FileNotFoundError 分支')
except OSError:
print('其他 OSError 分支')
输出是什么?
A. 打印「其他 OSError 分支」
B. 两行都打印
C. 只打印「文件不存在:走 FileNotFoundError 分支」
D. FileNotFoundError 不被捕获,直接抛出终止程序
第 6 题:os 创建目录的重复调用【记忆】
import os
os.makedirs('q6_dir', exist_ok=True)
try:
os.makedirs('q6_dir')
except OSError as e:
print(type(e).__name__)
输出是什么?
A. FileNotFoundError
B. FileExistsError
C. PermissionError
D. 不打印任何内容,程序静默继续
第 7 题:csv.reader 的返回形态【理解】
import csv
with open('q7.csv', 'w', encoding='utf-8', newline='') as f:
f.write('a,b\n10,20\n')
with open('q7.csv', encoding='utf-8', newline='') as f:
print(list(csv.reader(f)))
输出是什么?
A. [['a', 'b'], ['10', '20']]
B. [['a', 'b'], [10, 20]]
C. ['a', 'b', '10', '20']
D. [('a', 'b'), ('10', '20')]
第 8 题:json.dumps 的默认编码行为【应用】
import json
print(json.dumps({'msg': '你好'}))
print(json.dumps({'msg': '你好'}, ensure_ascii=False))
输出是什么?
A. 两行都输出 {"msg": "你好"}
B. 第一行 {"msg": "\u4f60\u597d"},第二行 {"msg": "你好"}
C. 第一行 {"msg": "你好"},第二行抛 TypeError
D. 两行都输出 {"msg": "\u4f60\u597d"}
第 9 题:configparser 的类型转换与缺键行为【应用】
import configparser
config = configparser.ConfigParser()
config.read_string('[server]\nhost = 127.0.0.1\nport = 8080\ndebug = true\n')
print(config.getboolean('server', 'debug'))
print(type(config.getboolean('server', 'debug')).__name__)
try:
config.get('server', 'nokey')
except Exception as e:
print(type(e).__name__)
输出是什么?
A. True、bool、KeyError
B. True、bool、NoOptionError
C. 'true'、str、KeyError
D. True、bool、NoSectionError
第 10 题:with 块结束后的文件对象【分析】
f = open('q10.txt', 'w', encoding='utf-8')
f.write('abc')
f.close()
f = open('q10.txt', encoding='utf-8')
with f:
print(repr(f.read(1)))
print(f.closed)
print(f.read(1))
输出是什么?
A. 依次输出 'a'、True,最后一行抛 ValueError(I/O operation on closed file)
B. 依次输出 'abc'、True、''
C. 依次输出 'a'、False、'bc'
D. 最后一行抛 SyntaxError
点击查看答案
答案:B A B A C B A B B A
第 1 题(B):'w' 模式每次打开都会先清空再写入,'a' 模式不清空、从末尾追加(第 1 篇)。流程是:写 abc → 追加成 abcdef → 再用 'w' 打开清空并写入 xyz,最终内容只有 xyz。选 A 的同学把第三次 'w' 的截断行为忽略了;选 D 则把 'w' 与 'x' 混为一谈——'x' 模式在文件已存在时才会抛 FileExistsError。
第 2 题(A):readline() 保留行尾的换行符,所以第一行返回 '第一行\n';最后一行没有换行符时按原样返回 '第二行';读到文件末尾后再调用返回空串 '',而不是 None,更不会抛 EOFError(第 2 篇反复强调的 EOF 约定)。选 C 是假设了”每行末尾都有换行”;选 B 混淆了空串与 None。
第 3 题(B):UTF-8 编码的「你好」字节流恰好能被 GBK 解码器拼成另两个合法汉字「浣犲ソ」——所以不报错,只是静默乱码(本机实测输出正是 浣犲ソ)。编码错配时”静默乱码”比”直接报错”更常见也更危险:程序照常运行,数据却已损坏。第 3 篇的结论在此应验:读写文件必须显式指定 encoding,别无他法。选 C 的同学记住了会报错,却忽视了”可能恰好能解码”的第三种结果;若读写方向颠倒(按 GBK 写、按 UTF-8 读)则通常抛 UnicodeDecodeError。
第 4 题(A):p.parent 返回父目录 logs,p.name 是完整文件名 access.log,p.stem 是去掉扩展名的 access,p.suffix 是扩展名 .log(第 4 篇的四个基础属性)。注意 parent 打印出来就是 logs 而不是 Path('logs')——Path 对象的 __str__ 就是路径字符串,打印时看不出类型差异,这也是初学 pathlib 常见的迷惑点。
第 5 题(C):open 一个不存在的文件抛 FileNotFoundError,它是 OSError 的子类(第 5 篇的 IOError 家族图)。except 自上而下匹配第一个成功的分支,所以先写子类 FileNotFoundError 才轮得到父类 OSError;顺序颠倒的话子类分支永远执行不到(第四章的语法规则在这里有了真实的文件场景。子类在前、父类在后是铁律。
第 6 题(B):第一次 os.makedirs(..., exist_ok=True) 目录已存在也不报错;第二次没带 exist_ok,目录已存在 → FileExistsError(OSError 的子类),代码输出它的类名。选 D 的同学忽略了一点:必须显式写 exist_ok=True 或捕获 FileExistsError,makedirs 本身并不会”静默继续”(第 6 篇)。
第 7 题(A):csv.reader 把每行按分隔符切成字符串列表,10 和 20 也是字符串,不会自动转数字(第 7 篇)。选 B 混淆了 CSV 库与类型转换的职责——需要数值时必须自己 int();选 C 是忘了按行分组;选 D 是把它与返回元组的其它解析器弄混。
第 8 题(B):json.dumps 默认 ensure_ascii=True,「你好」被转成 \u4f60\u597d 转义序列输出——这是 JSON 标准允许的形式,任何解析器(json.loads)都能还原成中文;显式传 ensure_ascii=False 才原样输出中文(第 7 篇)。转义不是乱码也不是报错,它只是”安全但长”的表示:网络传输与存盘都用默认值最稳妥,人读的文件再考虑 False。
第 9 题(B):getboolean 会把字符串 'true' 解析成布尔值并返回真正的 bool 对象(True、bool),这是第 8 篇配置读取的核心便利;查询不存在的 option 抛 configparser.NoOptionError——它是专门为配置定义的异常,不是字典的 KeyError(第 8 篇)。选 A 混淆了两种缺键异常;选 C 忘了 getboolean 做类型转换;选 D 错在”节存在、选项不存在”应抛 NoOptionError 而非 NoSectionError。
第 10 题(A):with 块结束时自动调用文件对象的 __exit__ 关闭文件,所以 f.closed 为 True;对一个已关闭的文件对象做任何 I/O 操作(读、写、seek)都会抛 ValueError: I/O operation on closed file(第 1、2 篇的关闭语义)。选 B、C 都是”以为文件还敞着”的错觉——with 的关闭是自动且强制的,与手动 close() 效果相同。这也是”永远用 with 而不是裸 open”的根本理由:块结束,句柄必然归还。
编程题(共 3 题,附可运行参考解答)
编程题 1:目录遍历统计文件大小
题目:用 os.walk 遍历一个三层示例目录,按扩展名分组统计各组的文件数与合计大小,输出文件总数、总大小与最大的文件。要求能独立运行——示例目录与文件在代码内自建。
参考解答:
import os
# 构造三层示例目录与文件(内容故意不同大小)
os.makedirs('proj/src/utils', exist_ok=True)
os.makedirs('proj/docs', exist_ok=True)
files = {
'proj/README.md': '项目说明\n' * 100,
'proj/docs/guide.md': '使用指南\n' * 200,
'proj/src/main.py': 'print("hello")\n' * 30,
'proj/src/utils/helper.py': 'def add(a, b):\n return a + b\n' * 40,
'proj/src/data.json': '{"a": 1}' * 500,
}
for name, content in files.items():
with open(name, 'w', encoding='utf-8') as f:
f.write(content)
ext_stats = {}
total_bytes = total_files = 0
largest = ('', 0)
for root, dirs, names in os.walk('proj'):
for name in names:
path = os.path.join(root, name)
size = os.path.getsize(path)
total_files += 1
total_bytes += size
if size > largest[1]:
largest = (path, size)
ext = os.path.splitext(name)[1] or '(无扩展名)'
count, size_sum = ext_stats.get(ext, (0, 0))
ext_stats[ext] = (count + 1, size_sum + size)
for ext in sorted(ext_stats):
count, size_sum = ext_stats[ext]
print(f'{ext:<10} 文件数 {count:>2},合计大小 {size_sum:>6} 字节')
print('---')
print(f'文件总数 {total_files},总大小 {total_bytes} 字节')
print('最大文件:', largest[0], ',', largest[1], '字节')
运行结果:
.json 文件数 1,合计大小 4000 字节
.md 文件数 2,合计大小 4200 字节
.py 文件数 2,合计大小 1840 字节
---
文件总数 5,总大小 10040 字节
最大文件: proj\src\data.json , 4000 字节
关键点:os.walk 递归产出每个目录下的文件名单(第 6 篇),os.path.splitext 拆扩展名作为分组键,os.path.getsize 取字节大小(第 6 篇);字典值用元组 (计数, 累计大小) 就地更新,or '(无扩展名)' 处理无后缀文件。Windows 上 os.path.join 拼接出的路径用反斜杠(输出里的 proj\src\data.json 就是本机实况)。数组求和与”最大文件”维护是遍历型统计的标准姿势:一趟循环即出全部指标,复杂度 O(文件数)。
编程题 2:损坏 JSON 容错读取
题目:一个 JSON Lines 文件(每行一条独立 JSON 记录)中有一行被写坏。请逐行读取并解析:坏行跳过、记录跳过原因,好行全部保留,最终输出成功条数与跳过条数。要求自包含:坏行在代码内制造。
参考解答:
import json
# 一行一个 JSON 记录,其中第 2 行被写坏(结构不完整)
with open('records.jsonl', 'w', encoding='utf-8') as f:
f.write('{"id": 1, "name": "张伟"}\n')
f.write('{"id": 2, "name": "李娜", \n')
f.write('{"id": 3, "name": "王强"}\n')
good, bad = [], 0
with open('records.jsonl', encoding='utf-8') as f:
for line_no, line in enumerate(f, 1):
try:
good.append(json.loads(line))
except json.JSONDecodeError as e:
bad += 1
print(f'第 {line_no} 行损坏,跳过:{e}')
print('成功解析', len(good), '条,跳过', bad, '条')
for rec in good:
print(f' id={rec["id"]},name={rec["name"]}')
运行结果:
第 2 行损坏,跳过:Expecting property name enclosed in double quotes: line 2 column 1 (char 25)
成功解析 2 条,跳过 1 条
id=1,name=张伟
id=3,name=王强
关键点:json.loads 失败抛 json.JSONDecodeError(它是 ValueError 的子类,第 5 篇的捕获原则在这里生效——捕获精确类型,不裸 except);按行读取配合 enumerate(f, 1) 天然给出行号(第 2 篇的迭代读取 + 第四章的异常流程);坏行只影响自己这一行,好数据全部保留——这正是日志类数据”部分损坏不拖垮全量”的容错设计。错误信息里的 (char 25) 是坏行内出错字符的位置,直接定位问题,无需人工数数。
编程题 3:GBK 编码文件转 UTF-8
题目:一份来自旧系统的 GBK 编码文本文件,需要转存为 UTF-8。请完成:先演示”按 UTF-8 直接读 GBK 文件”的后果,再按 GBK 读入、UTF-8 写出,最后按 UTF-8 读回验证。
参考解答:
# 模拟旧系统导出的 GBK 编码文件
with open('old_gbk.txt', 'w', encoding='gbk') as f:
f.write('报告编号:2026-0902\n')
f.write('作者:张三丰\n')
# 错误示范:按 UTF-8 读 GBK 文件
try:
with open('old_gbk.txt', encoding='utf-8') as f:
print(f.read())
except UnicodeDecodeError as e:
print('按 UTF-8 读取 GBK 文件 -> UnicodeDecodeError')
# 正确做法:显式按 GBK 读入,再按 UTF-8 写出
with open('old_gbk.txt', encoding='gbk') as f:
text = f.read()
with open('new_utf8.txt', 'w', encoding='utf-8') as f:
f.write(text)
print('转码后按 UTF-8 读回:')
print(open('new_utf8.txt', encoding='utf-8').read().strip())
运行结果:
按 UTF-8 读取 GBK 文件 -> UnicodeDecodeError
转码后按 UTF-8 读回:
报告编号:2026-0902
作者:张三丰
关键点:读写每次都显式给 encoding(第 3 篇的铁律):第一个 try 演示了不匹配的代价——GBK 字节流里存在非法 UTF-8 序列时UnicodeDecodeError当场抛出;encoding='gbk' 精确解码后,encoding='utf-8' 写出,最后按 UTF-8 读回逐字正确。同一份文本经「读入—转出—读回」三趟而不丢字,就是编码处理正确的判据。注意 Windows 上不写 encoding 的 open 默认按 GBK 系处理,这份代码在 Linux 上跑才会立刻暴露差别——显式指定让行为在所有平台一致。
综合任务:学生成绩 CSV → 校验 → 汇总报表 → JSON 导出
任务描述:一份成绩单 grades.csv 含学号、姓名、语文、数学、英语五列,其中混有脏数据:分数缺失(空字段)、分数超出 0-100 范围、分数不是数字(如 abc)。请完成全链路:①用 csv.DictReader 读取全部行;②逐行校验三个分数(空、非数字、越界三类错误分别给出明确原因),只保留合法记录;③输出汇总报表——每科平均分与及格率(≥60 分);④把合法记录导出为 grades_valid.json(中文不转义、缩进 2 格),并预览前两条。
考察点:CSV 的 DictReader 读取与 newline=''(第 7 篇)、自定义校验与 ValueError 精确捕获(第 5 篇 + 第四章语法)、json.dump 与 ensure_ascii=False(第 7 篇)、字典推导与统计报表(第六章知识联动)。
参考实现:
import csv
import json
csv_text = (
'学号,姓名,语文,数学,英语\n'
'001,张伟,88,76,92\n'
'002,李娜,76,82,79\n'
'003,王强,65,,58\n'
'004,赵磊,84,90,58\n'
'005,孙丽,103,95,88\n'
'006,周明,70,76,abc\n'
)
with open('grades.csv', 'w', encoding='utf-8', newline='') as f:
f.write(csv_text)
rows = []
with open('grades.csv', encoding='utf-8', newline='') as f:
for row in csv.DictReader(f):
rows.append(row)
def parse_score(row, col):
raw = row[col].strip()
if not raw:
raise ValueError(f'{col} 为空')
try:
score = int(raw)
except ValueError:
raise ValueError(f'{col} 不是数字') from None
if not (0 <= score <= 100):
raise ValueError(f'{col}={score} 超出 0-100 范围')
return score
valid, invalid = [], []
for row in rows:
try:
scores = {col: parse_score(row, col) for col in ('语文', '数学', '英语')}
valid.append({'学号': row['学号'], '姓名': row['姓名'], **scores})
except ValueError as e:
invalid.append((row['学号'], row['姓名'], str(e)))
print(f'有效记录 {len(valid)} 条,无效记录 {len(invalid)} 条')
for sid, name, reason in invalid:
print(f' {sid} {name}:{reason}')
print('\n=== 汇总报表 ===')
for col in ('语文', '数学', '英语'):
scores = [r[col] for r in valid]
avg = sum(scores) / len(scores)
passed = sum(1 for s in scores if s >= 60)
print(f'{col}:平均 {avg:.1f},及格 {passed}/{len(scores)}({passed / len(scores):.0%})')
with open('grades_valid.json', 'w', encoding='utf-8') as f:
json.dump(valid, f, ensure_ascii=False, indent=2)
print('\n已导出 grades_valid.json,前两条预览:')
print(json.dumps(valid[:2], ensure_ascii=False, indent=2))
运行结果:
有效记录 3 条,无效记录 3 条
003 王强:数学 为空
005 孙丽:语文=103 超出 0-100 范围
006 周明:英语 不是数字
=== 汇总报表 ===
语文:平均 82.7,及格 3/3(100%)
数学:平均 82.7,及格 3/3(100%)
英语:平均 76.3,及格 2/3(67%)
已导出 grades_valid.json,前两条预览:
[
{
"学号": "001",
"姓名": "张伟",
"语文": 88,
"数学": 76,
"英语": 92
},
{
"学号": "002",
"姓名": "李娜",
"语文": 76,
"数学": 82,
"英语": 79
}
]
逐项核对:csv.DictReader 用表头做键,每行直接是字典(第 7 篇),newline='' 保证写入时不多出空行;parse_score 把三类错误统一转成携带明确原因的 ValueError(空、非数字、越界,判断顺序缺一不可——先判空再转 int、越界检查放在最后),from None 压制底层异常的 traceback 链,让报错信息只有业务语义(第四章的异常链知识);调用方一行 try/except ValueError 完成”好行入库、坏行记案”。报表与 JSON 输出的每个数字都来自真实运行:三条合法记录让语数平均 82.7、英语因 004 号 58 分及格率只有 67%。json.dump(..., ensure_ascii=False, indent=2) 是”人读的中文 JSON”标准写法(第 7 篇)。整条链路没有引入任何新知识,全部是本章 1-8 篇的构件。
扩展思考:把校验规则改成”三门总分低于 180 记为不合格”需要动哪几行?如果 CSV 里出现一行列数比表头少(如 007,陈晨,90,80),row['英语'] 会抛什么异常、出现在哪个环节?用第 5 篇的防御思路,这个环节应该加什么保护(提示:检查 row.get('英语') 与 row['英语'] 的区别)?
学习自检清单
完成本章学习后,请逐项确认你能独立做到:
- 说出
w、a、x、r+、wb等模式的语义差异,并解释为什么with块结束后句柄必然关闭、已关闭对象再操作会抛 ValueError(第 1 篇)。 - 区分
read()、readline()、逐行迭代、readlines()、writelines()的返回与用途,说出读到 EOF 时readline()返回什么。 - 每次读写显式指定
encoding,解释 Windows 默认编码(GBK 系)与 UTF-8 不匹配时”静默乱码”与 UnicodeDecodeError 两种后果。 - 用 Path 对象取
parent/name/stem/suffix,用joinpath、exists、glob/rglob完成路径拼接与遍历。 - 画出 OSError 家族的部分层次(FileNotFoundError、PermissionError、IsADirectoryError 等),并在 except 中遵守”子类在前、父类在后”。
- 用
os.makedirs(含exist_ok)、os.walk、os.path.getsize/splitext/join完成目录遍历统计;用os.getenv/os.environ读取环境变量。 - 用
csv.reader/csv.DictReader读、csv.writer写(记得newline=''),知道 CSV 里数字永远是字符串、需要自行转换。 - 用
json.load/json.loads/json.dump/json.dumps,能在ensure_ascii与indent之间按需选择,能捕获json.JSONDecodeError做坏数据容错。 - 用 configparser 按”节/选项”读取配置,理解
getboolean的类型转换与缺失 option 时的 NoOptionError,并会用默认值补齐可选配置(第 8 篇)。 - 独立完成本篇综合任务,能说清”校验通过的数据才允许进入报表与导出”这条流水线的每一步。
如果某一项无法打勾,请回到对应篇章重新练习。第七章的核心思想是:数据只有可靠地落盘、正确地读回、失格即拦截,才谈得上分析——前三篇管”怎么读写”,第四到六篇管”路径与异常”,第七、八篇管”结构化与配置”;第九篇的增量读取和本篇的校验导出,都是这套基本功的实战组合。带着这份清单进入第八章面向对象:你会发现异常与文件知识将继续在类设计、资源管理中发挥作用——尤其是 with 与 __enter__/__exit__,那正是第八章的新主角。