课程章节

实验4.文件与异常综合实验

文件与异常综合实验

本实验把文本读写、with 上下文管理、异常处理、JSON 序列化、CSV 读写串成一个可持久化的成绩册:程序退出后数据仍在,再次打开可继续使用。

实验目标

  1. 会用 open + with 读写文本,理解 r / w / a 的区别。
  2. 会用 try / except / else / finally 处理文件不存在、格式错误。
  3. 会用 json.dump / json.load 保存和恢复字典结构。
  4. 会用 csv.writer / csv.reader 导出、导入表格数据。
  5. 能把实验3中的统计函数复用到文件数据上。

文件规划

文件 作用
students.json 主数据,字典列表,程序启动时加载
report.txt 文本报告,给人看
scores.csv 表格导出,方便用 Excel 打开

参考实现

"""
成绩册持久化
覆盖:文件读写、异常、JSON、CSV、with
"""

import csv
import json

JSON_PATH = 'students.json'
REPORT_PATH = 'report.txt'
CSV_PATH = 'scores.csv'


def load_students(path=JSON_PATH):
    """从 JSON 读取学生列表;文件不存在或格式错误时返回空列表。"""
    try:
        with open(path, 'r', encoding='utf-8') as file:
            data = json.load(file)
    except FileNotFoundError:
        print(f'未找到 {path},将创建新的空成绩册。')
        return []
    except json.JSONDecodeError:
        print(f'{path} 不是合法的 JSON,已忽略并重新开始。')
        return []
    except OSError as e:
        print(f'读取文件失败:{e}')
        return []
    if not isinstance(data, list):
        print('JSON 根节点应为列表,已忽略并重新开始。')
        return []
    return data


def save_students(students, path=JSON_PATH):
    """把学生列表写入 JSON;ensure_ascii=False 便于查看中文。"""
    with open(path, 'w', encoding='utf-8') as file:
        json.dump(students, file, ensure_ascii=False, indent=2)
    print(f'已保存 {len(students)} 条记录到 {path}')


def add_student(students, name, score):
    if not name or not name.strip():
        raise ValueError('姓名不能为空')
    score = float(score)
    if score < 0 or score > 100:
        raise ValueError('成绩必须在 0~100 之间')
    students.append({
        'name': name.strip(),
        'score': score,
    })


def average_score(students):
    if not students:
        return 0
    return sum(s['score'] for s in students) / len(students)


def write_report(students, path=REPORT_PATH):
    """写入人类可读的文本报告。"""
    with open(path, 'w', encoding='utf-8') as file:
        file.write('成绩报告\n')
        file.write('=' * 20 + '\n')
        for student in students:
            file.write(f"{student['name']}: {student['score']:.1f}\n")
        file.write('-' * 20 + '\n')
        file.write(f"人数: {len(students)}\n")
        file.write(f"平均分: {average_score(students):.2f}\n")
    print(f'报告已写入 {path}')


def export_csv(students, path=CSV_PATH):
    with open(path, 'w', encoding='utf-8', newline='') as file:
        writer = csv.writer(file)
        writer.writerow(['姓名', '成绩'])
        for student in students:
            writer.writerow([student['name'], student['score']])
    print(f'已导出 CSV:{path}')


def import_csv(path=CSV_PATH):
    """从 CSV 读取;表头不符或成绩非法时跳过该行。"""
    students = []
    try:
        with open(path, 'r', encoding='utf-8', newline='') as file:
            reader = csv.reader(file)
            header = next(reader, None)
            if header != ['姓名', '成绩']:
                print('CSV 表头应为 姓名,成绩')
                return []
            for row in reader:
                if len(row) != 2:
                    continue
                name, raw_score = row
                try:
                    score = float(raw_score)
                except ValueError:
                    print(f'跳过非法成绩行:{row}')
                    continue
                if 0 <= score <= 100:
                    students.append({'name': name, 'score': score})
    except FileNotFoundError:
        print(f'未找到 {path}')
    return students


def append_log(message, path='op.log'):
    """演示追加模式 a:日志不会覆盖旧内容。"""
    with open(path, 'a', encoding='utf-8') as file:
        file.write(message + '\n')


def main():
    students = load_students()

    # 若是空册,写入几条示例,便于首次演示
    if not students:
        demo = [('张三', 95), ('李四', 82), ('王五', 67)]
        for name, score in demo:
            try:
                add_student(students, name, score)
            except ValueError as e:
                print(f'添加 {name} 失败:{e}')
        append_log('初始化示例数据')

    # 故意触发并捕获:非法成绩
    try:
        add_student(students, '非法', 120)
    except ValueError as e:
        print(f'预期错误已捕获:{e}')
        append_log(f'捕获非法输入:{e}')

    save_students(students)
    write_report(students)
    export_csv(students)

    # 重新导入验证往返一致
    imported = import_csv(CSV_PATH)
    print('CSV 回读条数:', len(imported))
    print('JSON 回读条数:', len(load_students(JSON_PATH)))
    print('平均分:', round(average_score(students), 2))


if __name__ == '__main__':
    main()

预期结果要点

首次运行会创建 students.json、report.txt、scores.csv、op.log。控制台应出现类似:

未找到 students.json,将创建新的空成绩册。
预期错误已捕获:成绩必须在 0~100 之间
已保存 3 条记录到 students.json
报告已写入 report.txt
已导出 CSV:scores.csv
CSV 回读条数:3
JSON 回读条数:3
平均分:81.33

再次运行时不应再打印“未找到 students.json”,记录会从磁盘恢复。

扩展挑战

  1. 增加菜单:1 添加 2 列表 3 导出 4 导入 5 退出,并把选择写入 op.log。
  2. 用 'a' 模式把每次统计结果追加到 history.txt,对比 w 与 a。
  3. 读取损坏的 JSON 时,除了提示,再自动备份坏文件为 students.json.bak。

知识点总结

  1. 打开模式:r 只读,w 截断后写,a 追加;写 CSV 时建议 newline='',避免 Windows 下多出空行;文本文件务必指定 encoding='utf-8'。
  2. with 上下文:with open(...) as file: 在离开代码块时自动关闭文件,即使出现异常也更安全,应优先于手动 close()。
  3. 异常处理:try 放可能出错的代码;按具体异常分支 except FileNotFoundError、json.JSONDecodeError、ValueError;else 在无异常时执行;finally 适合释放资源;业务校验失败用 raise ValueError 主动抛出,让调用方用 except 接住。
  4. JSON:Python 字典/列表与 JSON 对象/数组对应;json.dump / json.load 面向文件,dumps / loads 面向字符串;ensure_ascii=False 保留中文可读性;indent 让文件便于人眼检查。
  5. CSV:标准库 csv 模块;writerow 一行记录,reader 迭代得到列表;导入时要校验表头和字段数,脏数据应跳过并提示,而不是让整个程序崩溃。
  6. 持久化流程:启动加载 → 内存修改(列表/字典)→ 退出或变更后保存;统计类函数只依赖内存中的数据结构,不直接碰磁盘,职责更清晰。
  7. 易错点:用 w 误覆盖整份数据;忘记 encoding 导致中文乱码;except 过宽(裸 except:)吞掉所有错误;JSON 根类型不对仍直接当列表用;CSV 没有处理空行或表头。
  8. 学习重点:能独立完成“读入—校验—修改—写回”的闭环,并知道哪些错误必须处理、哪些应让程序尽早失败。