Kodewick
第 23 课,共 31 课目录

读写文件

第 23 课,共 31 课 · 第 4 部分 · 代码和数据 · 约 20 分钟

程序一结束,变量就没了。文件会留下来:今天把点了几盏灯写进文件,明天的程序还能读回来。Python 用 open() 打开文件,然后从里面读,或者往里面写。

这一页的每个例子都在自己的一个虚拟文件夹里跑。文件夹一开始就有页面上列出的文件(比如下面的 lamps.txt),例子写的东西只留在这个例子里,怎么改都弄不坏。在你自己的电脑上,同样的代码读写的是真文件,就在你运行程序的那个文件夹里。

写文件

open(名字, "w") 打开一个文件来写,没有就新建一个。write 往里写文字,close 把文件关上。再用 open(名字) 打开来读,read 把里面的内容整个作为一个字符串返回:

main.py
1f = open("notes.txt", "w")2f.write("点了 4 盏灯\n")3f.write("还剩 2 桶灯油\n")4f.close()5 6f = open("notes.txt")7print(f.read(), end="")8f.close()
输出
点了 4 盏灯
还剩 2 桶灯油

write 不会自己换行,所以每行末尾写上 \n。文件最后已经有一个换行了,end="" 让 print 别再多加一个。

close 很容易忘;要是在它之前出了错,文件可能一直关不上。with 代码块结束时会替你关上文件,就算是被异常打断也一样。从现在起都这样打开文件:

main.py
1with open("notes.txt", "w") as f:2    f.write("点了 4 盏灯\n")3    f.writelines(["还剩 2 桶灯油\n", "灯泡 7 个\n"])4print(f.closed)5 6with open("notes.txt") as f:7    print(f.read(), end="")
输出
True
点了 4 盏灯
还剩 2 桶灯油
灯泡 7 个

writelines 把列表里的字符串一个接一个写进去;和 write 一样,它自己不加 \n。

读文件

文件夹里本来就有这个文件,这一页的每个例子一开始都有它:

lamps.txt
榆树街
橡树巷
磨坊路
港湾路

read() 把整个文件作为一个字符串返回,.splitlines() 再把它切成一行一行。readline() 只读下一行,\n 也带着;readlines() 把剩下的行放进一个列表:

main.py
1with open("lamps.txt") as f:2    text = f.read()3print(text.splitlines())4 5with open("lamps.txt") as f:6    print(repr(f.readline()))7    print(f.readlines())
输出
['榆树街', '橡树巷', '磨坊路', '港湾路']
'榆树街\n'
['橡树巷\n', '磨坊路\n', '港湾路\n']

repr() 显示字符串的时候连引号和 \n 一起显示,想看清到底读到了什么时很有用。最常用的是直接遍历文件,一次拿一行,多大的文件都能这么读:

main.py
1with open("lamps.txt") as f:2    for line in f:3        print("点亮" + line.strip())
输出
点亮榆树街
点亮橡树巷
点亮磨坊路
点亮港湾路
先猜一猜

不写 .strip(),有什么不一样?

1with open("lamps.txt") as f:2    for line in f:3        print(line)

文件模式和追加

open 的第二个参数是模式:

模式意思文件已经有了文件还没有
"r"读(不写就是它)从头读FileNotFoundError
"w"写先清空新建
"a"追加加在末尾新建

"w" 要小心:用它一打开,原来的内容就没了,你一个字还没写。想往文件里加东西,用 "a":

main.py
1with open("lamps.txt", "a") as f:2    f.write("采石场路\n")3with open("lamps.txt") as f:4    print(f"{len(f.readlines())} 条街")5 6with open("lamps.txt", "w") as f:7    f.write("矿场大门\n")8with open("lamps.txt") as f:9    print(f.read(), end="")
输出
5 条街
矿场大门

小技巧: 在自己电脑上,文件里有汉字时,加上 encoding="utf-8":open("lamps.txt", "a", encoding="utf-8")。不然有的系统(尤其是 Windows)会用别的编码读写,汉字就成了乱码。

文件不存在

读一个不存在的文件,会抛出 FileNotFoundError。用第 21 课的 try 接住它:这里没有存档,就从头开始。

main.py
1try:2    with open("save.txt") as f:3        level = f.read()4except FileNotFoundError:5    level = "1"6print(f"从第 {level} 关开始")7 8open("lamp.txt")
输出
从第 1 关开始
Traceback (most recent call last):
  File "main.py", line 8, in <module>
    open("lamp.txt")
FileNotFoundError: [Errno 2] No such file or directory: 'lamp.txt'

发生了什么: 程序在这里停下来了。

最后一行外面没有 try,文件名拼错了,程序就停了。报错里写着它找的是哪个文件,先查这两样:名字拼对了没有,文件在不在程序运行的那个文件夹里。

读写数字

