8-文件操作
约 6391 字大约 21 分钟
2026-10-31
上一章用类重构了班级成绩管理,程序已经很"像样"了——但它有一个致命软肋:所有数据都住在内存里,程序一关,一切清零。四名学生的成绩、精心算出的排名,下次运行时全部要从头录入。数据没有"家",程序就永远只是个一次性计算器。
解决思路就是给数据安个家——文件。回想第9章预告过的知识地图:数据持久化有三个台阶,变量存于内存、关机即失,是第一级;文件保存在磁盘上、关机不丢,是第二级;数据库管理海量数据、查询高效,是第三级。本章就登上第二级台阶,学会用Python读写文件,把成绩数据真正"落袋为安"。同时本章还要补上此前一直按下未表的一个重要知识——异常处理:文件找不到、数据转不成数字,都是程序运行中最常见的"意外",学完异常处理,程序才能从"一碰就碎"变成"处变不惊",这也是走进第9章数据库世界前的必修课。
本章分三步走:第一步学习文件的打开、读取与写入,解决"数据怎么存、怎么取";第二步学习异常处理,解决"出错了怎么办";第三步用文件管理结构化数据,把前两步合龙,开发一个真正能"记住数据"的成绩管理系统。
【学习目标】
知识目标
理解文件在数据持久化中的地位,掌握open打开文件的模式(r/w/a)与encoding参数。
掌握with语句自动关闭文件的用法及其优点。
掌握read、readline、readlines三种读取方法的区别与选用。
理解异常的概念,掌握try-except、try-except-else-finally的语法。
了解常见异常类型,能够阅读并利用报错信息定位问题。
了解raise主动抛出异常的用法和CSV数据格式。
能力目标
能够用with语句读写文本文件,完成数据的保存与读取。
能够用strip、split解析按行组织的CSV格式数据。
能够用try-except捕获并处理文件、输入、转换三类常见异常。
能够开发带异常防护、数据可持久化的文件管理小程序。
素养目标
培养数据安全意识,理解"数据是资产、持久化是底线"。
培养健壮性思维,养成"预判意外、优雅处理"的编程习惯。
体会文件方案的局限,为数据库学习建立问题意识。
1.文件读写基础
1.1 为什么需要文件
程序运行时,变量、列表、字典、对象全部住在内存里。内存就像一块白板,写得快、擦得也快——程序结束(甚至断电),白板就被擦得一干二净。例题7-2的班级数据就是这样:运行时样样都有,运行完荡然无存。
文件存放在磁盘(硬盘)上,磁盘像一本装订好的笔记本,写上去的字不会因为"下课"而消失。Python读写文件的流程可以概括为三步:打开文件(拿到一个文件对象)、读或写、关闭文件。下面逐个击破。
1.2 打开与关闭文件
代码 8-1:open与close的基本用法
f = open("hello.txt", "w", encoding="utf-8") # 打开:没有该文件就新建
f.write("Hello, Python!") # 写入内容
f.close() # 用完必须关闭
f = open("hello.txt", "r", encoding="utf-8") # 重新以读取模式打开
content = f.read()
print(content) # Hello, Python!
f.close()open()的三个参数值得逐个说明:第一个是文件名(相对路径表示与程序文件同目录,也可以写"D:/works/hello.txt"这样的完整路径);第二个是打开模式,决定"读"还是"写";第三个encoding="utf-8"指定编码,不写它,中文很容易变成乱码,请养成每写一个open就带上它的习惯。f.close()负责"合上笔记本"——文件用完不关闭,可能造成数据没有真正写入磁盘、文件被占用无法删除等问题。
打开模式汇总见表8-1。
表 8-1 文件打开模式
| 模式 | 含义 | 文件不存在时 | 文件已存在时 |
|---|---|---|---|
| r | 读取(read) | 报错(FileNotFoundError) | 从头读 |
| w | 写入(write) | 新建文件 | 清空原内容再写 |
| a | 追加(append) | 新建文件 | 在末尾接着写 |
表中藏着一个高频事故:w模式会清空旧文件。本想"添加一条记录",结果手一抖写成"w",整个文件瞬间归零。记不住就记一句口诀:w是重写、a是续写。
每次都惦记close()太累,Python提供了更优雅的with语句:
代码 8-2:with语句——自动关闭
with open("hello.txt", "w", encoding="utf-8") as f:
f.write("Hello, Python!")
f.write("\n人生苦短,我用Python")
with open("hello.txt", "r", encoding="utf-8") as f:
print(f.read())程序运行结果如下:
Hello, Python!
人生苦短,我用Pythonwith语句会在代码块结束时自动调用close(),哪怕中间出了错也不会忘记"合上笔记本"。其实第6章shutil一节已经多次用过这个写法,当时没有展开,现在可以说清原理了。今后读写文件一律用with,这是本书的统一约定。
1.3 从文件读取数据
读取文件有三个方法,先把hello.txt的内容一次性读出来:
代码 8-3:read——一次全读
with open("hello.txt", "r", encoding="utf-8") as f:
text = f.read() # 一次性读出全部内容,存成一个字符串
print(text)程序运行结果如下:
Hello, Python!
人生苦短,我用Python代码 8-4:readline——一次读一行
with open("hello.txt", "r", encoding="utf-8") as f:
line1 = f.readline() # 每调用一次,往后读一行
line2 = f.readline()
print(line1) # Hello, Python!(行尾自带\n,print再补一个,多空一行)
print(line2) # 人生苦短,我用Python程序运行结果如下:
Hello, Python!
人生苦短,我用Python注意line1末尾藏着一个换行符\n,print自己又要换一行,所以输出中多了一个空行——处理行尾换行符是文件操作的基本功,稍后用strip()解决。
代码 8-5:readlines——读成列表
# 先准备一个名单文件
with open("names.txt", "w", encoding="utf-8") as f:
f.write("张三\n李四\n王五\n")
with open("names.txt", "r", encoding="utf-8") as f:
lines = f.readlines() # 每行一个元素,装进列表
print(lines) # ['张三\n', '李四\n', '王五\n']readlines()把整个文件读成字符串列表,一行一个元素,\n也原样保留。第3章学过,列表能遍历、能计数、能索引,所以readlines()特别适合"先把内容拿回来再慢慢处理"的场景。
还有一种更简洁的遍历方式——文件对象本身就能用for循环:
代码 8-6:for直接遍历文件
with open("names.txt", "r", encoding="utf-8") as f:
for line in f: # 每循环一次,line就是一行
name = line.strip() # 去掉行尾的换行符
print(f"你好,{name}!")程序运行结果如下:
你好,张三!
你好,李四!
你好,王五!三种读法怎么选?汇总见表8-2。
表 8-2 三种读取方法对比
| 方法 | 返回值 | 适用场景 |
|---|---|---|
| read() | 一个字符串(全部内容) | 小文件整读,如配置、短文 |
| readline() | 一个字符串(一行) | 逐行处理,控制精细但繁琐 |
| readlines() | 字符串列表(每行一个元素) | 先读回内存再统一处理 |
| for line in f | 每次循环得到一行 | 逐行处理的首选,省内存 |
1.4 向文件写入数据
写入只有两个要点:write()负责写,它不会自动换行,需要换行要自己补\n;w与a模式决定"重写"还是"续写"。
代码 8-7:w模式重写
with open("note.txt", "w", encoding="utf-8") as f:
f.write("第一行内容\n")
f.write("第二行内容\n")
with open("note.txt", "r", encoding="utf-8") as f:
print(f.read())程序运行结果如下:
第一行内容
第二行内容代码 8-8:a模式追加
with open("note.txt", "a", encoding="utf-8") as f:
f.write("第三行内容(追加)\n")
with open("note.txt", "r", encoding="utf-8") as f:
print(f.read())程序运行结果如下:
第一行内容
第二行内容
第三行内容(追加)【例题 8-1】成绩单落盘
题目:把第7章例题7-2的四名学生成绩写入文件scores.txt(第一行写表头,之后一行一名学生、字段用逗号分隔),再读回文件统计每人的总分与平均分。
代码 8-9:写入再读出
# ---------- 第一步:把成绩写入文件 ----------
students = [
["张三", 85, 92, 78, 96],
["李四", 70, 88, 95, 81],
["王五", 60, 74, 82, 90],
["赵六", 55, 66, 71, 68],
]
with open("scores.txt", "w", encoding="utf-8") as f:
f.write("姓名,语文,数学,英语,Python\n") # 第一行:表头
for stu in students:
line = stu[0] # 先放姓名
for score in stu[1:]:
line += "," + str(score) # 拼接各科成绩
f.write(line + "\n") # 一名学生一行
print("成绩单已保存到 scores.txt")
# ---------- 第二步:读出文件并统计 ----------
with open("scores.txt", "r", encoding="utf-8") as f:
lines = f.readlines()
lines = lines[1:] # 跳过表头
print("姓名\t总分\t平均分")
for line in lines:
fields = line.strip().split(",") # 去换行符、按逗号拆字段
name = fields[0]
total = 0
for s in fields[1:]:
total += int(s) # 文件里读出来的都是字符串!
average = round(total / (len(fields) - 1), 1)
print(f"{name}\t{total}\t{average}")程序运行效果如下:
成绩单已保存到 scores.txt
姓名 总分 平均分
张三 351 87.8
李四 334 83.5
王五 306 76.5
赵六 260 65.0这个例题串起了三个关键习惯:写入时用逗号把一条记录拼成一行(str()转换必不可少);读出时strip()清行尾、split(",")拆字段(第3章的split/join一对搭档正式上岗);统计前必须int()转换——从文件里读出来的一切都是字符串,"85"和85相差十万八千里,这是第1章就敲过警钟的知识点。
2.异常处理
2.1 程序为什么会"崩溃"
回想一下全书遇到过的各种报错:
代码 8-10:三个经典的崩溃现场
# 现场1:类型转换失败(第1章埋的伏笔)
age = int("十八岁")
# 现场2:除数为0(第2章例题防御过的)
average = 100 / 0
# 现场3:文件不存在(刚学的open也会这样)
f = open("no_such_file.txt", "r", encoding="utf-8")任选一行运行,程序都会当场中断,打印一大段红色信息,例如现场1的输出:
Traceback (most recent call last):
File "demo.py", line 2, in <module>
age = int("十八岁")
ValueError: invalid literal for int() with base 10: '十八岁'这段信息叫异常(Exception)报告,学会阅读它是自救的第一步:最后一行是关键,格式为"异常类型: 说明"——这里是ValueError(值错误),说明"十八岁"不是合法的数字文本;中间几行是案发现场回放(哪一行代码出的事)。ValueError、ZeroDivisionError、FileNotFoundError……每个异常类型都是Python预先定义好的"事故类别"。
崩溃的意思是:异常一旦发生且没人处理,程序立刻停止,后面的代码全部不执行。对一个要交给用户使用的程序来说,"用户输入个错就整个退出"显然不合格。异常处理要做的,就是把"一碰就碎"变成"出了事,接住它,继续走"。
2.2 try-except捕获异常
代码 8-11:try-except基本用法
try:
age = int(input("请输入年龄:")) # 可能出事的代码放进try
print(f"明年你就{age + 1}岁了")
except ValueError: # 指定要接住的异常类型
print("输入的不是数字,请重新运行并输入数字")
print("程序正常运行结束")程序运行效果如下(输入"十八岁"时):
请输入年龄:十八岁
输入的不是数字,请重新运行并输入数字
程序正常运行结束try可以理解为"下面这段代码可能有险",except就是张在下面的安全网:try里的代码一旦抛出指定类型的异常,程序不崩溃,而是跳进except里执行处理代码,然后继续往下走。注意except后写的是异常类型——接什么类型的网,要和抛出的异常对得上号。
2.3 常见异常类型
把全书见过的报错归类汇总,见表8-3。
表 8-3 常见异常类型速查
| 异常类型 | 触发场景 | 本书伏笔回收 |
|---|---|---|
| ValueError | 值的类型对、内容不合法,如int("十八岁") | 第1章类型转换 |
| TypeError | 类型的操作不匹配,如"5" + 3 | 第1章拼接报错 |
| ZeroDivisionError | 除数为0 | 第2章例题防御 |
| FileNotFoundError | 打开的文件不存在 | 本章open |
| IndexError | 列表索引越界 | 第3章索引 |
| KeyError | 字典访问不存在的键 | 第4章get()防的就是它 |
| ModuleNotFoundError | import的模块不存在 | 第9章pip安装提示 |
其中对文件操作最要紧的是FileNotFoundError,它几乎每个初学者都撞上过——现在终于有了正式的解法:
代码 8-12:捕获FileNotFoundError
try:
with open("no_such_file.txt", "r", encoding="utf-8") as f:
print(f.read())
except FileNotFoundError:
print("文件不存在,请检查文件名")
print("程序没有崩溃,继续往下运行")程序运行结果如下:
文件不存在,请检查文件名
程序没有崩溃,继续往下运行2.4 try-except-else-finally
完整的异常处理结构还有两个可选部分,各司其职:
代码 8-13:完整的try结构
try:
f = open("scores.txt", "r", encoding="utf-8")
except FileNotFoundError:
print("文件不存在")
else:
# try里没出任何错时才执行else
print(f.read())
f.close()
finally:
# 无论出不出错都会执行finally,适合放"善后"代码
print("—— 本次读取流程结束 ——")程序运行效果如下(scores.txt已由例题8-1生成):
姓名,语文,数学,英语,Python
张三,85,92,78,96
李四,70,88,95,81
王五,60,74,82,90
赵六,55,66,71,68
—— 本次读取流程结束 ——四个部分的分工可以用"出门上班"来记:try是"去上班",except是"路上出意外就请假",else是"顺利到岗再干活",finally是"不管到没到岗,晚上都要回家关门"。finally最大的价值是善后:关闭文件、断开连接这类"无论成败都必须做"的事,放在finally里最保险。第9章连接数据库后要执行conn.close(),万一查询中途出错连接就关不上了——把close放进finally,这个问题就迎刃而解,这里先埋个伏笔。
2.5 主动抛出异常raise
异常不仅可以"被动接",还可以"主动抛"。当调用者传来的数据不合法时,与其默默吞掉,不如拉响警报:
代码 8-14:raise主动抛出异常
def set_score(score):
if score < 0 or score > 100:
raise ValueError("成绩必须在0~100之间") # 主动抛出,带上说明
print(f"成绩已记录:{score}")
set_score(85) # 正常:成绩已记录:85
set_score(150) # 150不合法,当场抛出ValueError运行到set_score(150)时程序会像被动异常一样打印报告,最后一行是:
ValueError: 成绩必须在0~100之间抛出的异常同样可以被try接住,用as给异常对象起个名字,就能读出里面的说明文字:
代码 8-15:接住raise抛出的异常
try:
set_score(150)
except ValueError as e:
print(f"录入失败:{e}")程序运行结果如下:
录入失败:成绩必须在0~100之间raise的价值在于把不合法的数据拦在源头:set_score自己把好关,谁来调用都逃不过。这和第7章封装的思想一脉相承——柜台不办不合规的业务,异常就是柜台的"退回单"。
【例题 8-2】带异常防护的成绩录入程序
题目:编写成绩录入程序:循环录入成绩(输入-1结束),录入时拦截非法输入;成绩保存到文件my_scores.txt,写入和读回全程有异常防护;最后统计总分与平均分。
代码 8-16:录入、保存、统计
# ---------- 第一步:录入成绩(哨兵值-1结束) ----------
scores = []
while True:
s = input("请输入成绩(输入-1结束):")
if not s.isdigit(): # 含负号、字母都会到这一支
if s == "-1":
break # 约定的结束信号
print("输入不合法,请输入0~100之间的数字")
continue
score = int(s)
if score > 100:
print("成绩不能超过100")
continue
scores.append(score)
# ---------- 第二步:保存到文件 ----------
try:
with open("my_scores.txt", "w", encoding="utf-8") as f:
for score in scores:
f.write(str(score) + "\n")
print(f"已保存{len(scores)}条成绩")
except OSError:
print("文件写入失败,请检查磁盘和权限")
# ---------- 第三步:读回文件并统计 ----------
try:
with open("my_scores.txt", "r", encoding="utf-8") as f:
saved = []
for line in f:
saved.append(int(line.strip()))
except FileNotFoundError:
print("文件不见了?请重新保存")
else:
total = sum(saved)
print(f"共{len(saved)}条,总分{total},平均分{round(total/len(saved), 1)}")程序运行效果如下:
请输入成绩(输入-1结束):85
请输入成绩(输入-1结束):92
请输入成绩(输入-1结束):abc
输入不合法,请输入0~100之间的数字
请输入成绩(输入-1结束):120
成绩不能超过100
请输入成绩(输入-1结束):78
请输入成绩(输入-1结束):-1
已保存3条成绩
共3条,总分255,平均分85.0程序的三段各有防护:录入段用isdigit()和范围检查双重拦截(第3章的知识在这里正式转正);写入段用OSError兜底(磁盘故障、权限不足都逃不掉);读回段先接FileNotFoundError,再用else安排统计——只有文件顺利读回,统计才有意义。
3.用文件管理结构化数据
3.1 CSV:最通用的纯文本数据格式
例题8-1不经意间用上了业界最通用的数据格式——CSV(Comma-Separated Values,逗号分隔值):第一行是表头,之后一行一条记录,记录内各字段用逗号分隔。Excel能直接打开它,第9章的数据库工具也普遍支持导入导出它。可以说CSV是数据世界的"普通话"。Python还内置了专门的csv模块提供更专业的读写功能,本阶段用split/join手工解析即可,思路完全一致,此处点到为止。
3.2 解析CSV数据
解析CSV的核心动作只有两步:strip()去行尾、split(",")拆字段。以scores.txt为例,找出数学最高分:
代码 8-17:解析成绩单
with open("scores.txt", "r", encoding="utf-8") as f:
lines = f.readlines()
header = lines[0].strip().split(",")
print("表头字段:" + str(header)) # ['姓名', '语文', '数学', '英语', 'Python']
best_name, best_math = "", 0
for line in lines[1:]: # 跳过表头
fields = line.strip().split(",")
name = fields[0]
math = int(fields[2]) # 第3个字段是数学
if math > best_math:
best_name, best_math = name, math
print(f"数学最高分:{best_name}({best_math}分)")程序运行结果如下:
表头字段:['姓名', '语文', '数学', '英语', 'Python']
数学最高分:张三(92分)按索引取字段时,别忘记对照表头数位置:fields[0]是姓名、fields[1]是语文、fields[2]是数学。
3.3 修改与删除记录:全文件重写
文件无法像列表那样"就地修改第几行",想改一条记录,只能采用"读入内存→修改→整体重写"的模式。把张三的数学成绩从92改成95:
代码 8-18:修改一条记录
with open("scores.txt", "r", encoding="utf-8") as f:
lines = f.readlines()
new_lines = []
for line in lines:
fields = line.strip().split(",")
if fields[0] == "张三": # 找到目标行
fields[2] = "95" # 改数学成绩(注意改的是字符串)
line = ",".join(fields) + "\n" # 重新拼回一行(join复习)
new_lines.append(line)
with open("scores.txt", "w", encoding="utf-8") as f: # w模式整体重写
f.writelines(new_lines)
print("修改完成")修改后scores.txt中张三那一行变成了"张三,85,95,78,96"。删除记录的套路完全相同:找到行后不append即可。这套"读回内存、改完重写"的模式简单可靠,请务必掌握——它就是文件版"增删改"的通用心法。
3.4 文件方案的局限与下一步
文件让数据留下来了,但用久了,三个局限会渐渐暴露:
其一,改一条要全重写。代码8-18为了改一个数字,把全文件读进来又整个写回去。记录只有四行无所谓,四万行呢?
其二,查找靠人工遍历。想找"总分最高的学生",只能for循环从头数到尾,数据量大了效率很低。
其三,并发会互相踩踏。两个程序同时打开同一个文件写数据,后写的会覆盖先写的,数据说丢就丢。
这三条局限恰好对应数据库的三大看家本领:高效更新、快速查询、并发安全。数据管理的三级台阶到此走完两级,第三级——数据库,就在下一章。
【例题 8-3】学生成绩文件管理系统
题目:综合运用本章知识,开发一个基于文件的班级成绩管理系统:数据保存在class_scores.txt中,程序启动自动载入,支持显示、查找、添加、删除、修改五项功能,全部操作即时存盘,录入环节带异常防护。菜单结构沿用第5章例题5-3。
代码 8-19:数据读写与录入防护函数
FILE_NAME = "class_scores.txt"
SUBJECTS = ["语文", "数学", "英语", "Python"]
def load_all():
"""从文件读出全部学生,返回字典列表;文件不存在当空名单"""
students = []
try:
with open(FILE_NAME, "r", encoding="utf-8") as f:
for line in f:
fields = line.strip().split(",")
if fields[0] == "姓名": # 跳过表头
continue
scores = []
for s in fields[1:]:
scores.append(int(s))
students.append({"name": fields[0], "scores": scores})
except FileNotFoundError:
pass # 第一次运行还没有文件
return students
def save_all(students):
"""把全部学生写回文件(整体重写模式)"""
with open(FILE_NAME, "w", encoding="utf-8") as f:
f.write("姓名," + ",".join(SUBJECTS) + "\n")
for stu in students:
line = stu["name"]
for s in stu["scores"]:
line += "," + str(s)
f.write(line + "\n")
def input_scores():
"""录入四门成绩,非法输入反复重录"""
scores = []
i = 0
while i < len(SUBJECTS):
s = input(f"请输入{SUBJECTS[i]}成绩(0~100):")
if s.isdigit() and 0 <= int(s) <= 100:
scores.append(int(s))
i += 1
else:
print("输入不合法,请重输")
return scores代码 8-20:功能函数与主程序
def show_student(stu):
"""显示单个学生"""
total = sum(stu["scores"])
avg = round(total / len(stu["scores"]), 1)
parts = []
i = 0
for s in stu["scores"]:
parts.append(SUBJECTS[i] + " " + str(s))
i += 1
print(stu["name"] + ":" + ",".join(parts) + f",总分{total},平均分{avg}")
def show_all(students):
if len(students) == 0:
print("还没有学生记录")
return
for stu in students:
show_student(stu)
def find_student(students):
name = input("请输入要查找的姓名:")
for stu in students:
if stu["name"] == name:
show_student(stu)
return
print(f"没有找到{name}")
def add_student(students):
name = input("请输入新学生姓名:")
for stu in students:
if stu["name"] == name:
print("该姓名已存在")
return
students.append({"name": name, "scores": input_scores()})
save_all(students)
print("添加成功,已保存到文件")
def remove_student(students):
name = input("请输入要删除的姓名:")
for stu in students:
if stu["name"] == name:
students.remove(stu)
save_all(students)
print("删除成功,已保存到文件")
return
print(f"没有找到{name}")
def update_score(students):
name = input("请输入要修改的姓名:")
for stu in students:
if stu["name"] == name:
print("重新录入各科成绩:")
stu["scores"] = input_scores()
save_all(students)
print("修改成功,已保存到文件")
return
print(f"没有找到{name}")
# ---------- 主程序:菜单循环 ----------
students = load_all() # 启动即载入
while True:
print()
print("==== 班级成绩文件管理系统 ====")
print("1.显示全部 2.查找学生 3.添加学生")
print("4.删除学生 5.修改成绩 0.退出")
choice = input("请选择:")
if choice == "1":
show_all(students)
elif choice == "2":
find_student(students)
elif choice == "3":
add_student(students)
elif choice == "4":
remove_student(students)
elif choice == "5":
update_score(students)
elif choice == "0":
print("再见!数据已全部保存在文件中。")
break
else:
print("无效选项,请重新选择")程序运行效果如下:
==== 班级成绩文件管理系统 ====
1.显示全部 2.查找学生 3.添加学生
4.删除学生 5.修改成绩 0.退出
请选择:3
请输入新学生姓名:孙七
请输入语文成绩(0~100):88
请输入数学成绩(0~100):79
请输入英语成绩(0~100):92
请输入Python成绩(0~100):85
添加成功,已保存到文件
==== 班级成绩文件管理系统 ====
1.显示全部 2.查找学生 3.添加学生
4.删除学生 5.修改成绩 0.退出
请选择:1
孙七:语文 88,数学 79,英语 92,Python 85,总分344,平均分86.0
==== 班级成绩文件管理系统 ====
1.显示全部 2.查找学生 3.添加学生
4.删除学生 5.修改成绩 0.退出
请选择:0
再见!数据已全部保存在文件中。这个系统是文件操作的综合演练:load_all()与save_all()封装了"读回内存、整体重写"的心法,五项功能全部即时存盘——程序关掉再打开,孙七还在。对照例题7-2还能发现一个设计差异:这里用"字典列表"而不是Student对象来装数据,因为下一步(第9章)数据库查询返回的正是这种"字段名-值"结构,提前让大家熟悉它的形状。
至此,数据持久化的第二级台阶已经走完。变量→文件→数据库,三级台阶只剩最后一级:数据库如何做到改得快、查得快、并发不乱?第9章见分晓。
