第 7 周:文件读写与异常处理
程序中的数据不能只靠手工输入。本周学习从文本文件和 CSV 文件读取数据,把结果写到新文件,并让程序在文件不存在、格式错误等情况下给出清楚提示。
建议用时:8-10 小时。
本周目标#
学完本周,你应该能够:
- 区分绝对路径、相对路径和当前工作目录。
- 使用
with open()安全打开文件。 - 理解
r、w、a三种模式的差异。 - 使用
read()、readline()、readlines()和逐行遍历。 - 使用
write()、writelines()和print(..., file=f)写文件。 - 解析以固定分隔符组织的文本行。
- 使用
csv.DictReader读取 CSV。 - 捕获常见异常,并输出可理解的错误信息。
- 独立完成成绩文件到报告文件的转换。
7.1 路径与当前工作目录#
文件路径告诉程序到哪里找文件。
绝对路径#
从磁盘根目录开始,完整描述文件位置:
/Users/kukuaki/Desktop/study/py/模拟赛/data/stage2_scores.txt
换一台电脑或换一个用户目录后,这个路径可能不存在,所以程序不应随意写死个人电脑的绝对路径。
相对路径#
相对于当前工作目录的路径:
模拟赛/data/stage2_scores.txt
如果程序从 /Users/kukuaki/Desktop/study/py 运行,上面的路径能指向目标文件。如果从其他目录运行,同一行代码可能找不到文件。
“代码文件在哪”和“程序从哪运行”不总是同一件事。遇到 FileNotFoundError 时,先确认当前工作目录。
检查当前目录#
可以使用:
import os
print(os.getcwd())
本周先会读结果。第 8 周会系统学习 os.path.join()、os.makedirs() 等路径和目录操作。
7.2 with open() 的基本结构#
with open("data.txt", "r", encoding="utf-8") as file:
content = file.read()
print(content)
参数含义:
"data.txt":文件路径。"r":只读模式。encoding="utf-8":按 UTF-8 读取文字。file:文件对象名称,可以换成f、source等。
with 代码块结束时会自动关闭文件,即使中途发生异常也会执行关闭操作。比手工写 file.close() 更安全。
7.3 打开模式#
| 模式 | 含义 | 文件不存在时 | 文件已存在时 |
|---|---|---|---|
"r" |
只读 | 报 FileNotFoundError |
从头读取 |
"w" |
只写 | 创建新文件 | 清空原内容 |
"a" |
追加 | 创建新文件 | 在末尾继续写 |
"r+" |
读写 | 报错 | 从开头读写 |
"x" |
独占创建 | 创建文件 | 报 FileExistsError |
初学阶段最常使用 "r"、"w"、"a"。
"w" 很危险:只要成功打开,原内容立即被清空。修改输入数据时,应写入新文件,例如:
input_path = "scores.txt"
output_path = "scores_report.txt"
不要为了省事把结果写回输入文件,除非题目明确要求并且已经备份。
7.4 一次读取全部内容:read()#
假设 hello.txt 内容是:
第一行
第二行
第三行
程序:
with open("hello.txt", "r", encoding="utf-8") as file:
content = file.read()
print(content)
content 是一个包含换行符的字符串。读取后文件指针到达末尾,再次 read() 会得到空字符串:
with open("hello.txt", "r", encoding="utf-8") as file:
first = file.read()
second = file.read()
print(repr(first))
print(repr(second))
第二次读取结果为空,是因为读取位置已经在文件末尾。不是文件突然没了内容。
7.5 逐行读取#
readline()#
每次读取一行:
with open("hello.txt", "r", encoding="utf-8") as file:
line1 = file.readline()
line2 = file.readline()
print(repr(line1))
print(repr(line2))
每行通常保留末尾换行符。清理两端空白:
clean_line = line1.strip()
readlines()#
一次读取所有行,返回字符串列表:
with open("hello.txt", "r", encoding="utf-8") as file:
lines = file.readlines()
print(lines)
# ['第一行\n', '第二行\n', '第三行']
直接遍历文件#
这是处理大文件最常用的形式:
with open("hello.txt", "r", encoding="utf-8") as file:
for line in file:
clean_line = line.strip()
if clean_line:
print(clean_line)
它每次只加载一行,不必一次把所有内容放进内存。
7.6 写文本文件#
写入新文件#
with open("output.txt", "w", encoding="utf-8") as file:
file.write("第一行\n")
file.write("第二行\n")
write() 不会自动添加换行,必须自己写 \n。
自动换行:print(..., file=...)#
with open("output.txt", "w", encoding="utf-8") as file:
print("第一行", file=file)
print("第二行", file=file)
print() 默认在末尾添加换行,并把内容写到指定文件对象。
写入多行:writelines()#
lines = ["第一行\n", "第二行\n", "第三行\n"]
with open("output.txt", "w", encoding="utf-8") as file:
file.writelines(lines)
writelines() 不会自动为每个字符串加换行,列表元素必须自己带 \n,或者提前处理。
追加内容#
with open("output.txt", "a", encoding="utf-8") as file:
file.write("追加的内容\n")
使用 "a" 不会清空原文件。
7.7 解析固定格式文本#
第二阶段成绩文件的每行格式:
学号|姓名|班级|信息科技|数学|英语
示例:
S001|张晨|高一(1)班|86|91|78
S002|李雨|高一(1)班|92|88|90
读取一行并拆分:
line = "S001|张晨|高一(1)班|86|91|78"
parts = line.strip().split("|")
student_id = parts[0]
name = parts[1]
class_name = parts[2]
scores = [int(parts[3]), int(parts[4]), int(parts[5])]
print(student_id, name, class_name, scores)
为什么先 strip()#
strip() 去掉行末换行符和两端空白,避免最后一列变成 "78\n"。虽然 int("78\n") 通常也能工作,但先清理能让字段比较和输出更稳定。
为什么先检查字段数#
parts = line.strip().split("|")
if len(parts) != 6:
print("字段数量不正确:", len(parts))
else:
# 继续处理
pass
如果某一行缺字段,直接访问 parts[5] 会报 IndexError。好的数据处理程序要能指出哪一行格式有问题。
7.8 读取 CSV#
CSV 是用逗号分隔字段的文本文件。第三阶段的输入 CSV 表头是:
reader_id,name,class_name,category,borrow_count,overdue_count
数据示例:
R001,林晓,高一(1)班,文学,8,1
R002,韩梅,高一(1)班,科技,4,0
使用 csv.DictReader:
import csv
with open("readers.csv", "r", encoding="utf-8", newline="") as file:
reader = csv.DictReader(file)
for row in reader:
print(row["reader_id"], row["name"])
print(row["borrow_count"], type(row["borrow_count"]).__name__)
要点:
DictReader使用第一行表头作为键。- 每一行得到字典。
- CSV 读出来的所有值都是字符串,数字仍要
int()。 - 读取 CSV 时建议加
newline="",让csv模块正确处理换行。
转换数字:
borrow_count = int(row["borrow_count"])
overdue_count = int(row["overdue_count"])
按位置读取:csv.reader#
import csv
with open("readers.csv", "r", encoding="utf-8", newline="") as file:
reader = csv.reader(file)
header = next(reader)
for row in reader:
print(row)
next(reader) 取到表头,后续循环从第一行数据开始。字段多或题目标明列名时,DictReader 更不容易把列顺序记错。
7.9 异常处理#
文件程序最容易遇到:
| 异常 | 典型原因 |
|---|---|
FileNotFoundError |
文件路径不存在 |
PermissionError |
没有读写权限 |
UnicodeDecodeError |
文件编码与声明不一致 |
ValueError |
int() 收到非数字 |
IndexError |
字段不足却按固定索引访问 |
KeyError |
CSV 表头缺少需要的列 |
基础结构:
try:
with open("data.txt", "r", encoding="utf-8") as file:
content = file.read()
except FileNotFoundError:
print("错误:没有找到 data.txt")
except PermissionError:
print("错误:没有权限读取 data.txt")
程序仍然继续执行后面的代码,而不是直接终止。
捕获异常不代表隐藏错误#
不推荐:
try:
score = int(text)
except Exception:
pass
这会吞掉所有错误,连变量名写错也可能被掩盖。应该只捕获预期异常,并给出可理解提示:
try:
score = int(text)
except ValueError:
print(f"错误:{text!r} 不是整数")
score = None
{text!r} 会用引号显示原值,方便定位空格或隐藏字符。
else 与 finally#
try:
number = int("12")
except ValueError:
print("转换失败")
else:
print("转换成功:", number)
finally:
print("无论成功还是失败都会执行")
基础文件题通常使用 with 加 try/except 就足够,不必为了“高级”强行加入 finally。
7.10 读入、处理、写出#
处理文件的推荐顺序:
- 明确输入路径和输出路径。
- 读取并解析数据。
- 在内存中完成计算。
- 检查结果数量和字段格式。
- 打开输出文件写入。
- 重新读取输出文件或用编辑器检查。
不要一边逐行读取,一边随意覆盖输入文件。保持“输入只读、输出另存”更容易验证。
7.11 本周作品:成绩报告生成器#
需求#
读取:
模拟赛/data/stage2_scores.txt
每行包含学号、姓名、班级和三科成绩。要求:
- 每个人输出一行:
学号 姓名 总分。 - 按班级第一次出现的顺序输出平均分,保留两位小数。
- 输出最高总分学生;并列时取文件中先出现者。
完整参考程序#
input_path = "模拟赛/data/stage2_scores.txt"
output_path = "模拟赛/output/第二阶段_成绩报告.txt"
students = []
class_totals = {}
try:
with open(input_path, "r", encoding="utf-8") as source:
for line_number, line in enumerate(source, start=1):
clean_line = line.strip()
if not clean_line:
continue
parts = clean_line.split("|")
if len(parts) != 6:
raise ValueError(
f"第 {line_number} 行应有 6 个字段,实际有 {len(parts)} 个"
)
student_id, name, class_name = parts[:3]
scores = [int(parts[3]), int(parts[4]), int(parts[5])]
total = sum(scores)
student = {
"student_id": student_id,
"name": name,
"class_name": class_name,
"total": total,
}
students.append(student)
if class_name not in class_totals:
class_totals[class_name] = {"total": 0, "count": 0}
class_totals[class_name]["total"] += total
class_totals[class_name]["count"] += 1
if not students:
raise ValueError("输入文件中没有学生数据")
highest_student = students[0]
for student in students[1:]:
if student["total"] > highest_student["total"]:
highest_student = student
with open(output_path, "w", encoding="utf-8") as target:
for student in students:
print(
student["student_id"],
student["name"],
student["total"],
file=target,
)
print("班级平均分", file=target)
for class_name, data in class_totals.items():
average = data["total"] / data["count"]
print(f"{class_name} {average:.2f}", file=target)
print(
"最高总分:"
f"{highest_student['student_id']} "
f"{highest_student['name']} "
f"{highest_student['total']}",
file=target,
)
print("报告已生成:", output_path)
except FileNotFoundError:
print("错误:找不到输入文件", input_path)
except ValueError as error:
print("数据处理错误:", error)
except PermissionError:
print("错误:没有权限读取或写入文件")
运行前检查#
- 当前工作目录是否是
study/py。 模拟赛/output/目录是否存在。- 输入文件编码是否为 UTF-8。
- 输入每行是否有 6 个字段。
路径和目录创建会在第 8 周进一步强化。若输出目录不存在,open(..., "w") 会报 FileNotFoundError,它不会自动创建多层目录。
验证输出#
报告核心内容应与参考答案一致。另做以下测试:
| 测试 | 修改内容 | 期望 |
|---|---|---|
| 正常数据 | 原文件 | 生成完整报告 |
| 空文件 | 所有行删除 | 提示没有学生数据 |
| 缺字段 | 某行少一列 | 指出具体行号 |
| 非数字 | 某科成绩写成 ABC |
提示数据处理错误 |
| 最高分并列 | 两名学生总分相同 | 输出先出现者 |
7.12 独立练习#
A. 快速判断题#
"r"和"w"的主要区别是什么?- 打开已存在文件使用
"w"会发生什么? with open()代码块结束后会自动做什么?read()和readlines()返回类型分别是什么?write("abc")会自动换行吗?- 文件每行末尾的
\n怎样去除? - CSV 经
DictReader读取后,数字字段的类型默认是什么? FileNotFoundError通常说明什么?- 为什么不应该直接捕获
Exception后pass? - 为什么修改工作簿或数据文件时应输出到新文件?
B. 编程题#
- 创建
names.txt,写入三个姓名,每行一个。 - 读取
names.txt,输出去掉行末换行后的姓名。 - 统计一个文本文件有多少非空行。
- 读取三行整数,计算总和并写入新文件。
- 读取一份用
|分隔的成绩文件,输出每个人总分。 - 用
csv.DictReader读取借阅数据,输出每个读者姓名和借阅次数。 - 文件不存在时给出清楚提示,而不是直接终止。
- 把一组结果列表写入文本文件,每行一项。
7.13 练习答案与解释#
A. 快速判断题答案#
"r"只读;"w"写入且会清空已有内容。- 原内容立即被清空,文件变为空文件。
- 自动关闭文件。
read()返回字符串;readlines()返回字符串列表。- 不会,需要自行在字符串中加
\n,或使用print(..., file=f)。 - 使用
strip(),或只想移除换行时使用rstrip("\n")。 str。- 指定路径的文件不存在,或路径写法、当前工作目录不对。
- 它会隐藏程序错误,导致后续得到错误结果却不知道原因。
- 避免清空或损坏原始数据,便于对比和重新测试。
B. 编程题参考答案#
写入姓名:
with open("names.txt", "w", encoding="utf-8") as file: file.write("张晨\n") file.write("李雨\n") file.write("王浩\n")读取姓名:
with open("names.txt", "r", encoding="utf-8") as file: for line in file: print(line.strip())统计非空行:
count = 0 with open("data.txt", "r", encoding="utf-8") as file: for line in file: if line.strip(): count += 1 print(count)三行整数求和:
total = 0 with open("numbers.txt", "r", encoding="utf-8") as file: for line in file: total += int(line.strip()) with open("sum.txt", "w", encoding="utf-8") as file: print(total, file=file)成绩总分:
with open("scores.txt", "r", encoding="utf-8") as file: for line in file: parts = line.strip().split("|") if len(parts) == 6: total = sum(int(value) for value in parts[3:]) print(parts[0], parts[1], total)这里使用了生成器表达式。若还不熟,可以写显式循环逐个转换。
读取借阅次数:
import csv with open( "模拟赛/data/stage3_borrowings.csv", "r", encoding="utf-8", newline="", ) as file: reader = csv.DictReader(file) for row in reader: print(row["name"], row["borrow_count"])文件不存在提示:
try: with open("missing.txt", "r", encoding="utf-8") as file: print(file.read()) except FileNotFoundError: print("错误:missing.txt 不存在")每行写一项:
results = ["优秀", "良好", "及格"] with open("results.txt", "w", encoding="utf-8") as file: for item in results: print(item, file=file)
7.14 常见错误清单#
- 相对路径依赖当前工作目录,却从错误目录运行程序。
- 使用
"w"打开输入文件,导致原内容丢失。 - 以为
write()会自动换行。 - 以为
writelines()会自动补换行。 - 忘记处理行末
\n,导致输出中出现空行。 - 按固定索引访问字段,却没有先检查字段数。
- CSV 读出的数字仍是字符串,直接参与错误类型的计算。
read()读两次,第二次得到空字符串却以为文件损坏。- 捕获所有异常并静默忽略。
- 结果目录不存在时直接写文件失败。
7.15 过关检查#
- 能解释相对路径和当前工作目录。
- 能正确写出
with open(..., encoding="utf-8")。 - 能说清
r、w、a的行为。 - 能分别使用
read()、逐行循环和readlines()。 - 能写出带换行的文本文件。
- 能用
split()解析固定字段,并检查字段数量。 - 能用
csv.DictReader读取带表头的 CSV。 - 能捕获
FileNotFoundError、ValueError和权限错误。 - 能完成成绩报告并验证正常、空文件、缺字段和非法数字。
- 明确知道输出文件不能覆盖输入原件。