文件里存的是文字,所以 write 只收字符串,f.write(12) 是 TypeError。写进去的时候用 str() 变成文字,读出来再用 int() 或 float() 变回数字:

main.py
1readings = [12, 7, 30]2with open("meter.txt", "w") as f:3    for r in readings:4        f.write(str(r) + "\n")5 6total = 07with open("meter.txt") as f:8    for line in f:9        total += int(line)10print(f"总数:{total}")
输出
总数:49

int() 不管数字两边的空格和 \n,所以 int("12\n") 就是 12。不过空行还是会 ValueError。

CSV 文件

CSV(逗号分隔值)文件是用纯文字写的表格:一行一条记录,列和列之间用逗号隔开,第一行常常是列名。电子表格软件能打开,也能存成这种格式。文件夹里也有一个:

stock.csv
name,count
矿石,12
灯油,5
灯芯,40
玻璃,3

csv 模块替你把每行拆开。csv.reader 把每行变成一个列表;csv.DictReader 把第一行当作名字,把每行变成一个字典:

main.py
1import csv2 3with open("stock.csv", newline="") as f:4    for row in csv.reader(f):5        print(row)6 7with open("stock.csv", newline="") as f:8    for row in csv.DictReader(f):9        if int(row["count"]) < 10:10            print("要补货:" + row["name"])
输出
['name', 'count']
['矿石', '12']
['灯油', '5']
['灯芯', '40']
['玻璃', '3']
要补货:灯油
要补货:玻璃

读出来的值都是字符串,所以比大小之前要 int()。打开 CSV 文件时写上 newline="",换行交给 csv 模块自己处理。写反过来就行:csv.writer 写列表,csv.DictWriter 写字典。

main.py
1import csv2 3orders = [{"name": "灯油", "count": 20}, {"name": "玻璃", "count": 10}]4with open("orders.csv", "w", newline="") as f:5    writer = csv.DictWriter(f, fieldnames=["name", "count"])6    writer.writeheader()7    writer.writerows(orders)8 9with open("orders.csv") as f:10    print(f.read(), end="")
输出
name,count
灯油,20
玻璃,10

JSON

JSON 是一种存嵌套数据的文字格式:字典、列表、字符串、数字、True/False 和 None(JSON 里写作 true、false 和 null)。很多程序和网站都用它交换数据。json.dump 把一个值写进文件,json.load 读回来,类型都不变(大体如此:元组读回来会变成列表);dumps 和 loads(多一个 s,表示字符串)对字符串做同样的事。

main.py
1import json2 3town = {"name": "像素小镇", "lamps": 4, "streets": ["榆树街", "橡树巷"], "lit": True}4with open("town.json", "w") as f:5    json.dump(town, f, ensure_ascii=False)6 7with open("town.json") as f:8    loaded = json.load(f)9print(loaded["streets"])10print(loaded == town)11 12text = json.dumps(town, ensure_ascii=False)13print(text)
输出
['榆树街', '橡树巷']
True
{"name": "像素小镇", "lamps": 4, "streets": ["榆树街", "橡树巷"], "lit": true}

和 CSV 不一样,数字读回来还是数字,列表还是列表。JSON 默认把汉字写成 \u50cf 这样的编码,ensure_ascii=False 让汉字原样写出来。json.dumps(town, indent=2) 把文字分成好几行,更好读。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。每道题的文件夹里一开始都有 lamps.txt 和 stock.csv。

练习 1

数一数有几条街

读 lamps.txt,显示里面一共有几条街。

要显示
4 条街
exercise1.py
1count = 02# 在这里读 lamps.txt3print(f"{count} 条街")
输出
点「运行」或按 Ctrl+Enter
练习 2

再加一条街

在 lamps.txt 末尾追加一行 采石场路,然后读这个文件,给每条街带上编号显示出来。

要显示
1 榆树街
2 橡树巷
3 磨坊路
4 港湾路
5 采石场路
exercise2.py
1with open("lamps.txt") as f:2    for line in f:3        print(line.strip())
输出
点「运行」或按 Ctrl+Enter
练习 3

库存一共多少

用 csv.DictReader 读 stock.csv,把 count 这一列加起来。

要显示
总数:60
exercise3.py
1import csv2 3total = 04print(f"总数:{total}")
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • open(名字, 模式) 打开文件;with open(...) as f: 替你关上文件,出了错也会关。
  • read() 读全部,readline() 读一行,readlines() 读成列表;for line in f: 一行一行读,每行末尾带着 \n。
  • "w" 先清空再写,"a" 加在末尾,"r" 读;文件不存在会抛出 FileNotFoundError。
  • 文件里存的是文字:数字写进去用 str(),读出来用 int() 或 float()。
  • csv 读写表格(reader、writer、DictReader、DictWriter);json 存嵌套数据(dump、load、dumps、loads)。